[
  {
    "path": ".fvmrc",
    "content": "{\n  \"flutter\": \"3.13.9\"\n}"
  },
  {
    "path": ".github/workflows/Package.core.yml",
    "content": "name: Package-core\non:\n  workflow_dispatch:\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  GO_VERSION: \"1.24\"\n  NDK_VERSION: \"23.1.7779620\"\n\njobs:\n  ci-pass:\n    name: CI is green\n    runs-on: ubuntu-latest\n    needs:\n      - build_release_assets\n    steps:\n      - run: exit 0\n\n  build_release_assets:\n    name: Build and upload assets\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - target: ios\n            host: macos-latest\n          - target: android\n            host: ubuntu-latest\n    runs-on: ${{ matrix.config.host }}\n    env:\n      TARGET: ${{ matrix.config.target }}\n    steps:\n      - name: Setup golang\n        uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Cache go modules (Linux)\n        if: matrix.config.host == 'ubuntu-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (macOS)\n        if: matrix.config.host == 'macos-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/Library/Caches/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ env.BRANCH }}\n\n      - name: Checkout core\n        uses: actions/checkout@v3\n        with:\n          repository: 'niuhuan/pikapika-go-core'\n          token: ${{ secrets.CORE_TOKEN }}\n          path: 'go'\n\n      - name: Install go mobile (mobile)\n        if: matrix.config.target == 'ios' || matrix.config.target == 'android'\n        run: |\n          go install golang.org/x/mobile/cmd/gomobile@latest\n\n      - name: Build (ios)\n        if: matrix.config.target == 'ios'\n        run: |\n          sh scripts/bind-ios.sh\n\n      - name: Setup java (Android)\n        if: matrix.config.target == 'android'\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n\n      - name: Setup android tools (Android)\n        if: matrix.config.target == 'android'\n        uses: android-actions/setup-android@v3\n        with:\n          cmdline-tools-version: 8512546\n          packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.NDK_VERSION}}'\n\n      - name: Build (android)\n        if: matrix.config.target == 'android'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.NDK_VERSION }}\n          bash scripts/bind-android-debug.sh\n\n      - name: Upload Asset (All)\n        uses: actions/upload-artifact@v4\n        with:\n          name: ${{ matrix.config.target }}-${{ github.run_number }}\n          path: 'go/mobile/lib'\n          retention-days: 3\n\n"
  },
  {
    "path": ".github/workflows/Package.yml",
    "content": "name: Build\n\non:\n  workflow_dispatch:\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  ANDROID_NDK_VERSION: \"23.1.7779620\"\n  GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46 # v0.0.0-20220722155234-aaac322e2105\n\njobs:\n\n  ci-pass:\n    name: CI is green\n    runs-on: ubuntu-latest\n    needs:\n      - build_release_assets\n    steps:\n      - run: exit 0\n\n  build_release_assets:\n    name: Build release assets\n    strategy:\n      fail-fast: false\n      matrix:\n        sources:\n          - branch: master\n        config:\n          - target: windows\n            host: windows-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: macos\n            host: macos-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: linux\n            host: ubuntu-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: ios\n            host: macos-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n          - target: android-arm32\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n          - target: android-arm64\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n          - target: android-x86_64\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n\n\n    runs-on: ${{ matrix.config.host }}\n\n    env:\n      TARGET: ${{ matrix.config.target }}\n      FLUTTER_VERSION: ${{ matrix.config.flutter_version }}\n      BRANCH: ${{ matrix.sources.branch }}\n      go_version: ${{ matrix.config.go_version }}\n\n    steps:\n\n      # Setup golang env and cache go module\n\n      - name: Setup golang\n        uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n\n      - name: Cache go modules (Windows)\n        if: matrix.config.host == 'windows-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~\\AppData\\Local\\go-build\n            ~\\go\\pkg\\mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (Linux)\n        if: matrix.config.host == 'ubuntu-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (macOS)\n        if: matrix.config.host == 'macos-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/Library/Caches/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      # checkout\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ env.BRANCH }}\n\n      # check_access\n\n      - id: check_asset\n        name: Check asset\n        run: |\n          echo \"::set-output name=skip_build::false\"\n\n      #\n\n      - name: Check core\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: actions/checkout@v3\n        with:\n          repository: 'niuhuan/pikapika-go-core'\n          token: ${{ secrets.CORE_TOKEN }}\n          path: 'go'\n\n      - name: Setup flutter\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          architecture: x64\n\n      - name: Cache Flutter dependencies (Linux/Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' || matrix.config.target == 'linux' )\n        uses: actions/cache@v3\n        with:\n          path: /opt/hostedtoolcache/flutter\n          key: ${{ runner.os }}-flutter\n\n      - name: Cache Flutter dependencies (Mac host)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'macos' )\n        uses: actions/cache@v3\n        with:\n          path: /Users/runner/hostedtoolcache/flutter\n          key: ${{ runner.os }}-flutter\n\n      - name: Cache Gradle dependencies (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') == false\n        uses: actions/setup-java@v3\n        with:\n          java-version: ${{ matrix.config.java }}\n          distribution: 'temurin'\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2')\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n\n      - name: Setup android tools (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: android-actions/setup-android@v3\n        with:\n          cmdline-tools-version: 8512546\n          packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.ANDROID_NDK_VERSION }}'\n\n      - name: Setup msys2 (Windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        uses: msys2/setup-msys2@v2\n        with:\n          install: gcc make\n\n      - name: Install dependencies (Linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        env:\n          ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'\n        run: |\n          curl -JOL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage\n          chmod a+x appimagetool-x86_64.AppImage\n          mkdir -p ${GITHUB_WORKSPACE}/bin\n          mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool\n          echo ::add-path::${GITHUB_WORKSPACE}/bin\n          sudo apt-get update\n          sudo apt-get install -y libgl1-mesa-dev xorg-dev libfuse2 locate\n\n      - name: Install hover (desktop)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos')\n        run: |\n          go install github.com/go-flutter-desktop/hover@latest\n\n      - name: Install go mobile (mobile)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-arm32' || matrix.config.target == 'android-x86_64' )\n        run: |\n          go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }}\n\n      - name: Set-Version (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          cd ci\n          cp version.code.txt ../lib/assets/version.txt\n\n      - name: Upgrade deps version (flutter2 non-mac)\n        if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') == false && startsWith(matrix.config.flutter_version, '2')\n        run: |\n          sed -i \"s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g\" pubspec.yaml\n          sed -i \"s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g\" pubspec.yaml\n          sed -i \"s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g\" pubspec.yaml\n          sed -i \"s/file_picker: 5.2.5/file_picker: 4.6.1/g\" pubspec.yaml\n          sed -i \"s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g\" pubspec.yaml\n          sed -i \"s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g\" pubspec.yaml\n          sed -i \"s/Icons.energy_savings_leaf/Icons.ad_units/g\" lib/screens/SettingsScreen.dart\n          sed -i \"s/gradle-7.5-bin.zip/gradle-6.7.1-all.zip/g\" android/gradle/wrapper/gradle-wrapper.properties\n          sed -i \"s/com.android.tools.build:gradle:7.2.0/com.android.tools.build:gradle:4.1.0/g\" android/build.gradle\n          sed -i \"s/1.7.10/1.3.50/g\" android/app/build.gradle\n          sed -i \"s/fontFamilyFallback/\\/\\/fontFamilyFallback/g\" lib/basic/config/Themes.dart\n          sed -i \"s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g\" pubspec.yaml\n          sed -i \"s/thumbVisibility: true/isAlwaysShown: true/g\" lib/basic/config/ShadowCategories.dart\n          flutter pub get\n\n      - name: Upgrade deps version (flutter2 mac)\n        if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') && startsWith(matrix.config.flutter_version, '2')\n        run: |\n          brew install gnu-sed\n          gsed -i \"s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g\" pubspec.yaml\n          gsed -i \"s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g\" pubspec.yaml\n          gsed -i \"s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g\" pubspec.yaml\n          gsed -i \"s/file_picker: 5.2.5/file_picker: 4.6.1/g\" pubspec.yaml\n          gsed -i \"s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g\" pubspec.yaml\n          gsed -i \"s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g\" pubspec.yaml\n          gsed -i \"s/Icons.energy_savings_leaf/Icons.ad_units/g\" lib/screens/SettingsScreen.dart\n          gsed -i \"s/fontFamilyFallback/\\/\\/fontFamilyFallback/g\" lib/basic/config/Themes.dart\n          gsed -i \"s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g\" pubspec.yaml\n          gsed -i \"s/thumbVisibility: true/isAlwaysShown: true/g\" lib/basic/config/ShadowCategories.dart\n          flutter pub get\n\n      - name: Build (windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        run: |\n          hover build windows\n          curl -JOL https://github.com/ComicSparks/build-tools/releases/download/storage/Resource_Hacker_5.1.8.zip\n          Expand-Archive .\\Resource_Hacker_5.1.8.zip\n          cmd /c \"Resource_Hacker_5.1.8\\ResourceHacker.exe\" -open go\\build\\outputs\\windows-release\\pikapika.exe -save go\\build\\outputs\\windows-release\\pikapika.exe -action addskip -res go/assets/icon.ico -mask ICONGROUP,MAINICON,0\n          cd go\\build\\outputs\\windows-release\n          DEL flutter_engine.pdb\n          DEL flutter_engine.exp\n          DEL flutter_engine.lib\n          Compress-Archive * ../../../../build/build.zip\n\n      - name: Build (macos)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos'\n        run: |\n          hover build darwin-dmg\n          mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg\n\n      - name: Build (linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        run: |\n          curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf\n          mkdir -p fonts\n          mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf\n          cat ci/linux_font.yaml >> pubspec.yaml\n          hover build linux-appimage\n          mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage\n\n      - name: Append application-identifier (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          /usr/libexec/PlistBuddy -c 'Add :application-identifier string opensource.pikapika' ios/Runner/Info.plist\n\n      - name: Build (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          sh scripts/build-ipa.sh\n\n      - name: Build (android-arm32)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-arm.sh\n\n      - name: Build (android-arm64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-arm64.sh\n\n      - name: Build (android-x86_64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-x64.sh\n\n      - name: Sign APK (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        env:\n          KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}\n        run: |\n          sh scripts/sign-apk-github-actions.sh\n\n      - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        name: 'Upload Artifact (iOS)'\n        uses: actions/upload-artifact@v4\n        with:\n          name: 'nosign.ipa'\n          path: 'build/nosign.ipa'\n          retention-days: 3\n      - if: steps.need_build.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        name: 'Upload Artifact (Android)'\n        uses: actions/upload-artifact@v4\n        with:\n          name: 'app-release-${{ matrix.config.target }}.apk'\n          path: 'build/app/outputs/flutter-apk/app-release.apk'\n          retention-days: 3\n      - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        name: 'Upload Artifact (Linux)'\n        uses: actions/upload-artifact@v4\n        with:\n          name: 'build.AppImage'\n          path: 'build/build.AppImage'\n          retention-days: 3\n      - if: steps.need_build.outputs.skip_build != 'true' && matrix.config.target == 'macos'\n        name: 'Upload Artifact (MacOS)'\n        uses: actions/upload-artifact@v4\n        with:\n          name: 'build.dmg'\n          path: 'build/build.dmg'\n\n"
  },
  {
    "path": ".github/workflows/Release.core.yml",
    "content": "name: Release-core\n\npermissions:\n  contents: write\n\non:\n  workflow_dispatch:\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  GO_VERSION: \"1.24\"\n  GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46\n\njobs:\n  ci-pass:\n    name: CI is green\n    runs-on: ubuntu-latest\n    needs:\n      - build_release_assets\n    steps:\n      - run: exit 0\n\n  build_release_assets:\n    name: Build and upload assets\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - target: ios\n            host: macos-latest\n          - target: android\n            host: ubuntu-latest\n    runs-on: ${{ matrix.config.host }}\n    env:\n      TARGET: ${{ matrix.config.target }}\n    steps:\n      - name: Setup golang\n        uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Cache go modules (Linux)\n        if: matrix.config.host == 'ubuntu-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (macOS)\n        if: matrix.config.host == 'macos-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/Library/Caches/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ env.BRANCH }}\n\n      - id: check_asset\n        name: Check asset\n        run: |\n          cd ci\n          go run ./cmd/check_asset_core\n\n      - name: Checkout core\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: actions/checkout@v3\n        with:\n          repository: 'niuhuan/pikapika-go-core'\n          token: ${{ secrets.CORE_TOKEN }}\n          path: 'go'\n\n      - if: steps.check_asset.outputs.skip_build != 'true'\n        name: Install go mobile (mobile)\n        run: |\n          go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }}\n\n      - name: Build (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          sh scripts/bind-ios.sh\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android'\n        uses: actions/setup-java@v3\n        with:\n          java-version: '11'\n          distribution: 'temurin'\n\n      - name: Setup android tools (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android'\n        uses: android-actions/setup-android@v3\n        with:\n          cmdline-tools-version: 8512546\n          packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;23.1.7779620'\n\n      - name: Build (android)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android'\n        run: |\n          sh scripts/bind-android-debug.sh\n\n      - name: Upload Asset (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          zip -r core.zip go/mobile/lib\n          cd ci\n          go run ./cmd/upload_asset_core\n\n"
  },
  {
    "path": ".github/workflows/Release.yml",
    "content": "name: Release\n\npermissions:\n  contents: write\n\non:\n  workflow_dispatch:\n    inputs:\n      skip_community_notification:\n        description: 'skip_community_notification'\n        type: boolean\n        required: false\n        default: false\n\nenv:\n  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n  ANDROID_NDK_VERSION: \"23.1.7779620\"\n  GO_MOBILE_VERSION: v0.0.0-20241213221354-a87c1cf6cf46\n\njobs:\n\n  ci-pass:\n    name: CI is green\n    runs-on: ubuntu-latest\n    needs:\n      - check_release\n      - build_release_assets\n      - send_to_community\n    steps:\n      - run: exit 0\n\n  check_release:\n    name: Check release\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          repository: ${{ github.event.inputs.repo }}\n          ref: 'master'\n      - uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n      - name: Cache go modules\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ubuntu-latest-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ubuntu-latest-go-\n      - name: Check release\n        run: |\n          cd ci\n          go run ./cmd/check_release\n\n  build_release_assets:\n    name: Build release assets\n    needs:\n      - check_release\n    strategy:\n      fail-fast: false\n      matrix:\n        sources:\n          - branch: master\n        config:\n          - target: windows\n            host: windows-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: macos\n            host: macos-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: linux\n            host: ubuntu-latest\n            flutter_version: '2.10.3'\n            go_version: '1.23'\n          - target: ios\n            host: macos-14\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n          - target: android-arm32\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n          - target: android-arm64\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n          - target: android-x86_64\n            host: ubuntu-latest\n            flutter_version: '3.13.9'\n            go_version: '1.24'\n            java: '11'\n\n\n    runs-on: ${{ matrix.config.host }}\n\n    env:\n      TARGET: ${{ matrix.config.target }}\n      FLUTTER_VERSION: ${{ matrix.config.flutter_version }}\n      BRANCH: ${{ matrix.sources.branch }}\n      go_version: ${{ matrix.config.go_version }}\n\n    steps:\n\n      # Setup golang env and cache go module\n\n      - name: Setup golang\n        uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n\n      - name: Cache go modules (Windows)\n        if: matrix.config.host == 'windows-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~\\AppData\\Local\\go-build\n            ~\\go\\pkg\\mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (Linux)\n        if: matrix.config.host == 'ubuntu-latest'\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      - name: Cache go modules (macOS)\n        if: startsWith(matrix.config.host, 'macos-')\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/Library/Caches/go-build\n            ~/go/pkg/mod\n          key: ${{ matrix.config.host }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ matrix.config.host }}-go-\n\n      # checkout\n\n      - name: Checkout\n        uses: actions/checkout@v3\n        with:\n          ref: ${{ env.BRANCH }}\n\n      # check_access\n\n      - id: check_asset\n        name: Check asset\n        run: |\n          cd ci\n          go run ./cmd/check_asset\n\n      # \n      - name: Check core\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: actions/checkout@v3\n        with:\n          repository: 'niuhuan/pikapika-go-core'\n          token: ${{ secrets.CORE_TOKEN }}\n          path: 'go'\n\n      - name: Setup flutter\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: subosito/flutter-action@v2\n        with:\n          flutter-version: ${{ env.FLUTTER_VERSION }}\n          architecture: x64\n\n      - name: Check core\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: actions/checkout@v3\n        with:\n          repository: 'niuhuan/pikapika-go-core'\n          token: ${{ secrets.CORE_TOKEN }}\n          path: 'go'\n\n      - name: Cache Flutter dependencies (Linux/Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' || matrix.config.target == 'linux' )\n        uses: actions/cache@v3\n        with:\n          path: /opt/hostedtoolcache/flutter\n          key: ${{ runner.os }}-flutter\n\n      - name: Cache Flutter dependencies (Mac host)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'macos' )\n        uses: actions/cache@v3\n        with:\n          path: /Users/runner/hostedtoolcache/flutter\n          key: ${{ runner.os }}-flutter\n\n      - name: Cache Gradle dependencies (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}\n          restore-keys: |\n            ${{ runner.os }}-gradle-\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2') == false\n        uses: actions/setup-java@v3\n        with:\n          java-version: ${{ matrix.config.java }}\n          distribution: 'temurin'\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' ) && startsWith(matrix.config.flutter_version, '3.24.2')\n        uses: actions/setup-java@v3\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n\n      - name: Setup android tools (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: android-actions/setup-android@v3\n        with:\n          cmdline-tools-version: 8512546\n          packages: 'platform-tools platforms;android-32 build-tools;30.0.2 ndk;${{ env.ANDROID_NDK_VERSION }}'\n\n      - name: Setup msys2 (Windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        uses: msys2/setup-msys2@v2\n        with:\n          install: gcc make\n\n      - name: Install dependencies (Linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        env:\n          ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'\n        run: |\n          curl -JOL https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage\n          chmod a+x appimagetool-x86_64.AppImage\n          mkdir -p ${GITHUB_WORKSPACE}/bin\n          mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool\n          echo ::add-path::${GITHUB_WORKSPACE}/bin\n          sudo apt-get update\n          sudo apt-get install -y libgl1-mesa-dev xorg-dev libfuse2 locate\n\n      - name: Install hover (desktop)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos')\n        run: |\n          go install github.com/go-flutter-desktop/hover@latest\n\n      - name: Install go mobile (mobile)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-arm32' || matrix.config.target == 'android-x86_64' )\n        run: |\n          go install golang.org/x/mobile/cmd/gomobile@${{ env.GO_MOBILE_VERSION }}\n\n      - name: Set-Version (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          cd ci\n          cp version.code.txt ../lib/assets/version.txt\n\n      - name: Upgrade deps version (flutter2 non-mac)\n        if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') == false && startsWith(matrix.config.flutter_version, '2')\n        run: |\n          sed -i \"s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g\" pubspec.yaml\n          sed -i \"s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g\" pubspec.yaml\n          sed -i \"s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g\" pubspec.yaml\n          sed -i \"s/file_picker: 5.2.5/file_picker: 4.6.1/g\" pubspec.yaml\n          sed -i \"s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g\" pubspec.yaml\n          sed -i \"s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g\" pubspec.yaml\n          sed -i \"s/Icons.energy_savings_leaf/Icons.ad_units/g\" lib/screens/SettingsScreen.dart\n          sed -i \"s/gradle-7.5-bin.zip/gradle-6.7.1-all.zip/g\" android/gradle/wrapper/gradle-wrapper.properties\n          sed -i \"s/com.android.tools.build:gradle:7.2.0/com.android.tools.build:gradle:4.1.0/g\" android/build.gradle\n          sed -i \"s/1.7.10/1.3.50/g\" android/app/build.gradle\n          sed -i \"s/fontFamilyFallback/\\/\\/fontFamilyFallback/g\" lib/basic/config/Themes.dart\n          sed -i \"s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g\" pubspec.yaml\n          sed -i \"s/thumbVisibility: true/isAlwaysShown: true/g\" lib/basic/config/ShadowCategories.dart\n          flutter pub get\n\n      - name: Upgrade deps version (flutter2 mac)\n        if: steps.check_asset.outputs.skip_build != 'true' && startsWith(matrix.config.host, 'macos-') && startsWith(matrix.config.flutter_version, '2')\n        run: |\n          brew install gnu-sed\n          gsed -i \"s/another_xlider: ^1.0.1+2/another_xlider: 1.0.1+2/g\" pubspec.yaml\n          gsed -i \"s/flutter_styled_toast: ^2.0.0/flutter_styled_toast: 2.0.0/g\" pubspec.yaml\n          gsed -i \"s/filesystem_picker: ^3.0.0-beta.1/filesystem_picker: 2.0.0/g\" pubspec.yaml\n          gsed -i \"s/file_picker: 5.2.5/file_picker: 4.6.1/g\" pubspec.yaml\n          gsed -i \"s/multi_select_flutter: ^4.0.0/multi_select_flutter: 4.1.2/g\" pubspec.yaml\n          gsed -i \"s/modal_bottom_sheet: ^3.0.0-pre/modal_bottom_sheet: 2.0.1/g\" pubspec.yaml\n          gsed -i \"s/Icons.energy_savings_leaf/Icons.ad_units/g\" lib/screens/SettingsScreen.dart\n          gsed -i \"s/fontFamilyFallback/\\/\\/fontFamilyFallback/g\" lib/basic/config/Themes.dart\n          gsed -i \"s/easy_localization: ^3.0.7+1/easy_localization: ^3.0.0/g\" pubspec.yaml\n          gsed -i \"s/thumbVisibility: true/isAlwaysShown: true/g\" lib/basic/config/ShadowCategories.dart\n          flutter pub get\n\n      - name: Build (windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        run: |\n          hover build windows\n          curl -JOL https://github.com/ComicSparks/build-tools/releases/download/storage/Resource_Hacker_5.1.8.zip\n          Expand-Archive .\\Resource_Hacker_5.1.8.zip\n          cmd /c \"Resource_Hacker_5.1.8\\ResourceHacker.exe\" -open go\\build\\outputs\\windows-release\\pikapika.exe -save go\\build\\outputs\\windows-release\\pikapika.exe -action addskip -res go/assets/icon.ico -mask ICONGROUP,MAINICON,0\n          cd go\\build\\outputs\\windows-release\n          DEL flutter_engine.pdb\n          DEL flutter_engine.exp\n          DEL flutter_engine.lib\n          Compress-Archive * ../../../../build/build.zip\n\n      - name: Build (macos)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos'\n        run: |\n          hover build darwin-dmg\n          mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg\n\n      - name: Build (linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        run: |\n          curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf\n          mkdir -p fonts\n          mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf\n          cat ci/linux_font.yaml >> pubspec.yaml\n          hover build linux-appimage\n          mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage\n\n      - name: Append application-identifier (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          /usr/libexec/PlistBuddy -c 'Add :application-identifier string opensource.pikapika' ios/Runner/Info.plist\n\n      - name: Build (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          sh scripts/build-ipa.sh\n\n      - name: Build (android-arm32)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-arm.sh\n\n      - name: Build (android-arm64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-arm64.sh\n\n      - name: Build (android-x86_64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64'\n        run: |\n          export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/${{ env.ANDROID_NDK_VERSION }}\n          sh scripts/build-apk-x64.sh\n\n      - name: Sign APK (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        env:\n          KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}\n        run: |\n          sh scripts/sign-apk-github-actions.sh\n\n      - name: Upload Asset (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          cd ci\n          go run ./cmd/upload_asset\n\n\n  send_to_community:\n    if: github.event.inputs.skip_community_notification != 'true'\n    needs:\n      - check_release\n      - build_release_assets\n    name: Send message to community\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          repository: ${{ github.event.inputs.repo }}\n          ref: 'master'\n      - uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n      - name: Cache go modules\n        uses: actions/cache@v3\n        with:\n          path: |\n            ~/.cache/go-build\n            ~/go/pkg/mod\n          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-\n      - name: Send to community\n        env:\n          TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}\n          TG_CHAT_IDS: ${{ secrets.TG_CHAT_IDS }}\n          DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}\n          DISCORD_CHAT_IDS: ${{ secrets.DISCORD_CHAT_IDS }}\n        run: |\n          cd ci\n          go run ./cmd/send_to_community\n\n\n"
  },
  {
    "path": ".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\n# PROJECT\n/go/mobile/lib/*.aar\n/go/mobile/lib/*.jar\n/go/mobile/lib/*.framework/\n/go/mobile/lib/*.xcframework/\n/go/vendor/\n\nios/build/\n\n# IDE\n*.iml\n.vscode/\n\n# APP\n/lib/assets/version.txt\ngo.work\n\n# FVM Version Cache\n.fvm/\n\ntmp/\n.tmp/\n"
  },
  {
    "path": ".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: f4abaa0735eba4dfd8f33f73363911d63931fe03\n  channel: stable\n\nproject_type: app\n"
  },
  {
    "path": "README-zh_CN.md",
    "content": "<div align=\"center\">\n  <h1 align=\"center\">\n    Pikapika \n\n[![license](https://img.shields.io/github/license/ComicSparks/pikapika)](https://raw.githubusercontent.com/ComicSparks/pikapika/master/LICENSE)\n[![releases](https://img.shields.io/github/v/release/ComicSparks/pikapika)](https://github.com/ComicSparks/pikapika/releases)\n  </h1>\n</div>\n\n- 美观易用且无广告的漫画客户端, 能运行在Windows/MacOS/Linux/Android/IOS中。\n- 此APP内容存在限制级别内容(例如 露骨/血腥/暴力/吸毒)，18岁以下的用户需在监护人陪同下使用，并请您在遵守当地法律法规。\n- 您的star和issue是对开发者的莫大鼓励, 可以源仓库下载最新的源码/安装包, 表示支持/提出建议。\n- 源仓库地址 [https://github.com/ComicSparks/pikapika](https://github.com/ComicSparks/pikapika)\n\n## 界面 / 功能\n\n![阅读器](images/reader.png)\n\n### 分流\n\nVPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN上访问代理, 使用代理请求分流服务器。\n\n### 漫画分类/搜索\n\n![分类](images/categories_screen.png) ![列表](images/comic_list.png)\n\n### 漫画阅读/下载/导入/导出\n\n您可以导出任意已经完成的下载到zip, 从另外一台设备导入。 导出的zip解压后可以直接使用其中的HTML进行阅读\n\n![导出下载](images/exporting.png)\n\n![HTML预览](images/exporting2.png)\n\n### 游戏\n\n![games](images/games.png)\n![game](images/game.png)\n\n## 特性\n\n- [x] 用户\n    - [x] 登录 / 注册 / 获取个人信息 / 自动打卡\n    - [x] 修改密码 / 修改签名 / 修改头像\n- [x] 漫画\n    - [x] 分类 / 搜索 / 随机本子 / 看此本子的也在看 / 排行榜\n    - [x] 在分类中搜索 / 按 \"分类 / 标签 / 创建人 / 汉化组\" 检索\n    - [x] 漫画详情 / 章节 / 看图 / 将图片保存到相册\n    - [x] 收藏 / 喜欢\n    - [x] 获取评论 / 评论 / 评论回复 (社区评论后无法删除, 请谨慎使用)\n    - [x] 更新提示\n- [x] 游戏\n    - [x] 列表 / 详情 / 无广告下载\n- [x] 下载\n    - [x] 导入导出 / 无线共享 / 移动设备与PC设备传输\n    - [x] 导出到加密的归档文件 / 直接观看加密的归档文件\n- [ ] 聊天室\n- [x] 缓存 / 自动清理\n- [x] 设备支持\n    - [x] 移动端\n      - [x] 文件关联\n      - [x] 自定义超链接\n    - [x] 安卓\n        - [x] 高刷新频率屏幕适配 (90/120/144... Hz)\n        - [x] 安卓10以上随系统进入深色/夜间模式\n\n## 其他说明\n\n数据资料存储位置\n\n- ios/android : 程序自身数据目录中, 删除就会清理\n- windows : 程序同一目录中data文件夹下\n- macos : ~/Library/Application\\ Support/pikapika\n- linux : ~/.pikapika\n\n## 技术架构\n\n### 多平台适配\n\n这个应用程序使用golang和dart(flutter)作为主要语言, 可以兼容Windows, linux, MacOS, Android, IOS\n\n使用了不同的框架桥接到桌面和移动平台上\n\n- go-flutter => Windows / MacOS / Linux\n- gomobile => Android / IOS\n\n![平台](images/platforms.png)\n\n### 构建环境\n\n(桌面端/移动端)\n\n- [golang](https://golang.org/) (1.17/1.18)\n- [flutter](https://flutter.dev/) (2.10.3/3.7.3)\n\n## 请您遵守使用规则\n\n软件副本分发以及代码使用规则\n\n- 本软件的代码在未经允许的情况下可以自用但不允许释放任何releases, 个人或企业不可用于商业用途, 不可上架任何商店。\n- 不要在任何其他 **二次元软件** 的 **聊天社区** 或 **开发社区** 内, 发布有关本软件的链接或信息, 对于观点不同产生的分歧作者不站队任何立场。\n- 不要发送本软件安装包到 **任何社区内** , 不要将APK/IPA/ZIP/DMG发送包括任何聊天软件内的群聊功能。 请使用Github中提供的Releases页面的链接。\n- 对本仓库的fork需要保留本仓库的链接, 以引导用户在主要仓库进行讨论。\n\n责任声明\n\n- 作者仅分享编程技术, 不分发软件, 不对分发软件承担任何后果。 因传播载造成的法律问题或纠纷, 需行为人自行承担, 请您遵守当地法以及副本接受方(社区或人)所在地区的法律。\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <h1 align=\"center\">\n    Pikapika \n\n[![license](https://img.shields.io/github/license/ComicSparks/pikapika)](https://raw.githubusercontent.com/ComicSparks/pikapika/master/LICENSE)\n[![releases](https://img.shields.io/github/v/release/ComicSparks/pikapika)](https://github.com/ComicSparks/pikapika/releases)\n  </h1>\n</div>\n\n- A visually appealing, easy-to-use, ad-free manga client that runs on Windows/MacOS/Linux/Android/iOS.\n- This app contains restricted content (such as explicit, gory, violent, drug-related scenes). Users under 18 should use it under parental supervision, and please comply with local laws and regulations.\n- Your stars and issues are a great encouragement to the developers. You can download the latest source code/installation package from the source repository to show support/offer suggestions.\n- Source Repository URL is [https://github.com/ComicSparks/pikapika](https://github.com/ComicSparks/pikapika)\n\n## Interface / Functions\n\n![reader](images/reader.png)\n\n### Traffic Diversion\n\nVPN -> Proxy -> Traffic Diversion: If these three functions are set simultaneously, you will access the proxy through the VPN on your phone, using the proxy to request the traffic diversion server.\n\n### Comic categories/search\n\nSearch comics in categories\n\n### Comic reader/download/imports/exports\n\nYou can export any completed downloads to a zip file and import it from another device. After extracting the exported zip, you can directly use the HTML files within for reading.\n\n### Games\n\nYou can download games without ads.\n\n## Features\n\n- [x] Users\n    - [x] Login / Register / Get personal information / Auto check-in\n    - [x] Change password / Signature / Avatar\n- [x] Comics\n    - [x] Categories / Search / Random comic / Also reading this comic / Rankings\n    - [x] Search in categories / Search by \"category / tag / creator / translation group\"\n    - [x] Comic details / Chapters / View images / Save images to the album\n    - [x] Favorites / Likes\n    - [x] Get comments / Comment / Comment reply (comments in the community cannot be deleted, please use with caution)\n    - [x] Update notification\n- [x] Games\n    - [x] List / Details / Ad-free download\n- [x] Downloads\n    - [x] Import/export / Wireless sharing / Transfer between mobile and PC devices\n    - [x] Export to encrypted archive file / Directly view encrypted archive file\n- [ ] Chat room\n- [x] Cache / Auto clean\n- [x] Device support\n    - [x] Mobile\n        - [x] File association\n        - [x] Custom hyperlinks\n    - [x] Android\n        - [x] High refresh rate screen adaptation (90/120/144... Hz)\n        - [x] Android 10 and above automatically switch to dark/night mode with the system\n\n## Other tips\n\nData storage location\n\n- ios/android: In the program's own data directory, deleting it will clear it\n- windows: In the data folder in the same directory as the program\n- macos: ~/Library/Application\\ Support/pikapika\n- linux: ~/.pikapika\n\n## Technology Stack\n\n### Multi-platform adaptation\n\nThis application uses golang and dart (flutter) as the main languages and is compatible with Windows, Linux, MacOS, Android, and iOS.\n\nDifferent frameworks are used to bridge to desktop and mobile platforms\n\n- go-flutter => Windows / MacOS / Linux\n- gomobile => Android / iOS\n\n![platforms](images/platforms.png)\n\n### Build environment\n\n(Desktop/Mobile)\n\n- [golang](https://golang.org/) (1.17/1.18)\n- [flutter](https://flutter.dev/) (2.10.3/3.7.3)\n\n## Please follow the usage rules.\n\nSoftware copy distribution and code usage rules\n\n- The code of this software can be used for personal use without permission, but no releases are allowed, and it cannot be used for commercial purposes by individuals or companies, nor can it be put on any store.\n- Do not post links or information about this software in any **two-dimensional** chat community or development community. The author does not take any stance on any views that differ.\n- Do not send the software installation package to **any community**, and do not send APK/IPA/ZIP/DMG to any group chat function in any chat software. Please use the link provided on the Releases page in Github.\n- Forks of this repository must retain the link to this repository to guide users to discuss in the main repository.\n\nDisclaimer\n\n- The author only shares programming technology and does not distribute software. The person who spreads the content is responsible for any legal issues or disputes caused by the spread. Please comply with local laws and the laws of the recipient (community or individual) region.\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "include: package:flutter_lints/flutter.yaml\n\nlinter:\n  rules:\n    avoid_print: false\n    unnecessary_this: false\n    file_names: false\n    constant_identifier_names: false\n    no_logic_in_create_state: false\n"
  },
  {
    "path": "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": "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 33 // 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 \"opensource.pic2acg\"\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-gradle-build-configuration.\n        minSdkVersion 21 // 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    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'\n    implementation fileTree(dir: \"../../go/mobile/lib\", include: [\"*.jar\", \"*.aar\"])\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"opensource.pic2acg\">\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    <uses-permission android:name=\"android.permission.READ_INTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"opensource.pic2acg\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_INTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_INTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.USE_BIOMETRIC\" />\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\" />\n\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <data android:scheme=\"https\" />\n        </intent>\n    </queries>\n\n    <application\n        android:icon=\"@mipmap/ic_launcher\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:label=\"pic2acg\"\n        android:requestLegacyExternalStorage=\"true\">\n        <activity\n            android:name=\"opensource.pic2acg.MainActivity\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:exported=\"true\"\n            android:hardwareAccelerated=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\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            <!-- 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            <intent-filter>\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=\"https\" />\n                <data android:scheme=\"http\" />\n                <data android:host=\"pika\" />\n            </intent-filter>\n            <intent-filter>\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=\"pika\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\"></action>\n                <category android:name=\"android.intent.category.DEFAULT\"></category>\n                <data\n                    android:host=\"*\"\n                    android:mimeType=\"*/*\"\n                    android:scheme=\"content\"></data>\n                <data android:pathPattern=\".*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\..*\\.pki\"></data>\n                <data\n                    android:host=\"*\"\n                    android:mimeType=\"*/*\"\n                    android:scheme=\"file\"></data>\n                <data android:pathPattern=\".*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\..*\\.pkz\"></data>\n                <data android:pathPattern=\".*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\.pki\"></data>\n                <data android:pathPattern=\".*\\..*\\..*\\..*\\..*\\..*\\.pki\"></data>\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        <!-- image_cropper -->\n        <activity\n            android:name=\"com.yalantis.ucrop.UCropActivity\"\n            android:screenOrientation=\"portrait\"\n            android:theme=\"@style/Theme.AppCompat.Light.NoActionBar\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/opensource/pic2acg/MainActivity.kt",
    "content": "package opensource.pic2acg\n\nimport android.content.ContentValues\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.hardware.biometrics.BiometricPrompt\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\nimport android.os.*\nimport android.provider.MediaStore\nimport android.util.Log\nimport android.view.Display\nimport android.view.KeyEvent\nimport android.view.WindowManager\nimport androidx.annotation.NonNull\nimport androidx.annotation.RequiresApi\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.EventChannel\nimport io.flutter.plugin.common.MethodChannel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.newSingleThreadContext\nimport kotlinx.coroutines.sync.Mutex\nimport mobile.Mobile\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.nio.file.Files\nimport java.util.concurrent.Executors\nimport java.util.concurrent.LinkedBlockingQueue\nimport java.util.concurrent.TimeUnit\n\nclass MainActivity : FlutterActivity() {\n\n    // 为什么换成换成线程池而不继续使用携程 : 下载图片速度慢会占满携程造成拥堵, 接口无法请求\n    private val pool = Executors.newCachedThreadPool { runnable ->\n        Thread(runnable).also { it.isDaemon = true }\n    }\n    private val uiThreadHandler = Handler(Looper.getMainLooper())\n    private val scope = CoroutineScope(newSingleThreadContext(\"worker-scope\"))\n\n    private val notImplementedToken = Any()\n    private fun MethodChannel.Result.withCoroutine(exec: () -> Any?) {\n        pool.submit {\n            try {\n                val data = exec()\n                uiThreadHandler.post {\n                    when (data) {\n                        notImplementedToken -> {\n                            notImplemented()\n                        }\n                        is Unit, null -> {\n                            success(null)\n                        }\n                        else -> {\n                            success(data)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                Log.e(\"Method\", \"Exception\", e)\n                uiThreadHandler.post {\n                    error(\"\", e.message, \"\")\n                }\n            }\n\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.KITKAT)\n    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        Mobile.initApplication(androidDataLocal())\n        // Method Channel\n        MethodChannel(\n            flutterEngine.dartExecutor.binaryMessenger,\n            \"method\"\n        ).setMethodCallHandler { call, result ->\n            result.withCoroutine {\n                when (call.method) {\n                    \"flatInvoke\" -> {\n                        Mobile.flatInvoke(\n                            call.argument(\"method\")!!,\n                            call.argument(\"params\")!!\n                        )\n                    }\n                    \"androidSaveFileToImage\" -> {\n                        saveImage(call.argument(\"path\")!!)\n                    }\n                    \"androidGetModes\" -> {\n                        modes()\n                    }\n                    \"androidSetMode\" -> {\n                        setMode(call.argument(\"mode\")!!)\n                    }\n                    \"androidGetVersion\" -> Build.VERSION.SDK_INT\n                    // 现在的文件储存路径, 默认路径返回空字符串 \"\"\n                    \"dataLocal\" -> androidDataLocal()\n                    // 迁移到那个地方, 如果是空字符串则迁移会默认位置\n                    \"migrate\" -> androidMigrate(call.argument(\"path\")!!)\n                    // 获取可以迁移数据地址\n                    \"androidGetExtendDirs\" -> androidGetExtendDirs()\n                    \"androidSecureFlag\" -> androidSecureFlag(call.argument(\"flag\")!!)\n                    \"verifyAuthentication\" -> auth()\n                    \"androidStorageRoot\" -> storageRoot()\n                    \"androidDefaultExportsDir\" -> androidDefaultExportsDir().absolutePath\n                    \"androidMkdirs\" -> androidMkdirs(\n                        call.arguments<String>() ?: throw Exception(\"need arg\")\n                    )\n                    else -> {\n                        notImplementedToken\n                    }\n                }\n            }\n        }\n\n        MethodChannel(\n            flutterEngine.dartExecutor.binaryMessenger,\n            \"network\"\n        ).setMethodCallHandler { call, result ->\n            result.withCoroutine {\n                when (call.method) {\n                    \"getNetworkType\" -> getNetworkType()\n                    \"getIsMobile\" -> isMobileNetwork()\n                    else -> notImplementedToken\n                }\n            }\n        }\n\n        //\n        val eventMutex = Mutex()\n        var eventSink: EventChannel.EventSink? = null\n        EventChannel(flutterEngine.dartExecutor.binaryMessenger, \"flatEvent\")\n            .setStreamHandler(object : EventChannel.StreamHandler {\n                override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n                    events?.let { events ->\n                        scope.launch {\n                            eventMutex.lock()\n                            eventSink = events\n                            eventMutex.unlock()\n                        }\n                    }\n                }\n\n                override fun onCancel(arguments: Any?) {\n                    scope.launch {\n                        eventMutex.lock()\n                        eventSink = null\n                        eventMutex.unlock()\n                    }\n                }\n            })\n        Mobile.eventNotify { message ->\n            scope.launch {\n                eventMutex.lock()\n                try {\n                    eventSink?.let {\n                        uiThreadHandler.post {\n                            it.success(message)\n                        }\n                    }\n                } finally {\n                    eventMutex.unlock()\n                }\n            }\n        }\n\n        //\n        EventChannel(flutterEngine.dartExecutor.binaryMessenger, \"volume_button\")\n            .setStreamHandler(volumeStreamHandler)\n\n    }\n\n    private fun androidDataLocal(): String {\n        val localFile = File(context!!.filesDir.absolutePath, \"data.local\")\n        if (localFile.exists()) {\n            val path = String(FileInputStream(localFile).use { it.readBytes() })\n            if (File(path).isDirectory) {\n                return path\n            }\n        }\n        return context!!.filesDir.absolutePath\n    }\n\n    private fun androidGetExtendDirs(): String {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            val result = context!!.getExternalFilesDirs(\"\")?.toMutableList()?.also {\n                it.add(context!!.filesDir.absoluteFile)\n            }?.joinToString(\"|\")\n            if (result != null) {\n                return result\n            }\n        }\n        throw Exception(\"System version too low\")\n    }\n\n    private fun androidMigrate(path: String) {\n        val current = androidDataLocal()\n        if (current == path) {\n            return\n        }\n        // 删除位置配置文件\n        if (File(current, \"data.local\").exists()) {\n            File(current, \"data.local\").delete()\n        }\n        // 目标位置文件夹不存在就创建，存在则清理\n        val target = File(path)\n        if (!target.exists()) {\n            target.mkdirs()\n        }\n        target.listFiles().forEach { delete(it) }\n        // 移动所有文件夹\n\n        File(current).listFiles().forEach {\n            move(it, File(target, it.name))\n        }\n        val localFile = File(context!!.filesDir.absolutePath, \"data.local\")\n        if (path == context!!.filesDir.absolutePath) {\n            localFile.delete()\n        } else {\n            FileOutputStream(localFile).use { it.write(path.toByteArray()) }\n        }\n    }\n\n    private fun delete(f: File) {\n        f.delete()\n    }\n\n    private fun move(f: File, t: File) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            if (f.isDirectory) {\n                Files.createDirectories(t.toPath())\n                f.listFiles().forEach { move(it, File(t, it.name)) }\n                Files.delete(f.toPath())\n            } else {\n                Files.move(f.toPath(), t.toPath())\n            }\n        } else {\n            if (f.isDirectory) {\n                t.mkdirs()\n                f.listFiles().forEach { move(it, File(t, it.name)) }\n                f.delete()\n            } else {\n                FileOutputStream(t).use { o ->\n                    FileInputStream(f).use { i ->\n                        o.write(i.readBytes())\n                    }\n                }\n                f.delete()\n            }\n        }\n    }\n\n    // save_image\n\n    private fun saveImage(path: String) {\n        BitmapFactory.decodeFile(path)?.let { bitmap ->\n            val contentValues = ContentValues().apply {\n                put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())\n                put(MediaStore.MediaColumns.MIME_TYPE, \"image/jpeg\")\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one\n                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)\n                    put(MediaStore.MediaColumns.IS_PENDING, 1)\n                }\n            }\n            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)\n                ?.let { uri ->\n                    contentResolver.openOutputStream(uri)?.use { fos ->\n                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)\n                    }\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one\n                        contentValues.clear()\n                        contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)\n                        contentResolver.update(uri, contentValues, null, null)\n                    }\n                }\n        }\n    }\n\n    // fps mods\n    private fun mixDisplay(): Display? {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n            display?.let {\n                return it\n            }\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            windowManager.defaultDisplay?.let {\n                return it\n            }\n        }\n        return null\n    }\n\n    private fun modes(): List<String> {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            mixDisplay()?.let { display ->\n                return display.supportedModes.map { mode ->\n                    mode.toString()\n                }\n            }\n        }\n        return ArrayList()\n    }\n\n    private fun setMode(string: String) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            mixDisplay()?.let { display ->\n                if (string == \"\") {\n                    uiThreadHandler.post {\n                        window.attributes = window.attributes.also { attr ->\n                            attr.preferredDisplayModeId = 0\n                        }\n                    }\n                    return\n                }\n                return display.supportedModes.forEach { mode ->\n                    if (mode.toString() == string) {\n                        uiThreadHandler.post {\n                            window.attributes = window.attributes.also { attr ->\n                                attr.preferredDisplayModeId = mode.modeId\n                            }\n                        }\n                        return\n                    }\n                }\n            }\n        }\n    }\n\n// volume_buttons\n\n    private var volumeEvents: EventChannel.EventSink? = null\n\n    private val volumeStreamHandler = object : EventChannel.StreamHandler {\n\n        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n            volumeEvents = events\n        }\n\n        override fun onCancel(arguments: Any?) {\n            volumeEvents = null\n        }\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {\n        volumeEvents?.let {\n            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {\n                uiThreadHandler.post {\n                    it.success(\"DOWN\")\n                }\n                return true\n            }\n            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {\n                uiThreadHandler.post {\n                    it.success(\"UP\")\n                }\n                return true\n            }\n        }\n        return super.onKeyDown(keyCode, event)\n    }\n\n    private fun androidSecureFlag(flag: Boolean) {\n        uiThreadHandler.post {\n            if (flag) {\n                window.setFlags(\n                    WindowManager.LayoutParams.FLAG_SECURE,\n                    WindowManager.LayoutParams.FLAG_SECURE\n                )\n            } else {\n                window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)\n            }\n        }\n    }\n\n    // withCoroutine -> queue\n    private fun auth(): Boolean {\n        var queue = LinkedBlockingQueue<Boolean>()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            var mBiometricPrompt = BiometricPrompt.Builder(this)\n                .setTitle(\"验证身份\")\n                .setDescription(\"需要验证您的身份\")\n                .setNegativeButton(\n                    \"取消\", mainExecutor\n                ) { _, _ -> queue.add(false) }\n                .build()\n\n\n            var mCancellationSignal = CancellationSignal()\n            mCancellationSignal.setOnCancelListener {\n                queue.add(false)\n            }\n\n            var mAuthenticationCallback = object : BiometricPrompt.AuthenticationCallback() {\n                override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) {\n                    super.onAuthenticationError(errorCode, errString)\n                    queue.add(false)\n                }\n\n                override fun onAuthenticationFailed() {\n                    super.onAuthenticationFailed()\n                    queue.add(false)\n                }\n\n                override fun onAuthenticationSucceeded(result1: BiometricPrompt.AuthenticationResult?) {\n                    super.onAuthenticationSucceeded(result1)\n                    queue.add(true)\n                }\n            }\n\n            mBiometricPrompt.authenticate(\n                mCancellationSignal,\n                mainExecutor,\n                mAuthenticationCallback\n            )\n\n        } else {\n            queue.add(false)\n        }\n\n        return queue.poll(5, TimeUnit.MINUTES) ?: false\n    }\n\n    fun storageRoot(): String {\n        return Environment.getExternalStorageDirectory().absolutePath\n    }\n\n    private fun downloadsDir(): File {\n        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)\n            ?: throw java.lang.IllegalStateException()\n    }\n\n    private fun defaultAppDir(): File {\n        return File(downloadsDir(), \"pic2acg\")\n    }\n\n    private fun androidDefaultExportsDir(): File {\n        return File(defaultAppDir(), \"exports\")\n    }\n\n    private fun androidMkdirs(path: String) {\n        val dir = File(path)\n        if (!dir.exists()) {\n            dir.mkdirs()\n        }\n    }\n\n    private fun getNetworkType(): String {\n        val ctx = context ?: return \"none\"\n        val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return \"none\"\n        val active = cm.activeNetwork ?: return \"none\"\n        val caps = cm.getNetworkCapabilities(active) ?: return \"none\"\n        return when {\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> \"mobile\"\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> \"wifi\"\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> \"ethernet\"\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> findNonVpnTransport(cm) ?: \"vpn\"\n            else -> \"other\"\n        }\n    }\n\n    private fun isMobileNetwork(): Boolean {\n        val ctx = context ?: return false\n        val cm = ctx.getSystemService(ConnectivityManager::class.java) ?: return false\n        val active = cm.activeNetwork ?: return false\n        val caps = cm.getNetworkCapabilities(active) ?: return false\n        if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {\n            return true\n        }\n        if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {\n            return findNonVpnTransport(cm) == \"mobile\"\n        }\n        return false\n    }\n\n    private fun findNonVpnTransport(cm: ConnectivityManager): String? {\n        for (network in cm.allNetworks) {\n            val caps = cm.getNetworkCapabilities(network) ?: continue\n            if (!caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {\n                continue\n            }\n            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {\n                continue\n            }\n            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {\n                return \"mobile\"\n            }\n            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {\n                return \"wifi\"\n            }\n            if (caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {\n                return \"ethernet\"\n            }\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "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": "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": "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=\"@mipmap/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\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        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:ignore=\"NewApi\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\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        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:ignore=\"NewApi\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"opensource.pic2acg\">\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": "android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.7.10'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.2.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}\nsubprojects {\n    project.evaluationDependsOn(':app')\n}\n\ntasks.register(\"clean\", Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Oct 29 09:53:43 CST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "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": "ci/cmd/check_asset/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GITHUB_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tversion := commons.LoadVersion()\n\t// get TARGET\n\ttarget := os.Getenv(\"TARGET\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${TARGET} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get FLUTTER_VERSION\n\tflutterVersion := os.Getenv(\"FLUTTER_VERSION\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${FLUTTER_VERSION} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get BRANCH\n\tbranch := os.Getenv(\"BRANCH\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${BRANCH} is not set\")\n\t\tos.Exit(1)\n\t}\n\t//\n\tvar releaseFileName = commons.AssetName(version, flutterVersion, target, branch)\n\t// get version\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/releases/tags/%v\", githubRepository, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\tpanic(\"NOT FOUND RELEASE\")\n\t}\n\tbuff, err := ioutil.ReadAll(getReleaseResponse.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar release commons.Release\n\terr = json.Unmarshal(buff, &release)\n\tif err != nil {\n\t\tprintln(string(buff))\n\t\tpanic(err)\n\t}\n\tfor _, asset := range release.Assets {\n\t\tif asset.Name == releaseFileName {\n\t\t\tprintln(\"::set-output name=skip_build::true\")\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\tprint(\"::set-output name=skip_build::false\")\n}\n"
  },
  {
    "path": "ci/cmd/check_asset_core/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GITHUB_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tversion := commons.LoadVersion()\n\t// get TARGET\n\ttarget := os.Getenv(\"TARGET\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${TARGET} is not set\")\n\t\tos.Exit(1)\n\t}\n\t//\n\tvar releaseFileName = fmt.Sprintf(\"core-%v-%v.zip\", version.Code, target)\n\t// get version\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/releases/tags/%v\", githubRepository, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\tpanic(\"NOT FOUND RELEASE\")\n\t}\n\tbuff, err := ioutil.ReadAll(getReleaseResponse.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar release commons.Release\n\terr = json.Unmarshal(buff, &release)\n\tif err != nil {\n\t\tprintln(string(buff))\n\t\tpanic(err)\n\t}\n\tfor _, asset := range release.Assets {\n\t\tif asset.Name == releaseFileName {\n\t\t\tprintln(\"::set-output name=skip_build::true\")\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\tprint(\"::set-output name=skip_build::false\")\n}\n"
  },
  {
    "path": "ci/cmd/check_release/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GITHUB_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tversion := commons.LoadVersion()\n\t// get version\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/releases/tags/%v\", githubRepository, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(nil)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(nil)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\turl := fmt.Sprintf(\"https://api.github.com/repos/%v/releases\", githubRepository)\n\t\tbody := map[string]interface{}{\n\t\t\t\"tag_name\":         version.Code,\n\t\t\t\"target_commitish\": commons.MainBranch,\n\t\t\t\"name\":             version.Code,\n\t\t\t\"body\":             version.Info,\n\t\t}\n\t\tvar buff []byte\n\t\tbuff, err = json.Marshal(&body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tvar createReleaseRequest *http.Request\n\t\tcreateReleaseRequest, err = http.NewRequest(\"POST\", url, bytes.NewBuffer(buff))\n\t\tif err != nil {\n\t\t\tpanic(nil)\n\t\t}\n\t\tcreateReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\t\tcreateReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\t\tvar createReleaseResponse *http.Response\n\t\tcreateReleaseResponse, err = http.DefaultClient.Do(createReleaseRequest)\n\t\tif err != nil {\n\t\t\tpanic(nil)\n\t\t}\n\t\tdefer createReleaseResponse.Body.Close()\n\t\tif createReleaseResponse.StatusCode != 201 {\n\t\t\tbuff, err = ioutil.ReadAll(createReleaseResponse.Body)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tprintln(string(buff))\n\t\t\tpanic(\"NOT 201\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ci/cmd/send_to_community/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/andersfylling/disgord\"\n\ttgbotapi \"github.com/go-telegram-bot-api/telegram-bot-api/v5\"\n)\n\nfunc main() {\n\t// get version\n\tvar version commons.Version\n\tcodeFile, err := ioutil.ReadFile(\"version.code.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Code = strings.TrimSpace(string(codeFile))\n\tinfoFile, err := ioutil.ReadFile(\"version.info.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Info = strings.TrimSpace(string(infoFile))\n\t// message\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\tvar message = fmt.Sprintf(\n\t\t\"%v 版本 %v 发布! \\n\\n\"+\n\t\t\t\"更新内容:\\n\"+\n\t\t\t\"%v\\n\\n\"+\n\t\t\t\"https://github.com/%v/%v/releases/tag/%v\",\n\t\tgithubRepository, version.Code, version.Info, githubRepository, githubRepository, version.Code,\n\t)\n\t// get accounts\n\ttgToken := os.Getenv(\"TG_BOT_TOKEN\")\n\ttgChatIdsStr := os.Getenv(\"TG_CHAT_IDS\")\n\tdiscordToken := os.Getenv(\"DISCORD_BOT_TOKEN\")\n\tdiscordChatIdsStr := os.Getenv(\"DISCORD_CHAT_IDS\")\n\tif tgToken != \"\" && tgChatIdsStr != \"\" {\n\t\tvar tgChatIds []int64\n\t\tjson.Unmarshal([]byte(tgChatIdsStr), &tgChatIds)\n\t\tif len(tgChatIds) > 0 {\n\t\t\tsendMessageToTg(tgToken, tgChatIds, message)\n\t\t}\n\t}\n\tif discordToken != \"\" && discordChatIdsStr != \"\" {\n\t\tvar discordChatIds []uint64\n\t\tjson.Unmarshal([]byte(discordChatIdsStr), &discordChatIds)\n\t\tif len(discordChatIds) > 0 {\n\t\t\tsendMessageToDiscord(discordToken, discordChatIds, message)\n\t\t}\n\t}\n}\n\nfunc sendMessageToTg(token string, ids []int64, message string) {\n\tbot, err := tgbotapi.NewBotAPI(token)\n\tif err != nil {\n\t\tlog.Panic(err)\n\t}\n\tfor _, id := range ids {\n\t\tmsg := tgbotapi.NewMessage(id, message)\n\t\t_, err = bot.Send(msg)\n\t\tif err != nil {\n\t\t\tfmt.Sprintf(\"Send message to tg chat : %v (error : %v)\", id, err.Error())\n\t\t} else {\n\t\t\tfmt.Sprintf(\"Send message to tg chat : %v (success)\", id)\n\t\t}\n\t}\n}\n\nfunc sendMessageToDiscord(token string, ids []uint64, message string) {\n\tclient, err := disgord.NewClient(context.Background(), disgord.Config{\n\t\tBotToken: token,\n\t})\n\tif err != nil {\n\t\tfmt.Sprintf(\"discord login failed : %v\", err.Error())\n\t\treturn\n\t}\n\tfor _, id := range ids {\n\t\t_, err = client.SendMsg(disgord.Snowflake(id), message)\n\t\tif err != nil {\n\t\t\tfmt.Sprintf(\"Send message to tg chat : %v (error : %v)\", id, err.Error())\n\t\t} else {\n\t\t\tfmt.Sprintf(\"Send message to tg chat : %v (success)\", id)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ci/cmd/upload_asset/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n)\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GITHUB_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tversion := commons.LoadVersion()\n\t// get TARGET\n\ttarget := os.Getenv(\"TARGET\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${TARGET} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get FLUTTER_VERSION\n\tflutterVersion := os.Getenv(\"FLUTTER_VERSION\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${FLUTTER_VERSION} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get BRANCH\n\tbranch := os.Getenv(\"BRANCH\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${BRANCH} is not set\")\n\t\tos.Exit(1)\n\t}\n\t//\n\tvar releaseFileName = commons.AssetName(version, flutterVersion, target, branch)\n\t//\n\tvar releaseFilePath string\n\tvar contentType string\n\tvar contentLength int64\n\tswitch target {\n\tcase \"macos\":\n\t\treleaseFilePath = \"build/build.dmg\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"ios\":\n\t\treleaseFilePath = \"build/nosign.ipa\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"windows\":\n\t\treleaseFilePath = \"build/build.zip\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"linux\":\n\t\treleaseFilePath = \"build/build.AppImage\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-arm32\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-arm64\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-x86_64\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\tcontentType = \"application/octet-stream\"\n\t}\n\treleaseFilePath = path.Join(\"..\", releaseFilePath)\n\tinfo, err := os.Stat(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcontentLength = info.Size()\n\tif contentLength == 166 {\n\t\tpanic(\"NOT FOUND RELEASE FILE\")\n\t}\n\t// get githubRepository\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/releases/tags/%v\", githubRepository, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\tpanic(\"NOT FOUND RELEASE\")\n\t}\n\tbuff, err := ioutil.ReadAll(getReleaseResponse.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar release commons.Release\n\terr = json.Unmarshal(buff, &release)\n\tif err != nil {\n\t\tprintln(string(buff))\n\t\tpanic(err)\n\t}\n\tfile, err := os.Open(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer file.Close()\n\tuploadUrl := fmt.Sprintf(\"https://uploads.github.com/repos/%v/releases/%v/assets?name=%v\", githubRepository, release.Id, releaseFileName)\n\tuploadRequest, err := http.NewRequest(\"POST\", uploadUrl, file)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tuploadRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tuploadRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tuploadRequest.Header.Set(\"Content-Type\", contentType)\n\tuploadRequest.ContentLength = contentLength\n\tuploadResponse, err := http.DefaultClient.Do(uploadRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif uploadResponse.StatusCode != 201 {\n\t\tbuff, err = ioutil.ReadAll(uploadResponse.Body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tprintln(string(buff))\n\t\tpanic(\"NOT 201\")\n\t}\n}\n"
  },
  {
    "path": "ci/cmd/upload_asset_core/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n)\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GITHUB_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GITHUB_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tversion := commons.LoadVersion()\n\t// get TARGET\n\ttarget := os.Getenv(\"TARGET\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${TARGET} is not set\")\n\t\tos.Exit(1)\n\t}\n\t//\n\tvar releaseFileName = fmt.Sprintf(\"core-%v-%v.zip\", version.Code, target)\n\tvar releaseFilePath = \"core.zip\"\n\tvar contentLength int64\n\treleaseFilePath = path.Join(\"..\", releaseFilePath)\n\tinfo, err := os.Stat(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcontentLength = info.Size()\n\t// get version\n\tgithubRepository := os.Getenv(\"GITHUB_REPOSITORY\")\n\tif githubRepository == \"\" {\n\t\tprintln(\"Env ${GITHUB_REPOSITORY} is not set\")\n\t\tos.Exit(1)\n\t}\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/releases/tags/%v\", githubRepository, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\tpanic(\"NOT FOUND RELEASE\")\n\t}\n\tbuff, err := ioutil.ReadAll(getReleaseResponse.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar release commons.Release\n\terr = json.Unmarshal(buff, &release)\n\tif err != nil {\n\t\tprintln(string(buff))\n\t\tpanic(err)\n\t}\n\tfile, err := os.Open(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer file.Close()\n\tuploadUrl := fmt.Sprintf(\"https://uploads.github.com/repos/%v/releases/%v/assets?name=%v\", githubRepository, release.Id, releaseFileName)\n\tuploadRequest, err := http.NewRequest(\"POST\", uploadUrl, file)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tuploadRequest.Header.Set(\"User-Agent\", commons.Ua)\n\tuploadRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tuploadRequest.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\tuploadRequest.ContentLength = contentLength\n\tuploadResponse, err := http.DefaultClient.Do(uploadRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif uploadResponse.StatusCode != 201 {\n\t\tbuff, err = ioutil.ReadAll(uploadResponse.Body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tprintln(string(buff))\n\t\tpanic(\"NOT 201\")\n\t}\n}\n"
  },
  {
    "path": "ci/commons/funcs.go",
    "content": "package commons\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"strings\"\n)\n\nconst Ua = \"pikapika ci\"\nconst MainBranch = \"master\"\n\nfunc LoadVersion() Version {\n\tvar version Version\n\tcodeFile, err := ioutil.ReadFile(\"version.code.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Code = strings.TrimSpace(string(codeFile))\n\tinfoFile, err := ioutil.ReadFile(\"version.info.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Info = strings.TrimSpace(string(infoFile))\n\treturn version\n}\n\nfunc AssetName(version Version, flutterVersion, target, branch string) string {\n\treleaseFileName := fmt.Sprintf(\"pikapika-%v\", version.Code)\n\tswitch target {\n\tcase \"macos\":\n\t\treleaseFileName += \"-macos-intel\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".dmg\"\n\tcase \"ios\":\n\t\treleaseFileName += \"-ios_nosign\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".ipa\"\n\tcase \"windows\":\n\t\treleaseFileName += \"-windows-x86_64\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".zip\"\n\tcase \"linux\":\n\t\treleaseFileName += \"-linux-x86_64\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".AppImage\"\n\tcase \"android-arm32\":\n\t\treleaseFileName += \"-android-arm32\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".apk\"\n\tcase \"android-arm64\":\n\t\treleaseFileName += \"-android-arm64\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".apk\"\n\tcase \"android-x86_64\":\n\t\treleaseFileName += \"-android-x86_64\"\n\t\treleaseFileName += \"-flutter_\" + flutterVersion\n\t\treleaseFileName += \".apk\"\n\t}\n\tif branch != \"master\" && branch != \"main\" {\n\t\treleaseFileName = branch + \"-\" + releaseFileName\n\t}\n\treturn releaseFileName\n}\n"
  },
  {
    "path": "ci/commons/types.go",
    "content": "package commons\n\nimport \"time\"\n\ntype Version struct {\n\tCode string `json:\"code\"`\n\tInfo string `json:\"info\"`\n}\n\ntype Release struct {\n\tUrl             string    `json:\"url\"`\n\tHtmlUrl         string    `json:\"html_url\"`\n\tAssetsUrl       string    `json:\"assets_url\"`\n\tUploadUrl       string    `json:\"upload_url\"`\n\tTarballUrl      string    `json:\"tarball_url\"`\n\tZipballUrl      string    `json:\"zipball_url\"`\n\tDiscussionUrl   string    `json:\"discussion_url\"`\n\tId              int       `json:\"id\"`\n\tNodeId          string    `json:\"node_id\"`\n\tTagName         string    `json:\"tag_name\"`\n\tTargetCommitish string    `json:\"target_commitish\"`\n\tName            string    `json:\"name\"`\n\tBody            string    `json:\"body\"`\n\tDraft           bool      `json:\"draft\"`\n\tPrerelease      bool      `json:\"prerelease\"`\n\tCreatedAt       time.Time `json:\"created_at\"`\n\tPublishedAt     time.Time `json:\"published_at\"`\n\tAuthor          struct {\n\t\tLogin             string `json:\"login\"`\n\t\tId                int    `json:\"id\"`\n\t\tNodeId            string `json:\"node_id\"`\n\t\tAvatarUrl         string `json:\"avatar_url\"`\n\t\tGravatarId        string `json:\"gravatar_id\"`\n\t\tUrl               string `json:\"url\"`\n\t\tHtmlUrl           string `json:\"html_url\"`\n\t\tFollowersUrl      string `json:\"followers_url\"`\n\t\tFollowingUrl      string `json:\"following_url\"`\n\t\tGistsUrl          string `json:\"gists_url\"`\n\t\tStarredUrl        string `json:\"starred_url\"`\n\t\tSubscriptionsUrl  string `json:\"subscriptions_url\"`\n\t\tOrganizationsUrl  string `json:\"organizations_url\"`\n\t\tReposUrl          string `json:\"repos_url\"`\n\t\tEventsUrl         string `json:\"events_url\"`\n\t\tReceivedEventsUrl string `json:\"received_events_url\"`\n\t\tType              string `json:\"type\"`\n\t\tSiteAdmin         bool   `json:\"site_admin\"`\n\t} `json:\"author\"`\n\tAssets []struct {\n\t\tUrl                string    `json:\"url\"`\n\t\tBrowserDownloadUrl string    `json:\"browser_download_url\"`\n\t\tId                 int       `json:\"id\"`\n\t\tNodeId             string    `json:\"node_id\"`\n\t\tName               string    `json:\"name\"`\n\t\tLabel              string    `json:\"label\"`\n\t\tState              string    `json:\"state\"`\n\t\tContentType        string    `json:\"content_type\"`\n\t\tSize               int       `json:\"size\"`\n\t\tDownloadCount      int       `json:\"download_count\"`\n\t\tCreatedAt          time.Time `json:\"created_at\"`\n\t\tUpdatedAt          time.Time `json:\"updated_at\"`\n\t\tUploader           struct {\n\t\t\tLogin             string `json:\"login\"`\n\t\t\tId                int    `json:\"id\"`\n\t\t\tNodeId            string `json:\"node_id\"`\n\t\t\tAvatarUrl         string `json:\"avatar_url\"`\n\t\t\tGravatarId        string `json:\"gravatar_id\"`\n\t\t\tUrl               string `json:\"url\"`\n\t\t\tHtmlUrl           string `json:\"html_url\"`\n\t\t\tFollowersUrl      string `json:\"followers_url\"`\n\t\t\tFollowingUrl      string `json:\"following_url\"`\n\t\t\tGistsUrl          string `json:\"gists_url\"`\n\t\t\tStarredUrl        string `json:\"starred_url\"`\n\t\t\tSubscriptionsUrl  string `json:\"subscriptions_url\"`\n\t\t\tOrganizationsUrl  string `json:\"organizations_url\"`\n\t\t\tReposUrl          string `json:\"repos_url\"`\n\t\t\tEventsUrl         string `json:\"events_url\"`\n\t\t\tReceivedEventsUrl string `json:\"received_events_url\"`\n\t\t\tType              string `json:\"type\"`\n\t\t\tSiteAdmin         bool   `json:\"site_admin\"`\n\t\t} `json:\"uploader\"`\n\t} `json:\"assets\"`\n}"
  },
  {
    "path": "ci/go.mod",
    "content": "module ci\n\ngo 1.17\n\nrequire (\n\tgithub.com/andersfylling/disgord v0.35.1\n\tgithub.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1\n)\n\nrequire (\n\tgithub.com/andersfylling/snowflake/v5 v5.0.1 // indirect\n\tgithub.com/klauspost/compress v1.15.1 // indirect\n\tgo.uber.org/atomic v1.9.0 // indirect\n\tgolang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect\n\tgolang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect\n\tgolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect\n\tnhooyr.io/websocket v1.8.7 // indirect\n)\n"
  },
  {
    "path": "ci/go.sum",
    "content": "github.com/andersfylling/disgord v0.35.1 h1:auhxW9z96/uSF7MYwfuv8AP71AVIc0+jZQWjZdwIqNE=\ngithub.com/andersfylling/disgord v0.35.1/go.mod h1:gTzujw2mWxJWxAPo3LwxG5+a4/n4ikdD+JMb1mONmUM=\ngithub.com/andersfylling/snowflake/v5 v5.0.1 h1:unXbYSij6tRCGJzoLz9zl3nJsqd9hu7bbYSgB8K8/i0=\ngithub.com/andersfylling/snowflake/v5 v5.0.1/go.mod h1:AdhrB+kewjnQInv8cR7ABe2SGoVXh79njnipUnz1HFc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=\ngithub.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=\ngithub.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=\ngithub.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=\ngithub.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=\ngithub.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=\ngithub.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=\ngithub.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=\ngithub.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=\ngithub.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=\ngithub.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=\ngithub.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=\ngithub.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=\ngithub.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=\ngithub.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=\ngolang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=\ngolang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\nk8s.io/gengo v0.0.0-20220307231824-4627b89bbf1b/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nnhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=\nnhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\n"
  },
  {
    "path": "ci/linux_font.yaml",
    "content": "\n  fonts:\n  - family: Roboto\n    fonts:\n      - asset: fonts/Roboto.ttf\n\n"
  },
  {
    "path": "ci/version.code.txt",
    "content": "v1.8.19"
  },
  {
    "path": "ci/version.info.txt",
    "content": "v1.8.19"
  },
  {
    "path": "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": "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": "ios/Flutter/Debug.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "content": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "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    target.build_configurations.each do |config|\n      config.build_settings[\"IPHONEOS_DEPLOYMENT_TARGET\"] = \"11.0\"\n    end\n  end\nend\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\nimport Mobile\nimport LocalAuthentication\nimport Network\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n\n    private let networkMonitor = NWPathMonitor()\n    private var latestPath: NWPath?\n\n    override func application(\n        _ application: UIApplication,\n        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n    ) -> Bool {\n\n        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]\n        let applicationSupportsPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]\n\n        MobileMigration(documentsPath, applicationSupportsPath)\n        MobileInitApplication(applicationSupportsPath)\n\n        let monitorQueue = DispatchQueue(label: \"network.monitor\")\n        networkMonitor.pathUpdateHandler = { [weak self] path in\n            self?.latestPath = path\n        }\n        networkMonitor.start(queue: monitorQueue)\n\n        let controller = self.window.rootViewController as! FlutterViewController\n        let channel = FlutterMethodChannel.init(name: \"method\", binaryMessenger: controller as! FlutterBinaryMessenger)\n\n        channel.setMethodCallHandler { (call, result) in\n            Thread {\n                if call.method == \"flatInvoke\" {\n                    if let args = call.arguments as? Dictionary<String, Any>,\n                       let method = args[\"method\"] as? String,\n                       let params = args[\"params\"] as? String{\n                        var error: NSError?\n                        let data = MobileFlatInvoke(method, params, &error)\n                        if error != nil {\n                            result(FlutterError(code: \"\", message: error?.localizedDescription, details: \"\"))\n                        }else{\n                            result(data)\n                        }\n                    }else{\n                        result(FlutterError(code: \"\", message: \"params error\", details: \"\"))\n                    }\n                }\n                else if call.method == \"verifyAuthentication\"{\n                    let context = LAContext()\n                    let can = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)\n                    guard can == true else {\n                        result(false)\n                        return\n                    }\n                    context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: \"身份验证\") { (success, error) in\n                        result(success)\n                    }\n\n                }\n                else if call.method == \"iosSaveFileToImage\"{\n                    if let args = call.arguments as? Dictionary<String, Any>,\n                       let path = args[\"path\"] as? String{\n\n                        do {\n                            let fileURL: URL = URL(fileURLWithPath: path)\n                                let imageData = try Data(contentsOf: fileURL)\n\n                            if let uiImage = UIImage(data: imageData) {\n                                UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)\n                                result(\"OK\")\n                            }else{\n                                result(FlutterError(code: \"\", message: \"Error loading image \", details: \"\"))\n                            }\n\n                        } catch {\n                                result(FlutterError(code: \"\", message: \"Error loading image : \\(error)\", details: \"\"))\n                        }\n\n                    }else{\n                        result(FlutterError(code: \"\", message: \"params error\", details: \"\"))\n                    }\n                }\n                else if call.method == \"iosGetDocumentDir\" {\n                    result(documentsPath)\n                }\n                else if call.method == \"dataLocal\" {\n                    result(applicationSupportsPath)\n                }\n                else if call.method == \"fontList\" {\n                    result(UIFont.familyNames)\n                }\n                else {\n                    result(FlutterMethodNotImplemented)\n                }\n            }.start()\n        }\n\n        let networkChannel = FlutterMethodChannel(name: \"network\", binaryMessenger: controller as! FlutterBinaryMessenger)\n        networkChannel.setMethodCallHandler { [weak self] call, result in\n            let path = self?.latestPath ?? self?.networkMonitor.currentPath\n            guard let path = path, path.status == .satisfied else {\n                result(call.method == \"getIsMobile\" ? false : \"none\")\n                return\n            }\n            if call.method == \"getIsMobile\" {\n                result(path.usesInterfaceType(.cellular))\n                return\n            }\n            guard call.method == \"getNetworkType\" else {\n                result(FlutterMethodNotImplemented)\n                return\n            }\n            if path.usesInterfaceType(.wifi) {\n                result(\"wifi\")\n                return\n            }\n            if path.usesInterfaceType(.cellular) {\n                result(\"mobile\")\n                return\n            }\n            if path.usesInterfaceType(.wiredEthernet) {\n                result(\"ethernet\")\n                return\n            }\n            result(\"other\")\n        }\n\n        //\n        let eventChannel = FlutterEventChannel.init(name: \"flatEvent\", binaryMessenger: controller as! FlutterBinaryMessenger)\n        \n        class EventChannelHandler:NSObject, FlutterStreamHandler {\n             func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {\n                 objc_sync_enter(mutex)\n                 sink = events\n                 objc_sync_exit(mutex)\n                 return nil\n             }\n            \n             func onCancel(withArguments arguments: Any?) -> FlutterError? {\n                 objc_sync_enter(mutex)\n                 sink = nil\n                 objc_sync_exit(mutex)\n                return nil\n            }\n        }\n        class EventNotifyHandler:NSObject, MobileEventNotifyHandlerProtocol {\n            func onNotify(_ message: String?) {\n                objc_sync_enter(mutex)\n                if sink != nil {\n                    sink?(message)\n                }\n                objc_sync_exit(mutex)\n            }\n        }\n        eventChannel.setStreamHandler(EventChannelHandler.init())\n        MobileEventNotify(EventNotifyHandler.init())\n        \n        //\n        GeneratedPluginRegistrant.register(with: self)\n        return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n    }\n}\n\n\nvar sink : FlutterEventSink?\nlet mutex = NSObject.init()\n"
  },
  {
    "path": "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": "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": "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": "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": "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": "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>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>pikapika</string>\n\t<key>CFBundleDocumentTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>PKZ Archive</string>\n\t\t\t<key>LSHandlerRank</key>\n\t\t\t<string>Owner</string>\n\t\t\t<key>LSItemContentTypes</key>\n\t\t\t<array>\n\t\t\t\t<string>opensource.pkz</string>\n\t\t\t\t<string>pkz</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\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>CFBundleLocalizations</key>\n\t<array>\n\t\t<string>zh_TW</string>\n\t\t<string>zh_CN</string>\n\t\t<string>en_US</string>\n\t\t<string>ja_JP</string>\n\t\t<string>ko_KR</string>\n\t</array>\n\t<key>CFBundleName</key>\n\t<string>pikapika</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>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>pika</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSApplicationCategoryType</key>\n\t<string>public.app-category.entertainment</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>LSSupportsOpeningDocumentsInPlace</key>\n\t<true/>\n\t<key>NSFaceIDUsageDescription</key>\n\t<string>Authenticating using face id</string>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string>Save images</string>\n\t<key>NSPhotoLibraryUsageDescription</key>\n\t<string>Usage images</string>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n\t<key>UIFileSharingEnabled</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>UTExportedTypeDeclarations</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>UTTypeConformsTo</key>\n\t\t\t<array>\n\t\t\t\t<string>public.data</string>\n\t\t\t\t<string>public.content</string>\n\t\t\t\t<string>com.apple.package</string>\n\t\t\t</array>\n\t\t\t<key>UTTypeDescription</key>\n\t\t\t<string>PKZ Archive</string>\n\t\t\t<key>UTTypeIdentifier</key>\n\t\t\t<string>opensource.pkz</string>\n\t\t\t<key>UTTypeTagSpecification</key>\n\t\t\t<dict>\n\t\t\t\t<key>public.filename-extension</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>pkz</string>\n\t\t\t\t\t<string>pki</string>\n\t\t\t\t\t<string>zip</string>\n\t\t\t\t</array>\n\t\t\t\t<key>public.mime-type</key>\n\t\t\t\t<array>\n\t\t\t\t\t<string>text/vnd.opensource.pkz</string>\n\t\t\t\t\t<string>text/vnd.opensource.pki</string>\n\t\t\t\t\t<string>text/vnd.opensource.zip</string>\n\t\t\t\t</array>\n\t\t\t</dict>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "content": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "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\t0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 605DB0C59210B25A843453FD /* Pods_Runner.framework */; };\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\tDD1F548D273CB9A900B04493 /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1F548C273CB9A900B04493 /* Mobile.xcframework */; };\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\t1001C50AAB0DFA884ACAD48C /* 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\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\t3742BDBA4B7EA3162E2CDC75 /* 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\t605DB0C59210B25A843453FD /* 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\tCA7EB5DA1FDE22BAC5B01D77 /* 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\tDD1F548C273CB9A900B04493 /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = ../go/mobile/lib/Mobile.xcframework; 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\t0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */,\n\t\t\t\tDD1F548D273CB9A900B04493 /* Mobile.xcframework 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\t6CBB90743F7578DFC9C6BF75 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3742BDBA4B7EA3162E2CDC75 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t1001C50AAB0DFA884ACAD48C /* Pods-Runner.release.xcconfig */,\n\t\t\t\tCA7EB5DA1FDE22BAC5B01D77 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\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\tDD1F548C273CB9A900B04493 /* Mobile.xcframework */,\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t6CBB90743F7578DFC9C6BF75 /* Pods */,\n\t\t\t\tF6DB48AA376F5D49016BEA7A /* 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\t\tF6DB48AA376F5D49016BEA7A /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t605DB0C59210B25A843453FD /* 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\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\t6D683F8ECDB7CFFB7E7E554B /* [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\t774454864019DA9867B5A218 /* [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 = 1430;\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\t6D683F8ECDB7CFFB7E7E554B /* [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\t774454864019DA9867B5A218 /* [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\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\";\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\tFRAMEWORK_SEARCH_PATHS = \"../go/mobile/lib/**\";\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 = 43;\n\t\t\t\tDEVELOPMENT_TEAM = SSSSSSSSSS;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\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\tMARKETING_VERSION = 1.8.19;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika;\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\tTARGETED_DEVICE_FAMILY = \"1,2\";\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\tFRAMEWORK_SEARCH_PATHS = \"../go/mobile/lib/**\";\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\tFRAMEWORK_SEARCH_PATHS = \"../go/mobile/lib/**\";\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 = 43;\n\t\t\t\tDEVELOPMENT_TEAM = SSSSSSSSSS;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\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\tMARKETING_VERSION = 1.8.19;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika;\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\tTARGETED_DEVICE_FAMILY = \"1,2\";\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 = 43;\n\t\t\t\tDEVELOPMENT_TEAM = SSSSSSSSSS;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\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\tMARKETING_VERSION = 1.8.19;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = opensource.pikapika;\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\tTARGETED_DEVICE_FAMILY = \"1,2\";\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": "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": "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": "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": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1430\"\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": "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": "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": "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": "lib/assets/translations/en-US.json",
    "content": "{\n  \"language\": {\n    \"title\": \"Language\",\n    \"name\": \"English - United States\"\n  },\n  \"app\": {\n    \"categories\": \"Categories\",\n    \"my\": \"My\",\n    \"copied_to_clipboard\": \"Copied to clipboard\",\n    \"not_supported_platform\": \"Not supported platform\",\n    \"cancel\": \"Cancel\",\n    \"confirm\": \"Confirm\",\n    \"save_cancel\": \"Save canceled\",\n    \"save_success\": \"Save success\",\n    \"save_failed\": \"Save failed\",\n    \"pro\": \"Pro\",\n    \"pro_required\": \"Please upgrade to Pro to use this feature\",\n    \"choose_folder\": \"Choose a folder to save the file\",\n    \"permission_denied\": \"Permission denied\",\n    \"loading\": \"Loading\",\n    \"error\": \"Error\",\n    \"pat\": {\n      \"success\": \"Your sponsor login success, please return\",\n      \"title\": \"Replace PAT account\"\n    },\n    \"previous_page\": \"Previous page\",\n    \"next_page\": \"Next page\",\n    \"page\": \"Page\",\n    \"please_enter_page_number\": \"Please enter page number:\",\n    \"select_all\": \"Select all\",\n    \"load_failed\": \"Load failed\",\n    \"all\": \"All\",\n    \"delete\": \"Delete\",\n    \"save_image\": \"Save image\",\n    \"preview_image\": \"Preview image\",\n    \"please_select\": \"Please select\",\n    \"refresh\": \"Refresh\",\n    \"initializing\": \"Initializing\",\n    \"like_failed\": \"Like failed\",\n    \"network_error\": \"Network error, please check your network\",\n    \"no_permission\": \"No permission or path is not available\",\n    \"check_device_time\": \"Please check your device time\",\n    \"resource_not_available\": \"Resource not available\",\n    \"something_went_wrong\": \"Something went wrong\",\n    \"click_refresh\": \"Click refresh\",\n    \"pull_down_refresh\": \"Pull down refresh\",\n    \"continue_reading\": \"Continue reading\",\n    \"start_reading\": \"Start reading\",\n    \"image_crop\": \"Image crop\",\n    \"download\": \"Download\",\n    \"download_failed\": \"Download failed\",\n    \"download_finished\": \"Download finished\",\n    \"downloading\": \"Downloading\",\n    \"queue\": \"Queue\",\n    \"deleting\": \"Deleting\",\n    \"please_select_comic\": \"Please select comic\",\n    \"please_choose\": \"Please choose\",\n    \"last_viewed\": \"Last viewed\",\n    \"auto_punch\": \"Auto punch\",\n    \"yes\": \"Yes\",\n    \"no\": \"No\",\n    \"confirm_download\": \"Confirm download\",\n    \"copy\": \"Copy\"\n  },\n  \"net\": {\n    \"no_address\": \"No address\",\n    \"address\": \"Address\",\n    \"address_sync\": \"Address sync\",\n    \"address_sync_from_server\": \"Get the latest address from the server\",\n    \"address_sync_reset\": \"Reset the address to the default value\",\n    \"address_sync_success\": \"Address sync success\",\n    \"address_sync_failed\": \"Address sync failed\",\n    \"address_sync_reset_success\": \"Address sync reset success\",\n    \"address_sync_reset_failed\": \"Address sync reset failed\",\n    \"choose_address\": \"Choose address\",\n    \"image_address\": \"Image address\",\n    \"use_api_load_image\": \"Use API to load image\",\n    \"ping_testing\": \"Testing\",\n    \"ping_failed\": \"Failed\"\n  },\n  \"categories\": {\n    \"all\": \"All\",\n    \"recommend\": \"Recommend\",\n    \"rankings\": \"Rankings\",\n    \"random\": \"Random\",\n    \"game\": \"Game\"\n  },\n  \"settings\": {\n    \"settings\": \"Settings\",\n    \"interface\": \"Interface\",\n    \"network\": \"Network\",\n    \"seal\": \"Seal\",\n    \"interaction\": \"Interaction\",\n    \"reading\": \"Reading\",\n    \"download\": \"Downloads\",\n    \"auto_download_on_favorite\": \"Auto download on favorite\",\n    \"disable_auto_download_on_mobile\": \"Disable auto download on mobile data\",\n    \"auto_delete_download_on_unfavorite\": \"Auto delete download on unfavorite\",\n    \"web_server\": \"Web Server\",\n    \"web_server_subtitle\": \"Let devices in the local network view the downloaded comics through the browser\",\n    \"sync\": \"Sync\",\n    \"history_sync\": \"History Sync\",\n    \"local_favorite_sync_title\": \"Local Favorites Sync\",\n    \"use_local_favorite\": \"Use Local Favorites\",\n    \"use_local_favorite_desc\": \"Manage favorites locally with folder organization\",\n    \"account\": \"Account\",\n    \"modify_password\": \"Modify password\",\n    \"ebook\": \"E-book\",\n    \"system\": \"System\",\n    \"clear_cache\": \"Clear cache\",\n    \"migrate\": \"Migrate\",\n    \"migrate_subtitle\": \"Change your data folder to the memory card\",\n    \"migrate_confirm\": \"This feature will be saved after restarting the program, are you sure\",\n    \"app_orientation\": {\n      \"title\": \"App orientation\",\n      \"choose\": \"Choose app orientation\",\n      \"normal\": \"Normal\",\n      \"landscape\": \"Landscape\",\n      \"portrait\": \"Portrait\"\n    },\n    \"will_pop_notice\": \"Press the back key twice in a row to exit the app\",\n    \"android_secure_flag\": \"Disable screenshot/disable display in task view\",\n    \"android_display_mode\": {\n      \"title\": \"Screen refresh rate (Android)\",\n      \"dialog_title\": \"Android screen refresh rate \\n(No high refresh in power saving mode)\"\n    },\n    \"authentication\": \"Authentication when entering the app (if the system has already entered the password or fingerprint)\",\n    \"set_password\": \"Set application password\",\n    \"auto_clean\": {\n      \"title\": \"Auto clean cache\",\n      \"one_month_ago\": \"One month ago\",\n      \"one_week_ago\": \"One week ago\",\n      \"one_day_ago\": \"One day ago\",\n      \"no_auto_clean\": \"No auto clean\"\n    },\n    \"categories_column_count\": {\n      \"title\": \"Categories column count\",\n      \"choose\": \"Choose categories column count\",\n      \"auto\": \"Auto\"\n    },\n    \"categories_sort\": {\n      \"title\": \"Categories sort\"\n    },\n    \"chooser_root\": {\n      \"title\": \"Chooser root\",\n      \"hint\": \"Enter the folder selector root path\",\n      \"desc\": \"The default path for selecting the directory when exporting, also the root path, you can try to set this option if the export is not normal\"\n    },\n    \"content_failed_reload_action\": {\n      \"title\": \"Content failed reload action\",\n      \"choose\": \"Choose content failed reload action\",\n      \"pull_down\": \"Pull down\",\n      \"touch_loader\": \"Touch loader\"\n    },\n    \"copy_full_name\": {\n      \"title\": \"Copy full name\"\n    },\n    \"copy_full_name_template\": {\n      \"title\": \"Copy full name template\",\n      \"hint\": \"Enter the copy full name template\"\n    },\n    \"copy_skip_confirm\": {\n      \"title\": \"Copy skip confirm\"\n    },\n    \"download_and_export_path\": {\n      \"title\": \"Download and export path\",\n      \"confirm\": \"Download and export\",\n      \"desc\": \"You will select a directory, if the file system is writable, the download will be automatically exported\"\n    },\n    \"download_cache_path\": {\n      \"title\": \"Download cache path\",\n      \"confirm\": \"Download cache\",\n      \"desc\": \"You will select a directory, this directory is copied from the following directory to use. The download will be read as a cache folder first.\",\n      \"cancel_desc\": \"Are you sure you want to cancel the function of downloading content acceleration with other software? You can set it again after canceling\",\n      \"import_view_log_from_off\": {\n        \"title\": \"Import other program's history record\",\n        \"desc\": \"You will select a file, this file is copied from the following path to use.\",\n        \"choose_file_dialog_title\": \"Choose the file to import\"\n      }\n    },\n    \"download_thread_count\": {\n      \"title\": \"Download thread count\",\n      \"choose\": \"Choose download thread count\"\n    },\n    \"ebook_scrolling\": {\n      \"title\": \"E-book scrolling UI\"\n    },\n    \"ebook_scrolling_range\": {\n      \"title\": \"E-book scrolling UI\",\n      \"desc\": \"Scrolling range\",\n      \"screen_height\": \"Screen height\"\n    },\n    \"ebook_scrolling_trigger\": {\n      \"title\": \"E-book scrolling UI\",\n      \"desc\": \"Trigger distance\",\n      \"cm\": \"cm\"\n    },\n    \"export_path\": {\n      \"ios_desc\": \"You can find the exported content in the file manager\",\n      \"ios_desc2\": \"You are using an iOS device:\\nPlease open the system's built-in file manager to browse the exported content\",\n      \"export_path_desc\": \"Export path (click to modify)\",\n      \"android_desc\": \"You are using an Android device:\\nIf the export fails and prompts insufficient permissions, you can try to create a subdirectory under Download or Document for export\"\n    },\n    \"export_rename\": {\n      \"title\": \"Export with renaming\"\n    },\n    \"yes\": \"Yes\",\n    \"no\": \"No\",\n    \"full_screen_action\": {\n      \"title\": \"Control method\",\n      \"choose\": \"Choose control method\",\n      \"touch_once\": \"Touch once to fullscreen\",\n      \"controller\": \"Use controller to fullscreen\",\n      \"touch_double\": \"Double click to fullscreen\",\n      \"touch_double_once_next\": \"Double click to fullscreen + click once to next page\",\n      \"three_area\": \"Divide the screen into three areas (previous page, next page, fullscreen)\"\n    },\n    \"full_screen_ui\": {\n      \"title\": \"Full screen UI\",\n      \"choose\": \"Choose full screen UI\",\n      \"no\": \"Not use\",\n      \"hidden_bottom\": \"Remove virtual controller\",\n      \"all\": \"Full screen\"\n    },\n    \"auto_full_screen\": {\n      \"title\": \"Enter reader automatically full screen\"\n    },\n    \"auto_full_screen_on_forward\": {\n      \"title\": \"Auto Fullscreen on Forward\"\n    }, \n    \"ignore_info_history\": {\n      \"title\": \"Ignore info history\"\n    },\n    \"icon_loading\": {\n      \"title\": \"Minimize UI animation\"\n    },\n    \"ignore_upgrade_confirm\": {\n      \"title\": \"Close upgrade popup\"\n    },\n    \"hidden_fd_icon\": {\n      \"title\": \"Hide personal space's power icon\"\n    },\n    \"hidden_search_persion\": {\n      \"title\": \"Hide search by author\"\n    },\n    \"hidden_viewed\": {\n      \"title\": \"Hide viewed comics\"\n    },\n    \"hidden_sub_icon\": {\n      \"title\": \"Hide subscription\"\n    },\n    \"hide_online_favorite\": {\n      \"title\": \"Hide online favorites\",\n      \"desc\": \"Hide online favorites entry and favorite button\"\n    },\n    \"hidden_words\": {\n      \"title\": \"Hide by keyword\",\n      \"clear_all\": \"Confirm clear\",\n      \"clear_all_desc\": \"Are you sure you want to clear all keywords?\",\n      \"input_hint\": \"Enter the keyword to hide\",\n      \"no_words\": \"No keywords\"\n    },\n    \"image_address\": {\n      \"title\": \"Image address\",\n      \"pinging\": \"Pinging\",\n      \"failed\": \"Failed\"\n    },\n    \"image_filter\": {\n      \"title\": \"Reader image filter\",\n      \"normal\": \"Normal\",\n      \"gray\": \"Gray\",\n      \"brown\": \"Brown\",\n      \"choose\": \"Choose reader image filter\"\n    },\n    \"import_notice\": {\n      \"android_desc\": \"You are using an Android device:\\nIf you cannot import and export and prompt insufficient permissions, you can try to create a subdirectory under Download or Document for import\"\n    },\n    \"keyboard_controller\": {\n      \"title\": \"Reader keyboard page (only PC)\"\n    },\n    \"list_layout\": {\n      \"choose\": \"Choose layout\",\n      \"info_card\": \"Info\",\n      \"only_image\": \"Cover\",\n      \"cover_and_title\": \"Cover + Title\"\n    },\n    \"local_history_sync\": {\n      \"sync_to_local\": \"Sync history to local\",\n      \"not_set\": \"Not set\",\n      \"sync_success\": \"Sync success\",\n      \"sync_failed\": \"Sync failed\",\n      \"auto_sync\": \"Auto sync history to local\",\n      \"auto_sync_desc\": \"After opening the application, the history will be automatically backed up\",\n      \"choose_dir\": \"Choose directory\",\n      \"clear_path\": \"Clear path\",\n      \"clear_path_desc\": \"Are you sure you want to clear the path?\"\n    },\n    \"local_favorite_sync\": {\n      \"auto_sync\": \"Auto sync local favorites\",\n      \"auto_sync_desc\": \"Auto sync local favorites with WebDAV\",\n      \"manual_sync\": \"Manual sync local favorites\",\n      \"sync_success\": \"Sync success\",\n      \"sync_failed\": \"Sync failed\"\n    },\n    \"no_animation\": {\n      \"title\": \"Cancel page animation (tap screen, volume key, keyboard)\"\n    },\n    \"pager_action\": {\n      \"title\": \"List page loading method\",\n      \"choose\": \"Choose list page loading method\",\n      \"controller\": \"Use button\",\n      \"stream\": \"Stream\"\n    },\n    \"proxy\": {\n      \"title\": \"Proxy server\",\n      \"hint\": \"Enter proxy server\",\n      \"desc\": \" ( e.g. socks5://127.0.0.1:1080/ ) \",\n      \"no_proxy\": \"Not set\"\n    },\n    \"quality\": {\n      \"title\": \"Image quality when browsing\",\n      \"choose\": \"Choose image quality\",\n      \"original\": \"Original\",\n      \"low\": \"Low\",\n      \"medium\": \"Medium\",\n      \"high\": \"High\"\n    },\n    \"reader_background_color\": {\n      \"title\": \"Reader background color\",\n      \"choose\": \"Choose reader background color\",\n      \"black\": \"Black\",\n      \"gray\": \"Gray\",\n      \"white\": \"White\"\n    },\n    \"reader_direction\": {\n      \"title\": \"Reader direction\",\n      \"choose\": \"Choose reader direction\",\n      \"top_to_bottom\": \"Top to bottom\",\n      \"left_to_right\": \"Left to right\",\n      \"right_to_left\": \"Right to left\"\n    },\n    \"reader_scroll_by_screen_percentage\": {\n      \"title\": \"Flip distance by distance\",\n      \"screen_size\": \"Screen size\"\n    },\n    \"web_toon_scroll_mode\": {\n      \"title\": \"WebToon Flip Mode\",\n      \"choose\": \"Choose WebToon Flip Mode\",\n      \"image\": \"Image\",\n      \"screen\": \"Distance\"\n    },\n    \"reader_zoom\": {\n      \"out_title\": \"Zoom out multiplier (min scale)\",\n      \"in_title\": \"Zoom in multiplier (max scale)\",\n      \"double_tap_title\": \"Double tap zoom scale\"\n    },\n    \"drag_region_lock\": {\n      \"title\": \"Lock Drag Boundary\"\n    },\n    \"gesture_speed\": {\n      \"title\": \"Gesture Speed Multiplier\"\n    },\n    \"reader_slider_position\": {\n      \"title\": \"Slider position\",\n      \"choose\": \"Choose slider position\",\n      \"bottom\": \"Bottom\",\n      \"right\": \"Right\",\n      \"left\": \"Left\"\n    },\n    \"reader_two_page_direction\": {\n      \"title\": \"Two page reader content arrangement\",\n      \"choose\": \"Choose two page reader content arrangement\",\n      \"close_to\": \"Close to\",\n      \"pull_away\": \"Pull away\",\n      \"each_centered\": \"Each centered\"\n    },\n    \"reader_type\": {\n      \"title\": \"Reader mode\",\n      \"choose\": \"Choose reader mode\",\n      \"web_toon\": \"WebToon (Default)\",\n      \"web_toon_zoom\": \"WebToon (Double click to zoom)\",\n      \"gallery\": \"Gallery\",\n      \"web_toon_free_zoom\": \"WebToon (ListView double click to zoom)\\n(This mode progress bar is invalid)\",\n      \"two_page_gallery\": \"Two page mode\\n(Experimental)\",\n      \"left_to_right\": \"Left to right\",\n      \"right_to_left\": \"Right to left\",\n      \"two_page_direction\": \"Two page direction\",\n      \"two_page_direction_choose\": \"Choose two page direction\"\n    },\n    \"shadow_categories\": {\n      \"title\": \"Seal\",\n      \"search_hint\": \"Search\"\n    },\n    \"shadow_categories_mode\": {\n      \"title\": \"Seal mode\",\n      \"black_list\": \"Black list\",\n      \"white_list\": \"White list\"\n    },\n    \"startup_pic\": {\n      \"title\": \"Set startup picture\",\n      \"subtitle\": \"Set the picture displayed when the application starts\",\n      \"clear_title\": \"Clear startup picture\",\n      \"clear_subtitle\": \"Clear the picture displayed when the application starts\",\n      \"clear_success\": \"Startup picture cleared\",\n      \"update_success\": \"Startup picture updated\"\n    },\n    \"show_comment_at_download\": {\n      \"title\": \"Show comment at download\"\n    },\n    \"font\": {\n      \"title\": \"Font\",\n      \"hint\": \"Please enter the font\",\n      \"input_hint\": \"Please enter the font name and use English commas to separate, for example \\\"Songti, Heiti\\\", if you save it and it doesn't change, it means the font cannot be used or the name is wrong, you can refer to C:\\\\Windows\\\\Fonts to find your font. If you are using the flutter2 engine version, only the first font will take effect.\",\n      \"choose_hint\": \"You need to select multiple fonts until you click the background area\"\n    },\n    \"theme\": {\n      \"origin\": \"Origin\",\n      \"pink\": \"Pink\",\n      \"black\": \"Black\",\n      \"dark\": \"Dark\",\n      \"dusty_blue\": \"Dusty blue\",\n      \"dark_black\": \"Dark black\",\n      \"choose_theme\": \"Choose theme\",\n      \"book\": \"Book\",\n      \"enable_status_bar_color\": \"Enable status bar color\",\n      \"enable_status_restart_hint\": \"When disabled, you need to restart the application to refresh the status bar color\"\n    },\n    \"three_keep_right\": {\n      \"title\": \"Three area mode page always to the right\"\n    },\n    \"time_zone\": {\n      \"title\": \"Time zone\"\n    },\n    \"timeout_lock\": {\n      \"title\": \"Auto lock\",\n      \"notice\": \"Note: Auto lock only supports timeout after minimizing on desktop, and supports timeout after background and screen lock on mobile. If no password is set, auto lock is invalid. Android and desktop only lock the desktop, not the download, iOS is not tested, you need to manually enable background activity.\",\n      \"1_hour\": \"One hour\",\n      \"10_minutes\": \"Ten minutes\",\n      \"3_minutes\": \"Three minutes\",\n      \"1_minute\": \"One minute\",\n      \"10_seconds\": \"Ten seconds\",\n      \"1_second\": \"One second\",\n      \"no_lock\": \"No lock\"\n    },\n    \"using_right_click_pop\": {\n      \"title\": \"Mouse right click to return to the previous page\"\n    },\n    \"volume_controller\": {\n      \"title\": \"Reader volume button page turn\"\n    },\n    \"volume_next_chapter\": {\n      \"title\": \"Double click volume/keyboard/controller to next chapter\"\n    },\n    \"webdav\": {\n      \"title\": \"WebDav\",\n      \"not_set\": \"Not set\",\n      \"path\": \"WebDav path\",\n      \"path_hint\": \"Please enter the WebDav path\",\n      \"username\": \"WebDav username\",\n      \"username_hint\": \"Please enter the WebDav username\",\n      \"password\": \"WebDav password\",\n      \"password_hint\": \"Please enter the WebDav password\",\n      \"auto_sync_history_to_webdav\": \"Auto sync history to WebDav\",\n      \"sync_history_to_webdav\": \"Sync history to WebDAV\",\n      \"upload_history_to_webdav\": \"Upload history to WebDAV\",\n      \"upload_history_to_webdav_desc\": \"If there are multiple devices, please note the automatic synchronization function\",\n      \"sync_success\": \"Sync success\",\n      \"sync_failed\": \"Sync failed\"\n    }\n  },\n  \"local_favorite\": {\n    \"title\": \"Local Favorites\",\n    \"all_folders\": \"All\",\n    \"new_folder\": \"New Folder\",\n    \"select_mode\": \"Select\",\n    \"cancel_select_mode\": \"Cancel select\",\n    \"select_all\": \"Select all\",\n    \"delete_folder\": \"Delete Folder\",\n    \"move_to_folder\": \"Move to Folder\",\n    \"remove_selected\": \"Remove selected\",\n    \"remove_selected_confirm\": \"Remove selected comics from local favorites?\",\n    \"remove_selected_success\": \"Removed\",\n    \"remove_selected_failed\": \"Remove failed\",\n    \"select_comics\": \"Please select comics first\",\n    \"folder_limit_reached\": \"Free version allows up to 3 folders. Upgrade to Pro for unlimited folders\",\n    \"batch_download\": \"Batch Download\",\n    \"select_folder\": \"Select Folder\",\n    \"folder_name\": \"Folder Name\",\n    \"delete_confirm\": \"Confirm delete folder?\",\n    \"empty_folder\": \"No favorites yet\",\n    \"no_folders\": \"No folders yet, please create one first\",\n    \"remove_confirm_title\": \"Remove from Favorites\",\n    \"remove_confirm_content\": \"Are you sure you want to remove this comic from local favorites?\",\n    \"remove_failed\": \"Remove failed\",\n    \"load_failed\": \"Load failed\",\n    \"add_success\": \"Added to local favorites\",\n    \"add_failed\": \"Add failed\",\n    \"create_folder_failed\": \"Create folder failed\",\n    \"create_success\": \"Created successfully\",\n    \"delete_success\": \"Deleted successfully\",\n    \"delete_failed\": \"Delete failed\",\n    \"move_success\": \"Moved successfully\",\n    \"move_failed\": \"Move failed\",\n    \"select_comics_to_download\": \"Please select comics to download\",\n    \"download_started\": \"Download started\",\n    \"download_failed\": \"Download failed\"\n  },\n  \"screen\": {\n    \"about\": {\n      \"title\": \"About\",\n      \"version\": \"Software version\",\n      \"check_update\": \"Check update\",\n      \"tips\": \"Tips : \\n1. The author/uploader/category/tag on the detail page can be clicked\\n2. The author/uploader/title on the detail page can be copied by long press\\n3. Using pagination instead of waterfall flow can quickly flip pages\\n4. Download means caching to the local, you need to export to share\\n5. Download long press can delete\",\n      \"download_new_version\": \"Please download the new version from the channel\",\n      \"no_new_version\": \"No new version detected\",\n      \"download_release_version\": \"Download RELEASE version\",\n      \"update_content\": \"Update content\",\n      \"go_to_release_repository\": \"Go to RELEASE repository\"\n    },\n    \"account\": {\n      \"title\": \"Account\",\n      \"username\": \"Username\",\n      \"username_hint\": \"Please enter the username\",\n      \"password\": \"Password\",\n      \"password_hint\": \"Please enter the password\",\n      \"no_account_register\": \"No account, I want to register\",\n      \"password_reset\": \"Password reset\",\n      \"check_username_password_or_network\": \"Please check the username and password or network environment\",\n      \"check_device_time\": \"Please check the device time\",\n      \"username_or_password_error\": \"Username or password error\",\n      \"login_failed\": \"Login failed\",\n      \"not_set\": \"Not set\"\n    },\n    \"app\": {\n      \"will_pop_notice\": \"Press the back key twice in a row to exit the app\"\n    },\n    \"categories\": {\n      \"search_hint\": \"Search\"\n    },\n    \"clean\": {\n      \"title\": \"Clean\",\n      \"cleaning\": \"Cleaning\",\n      \"clean_network_cache\": \"Clean network cache\",\n      \"clean_image_cache\": \"Clean image cache\",\n      \"clean_all_cache\": \"Clean all cache\",\n      \"clean_success\": \"Clean success\",\n      \"clean_failed\": \"Clean failed\"\n    },\n    \"close_app\": {\n      \"title\": \"Tips\",\n      \"close_app\": \"Please close the app and reopen\"\n    },\n    \"comic_collections\": {\n      \"no_resource\": \"There is no resource here\"\n    },\n    \"comic_info\": {\n      \"chapter\": \"Chapter\",\n      \"comment\": \"Comment\",\n      \"recommend\": \"Recommend\"\n    },\n    \"comics\": {\n      \"search_hint\": \"Search category\",\n      \"choose_category\": \"Please choose category\"\n    },\n    \"comic_subscribes\": {\n      \"update_reminder\": \"Update reminder\",\n      \"check_update\": \"Check update\",\n      \"cancel_all_update_reminder\": \"Cancel all update reminder\"\n    },\n    \"comment\": {\n      \"title\": \"Comment\",\n      \"hint_text\": \"Please enter the comment content\",\n      \"success\": \"Comment success\",\n      \"i_have_something_to_say\": \"I have something to say\",\n      \"please_enter_comment\": \"Please enter the comment content\"\n    },\n    \"desktop_authentication\": {\n      \"current_password\": \"Current password\",\n      \"password_error\": \"Password error\",\n      \"password_initialization\": \"Password initialization\",\n      \"password\": \"Password\",\n      \"re_enter_password\": \"Re-enter password\",\n      \"password_mismatch\": \"The two passwords entered are different\",\n      \"set_password\": \"Set password\"\n    },\n    \"download_confirm\": {\n      \"please_select_ep\": \"Please select the EP to download\",\n      \"already_added_to_download_list\": \"Already added to download list\"\n    },\n    \"download_export_group\": {\n      \"title\": \"Batch export\",\n      \"please_select_content\": \"Please select the content to export\",\n      \"exporting\": \"Exporting\",\n      \"export_failed\": \"Export failed\",\n      \"export_success\": \"Export success\",\n      \"export_to_pkz\": \"Export to PKZ\\n(Encrypted mode, prevent web detection, can be opened with pikapika)\",\n      \"export_to_pki\": \"Export to PKI\\n(Encrypted mode, prevent web detection, can be imported with pikapika)\",\n      \"export_to_zip\": \"Export to ZIP\\n(Unencrypted mode, can be imported or viewed with pikapika)\",\n      \"export_to_jpeg_zip\": \"Export to ZIP+JPEG\\n(Can be used directly with other readers, cannot be imported again)\",\n      \"export_to_jpeg_folder\": \"Export to folder+JPEG\",\n      \"export_to_pdf\": \"Export to PDF\",\n      \"export_to_epub\": \"Export to EPUB\",\n      \"export_to_pdf_folder\": \"Export to folder, each chapter one PDF\",\n      \"export_to_cbz\": \"Export to cbz\",\n      \"after_power_use\": \"After power use\",\n      \"input_save_name\": \"Please enter the saved name\",\n      \"export_confirm\": \"Export confirm\",\n      \"export_to_pkz_title\": \"Export the selected comics to a PKZ\",\n      \"export_to_pki_title\": \"Export the selected comics to separate PKI\",\n      \"please_power_up\": \"Please power up first\",\n      \"export_to_zip_title\": \"Export the selected comics to ZIP\",\n      \"export_to_jpeg_zip_title\": \"Export the selected comics to ZIP+JPEG\",\n      \"export_to_jpeg_zip_title_not_down_over\": \"Export the selected comics to ZIP+JPEG\\n(Even if the download is not successful, it can be used)\",\n      \"export_to_jpeg_folder_title\": \"Export the selected comics to folder+JPEG\",\n      \"export_to_pdf_title\": \"Export the selected comics to PDF\",\n      \"export_to_epub_title\": \"Export the selected comics to EPUB\",\n      \"export_to_pdf_folder_title\": \"Export the selected comics to folder, each chapter one PDF\",\n      \"export_to_cbz_title\": \"Export the selected comics to cbz\",\n      \"exporting_please_wait\": \"Exporting, please wait\"\n    },\n    \"download_export_to_file\": {\n      \"title\": \"Export\",\n      \"transfer_to_other_device\": \"Transfer to other device\",\n      \"input_save_name\": \"Please enter the saved name\",\n      \"export_confirm\": \"Export confirm\",\n      \"export_to_pkz_title\": \"Export the selected comics to a PKZ\",\n      \"export_to_pkz_desc\": \"Export to xxx.pkz\\n(Encrypted mode, prevent web detection, can be opened with pikapika)\",\n      \"export_to_pki_title\": \"Export the selected comics to separate PKI\",\n      \"export_to_pki_desc\": \"Export to xxx.pki\\n(Encrypted mode, prevent web detection, can be imported with pikapika)\",\n      \"export_to_zip_title\": \"Export the selected comics to ZIP\",\n      \"export_to_zip_desc\": \"Export to xxx.zip\\n(Unencrypted mode, can be imported or viewed with pikapika)\",\n      \"export_to_jpeg_zip_title\": \"Export the selected comics to ZIP+JPEG\",\n      \"export_to_jpeg_zip_desc\": \"Export to xxx.jpeg\\n(Can be used directly with other readers, cannot be imported again)\",\n      \"export_to_pdf_title\": \"Export the selected comics to PDF\",\n      \"export_to_pdf_desc\": \"Export to xxx.pdf\\n(Even if the download is not successful, it can be used, and the failed image will be skipped)\\n(Can be opened directly in the photo album)\",\n      \"export_to_pdf_folder_title\": \"Export the selected comics to folder, each chapter one PDF\",\n      \"export_to_pdf_folder_desc\": \"Export to xxx.pdf\\n(Even if the download is not successful, it can be used, and the failed image will be skipped)\\n(Can be opened directly in the photo album)\",\n      \"export_to_epub_title\": \"Export the selected comics to EPUB\",\n      \"export_to_epub_desc\": \"Export to xxx.epub\\n(Can be opened directly in the reader)\",\n      \"export_to_jpeg_folder_title\": \"Export the selected comics to JPEGS.zip\",\n      \"export_to_jpeg_folder_desc\": \"Export to JPGS.zip\\n(Cannot be imported again)\",\n      \"export_to_cbz_title\": \"Export the selected comics to cbk.zip\",\n      \"export_to_cbz_desc\": \"Export to xxx.cbz, reader can use it directly (cannot be imported again)\"\n    },\n    \"download_export_to_socket\": {\n      \"title\": \"Network export\",\n      \"loading\": \"Loading\",\n      \"tips\": \"Do not exit the page before the transfer is successful, only one device can be exported at a time, the two devices need to be in the same network segment or infinite LAN, please enter IP:port on the other device, if there is only one IP, please select the IP of the infinite LAN, usually 192.168 starts\",\n      \"get_ip_failed\": \"Get IP failed\",\n      \"getting_ip\": \"Getting IP\",\n      \"port\": \"Port\"\n    },\n    \"download_import\": {\n      \"title\": \"Import\",\n      \"open_file\": \"Open file\",\n      \"select_file\": \"Select the file to import\",\n      \"import_success\": \"Import success\",\n      \"import_failed\": \"Import failed\",\n      \"select_file_desc\": \"Select zip file to import\\nSelect pki file to import\\nSelect pkz file to read\",\n      \"input_address\": \"Please enter the address provided by the export device\\nFor example \\\"192.168.1.2:50000\\\"\",\n      \"import_from_other_device\": \"Import from other device\",\n      \"select_folder_desc\": \"Select folder\\n(Import all zip/pki in the folder)\\n(After power use)\"\n    },\n    \"download_info\": {\n      \"loading\": \"Loading\",\n      \"chapter\": \"Chapter\",\n      \"comment\": \"Comment\",\n      \"recommend\": \"Recommend\"\n    },\n    \"download_list\": {\n      \"search_download\": \"Search download\",\n      \"multi_select_operation\": \"Multi select operation\",\n      \"download_list\": \"Download list\",\n      \"search\": \"Search\",\n      \"select_folder\": \"Select folder\",\n      \"download_already_in_delete_queue\": \"The download is already in the delete queue\",\n      \"import\": \"Import\",\n      \"export\": \"Export\",\n      \"file\": \"File\",\n      \"download_task\": \"Download task\",\n      \"pause_download\": \"Pause download?\",\n      \"start_download\": \"Start download?\",\n      \"resume_failed\": \"Resume failed task\",\n      \"resume_failed_desc\": \"All failed downloads have been resumed\",\n      \"downloading\": \"Downloading\",\n      \"paused\": \"Paused\",\n      \"move_download\": \"Move download\",\n      \"select_download_to_move\": \"Please select the download to move\",\n      \"select_download_to_delete\": \"Please select the download to delete\",\n      \"input_name\": \"==> Input name <==\",\n      \"empty_folder_will_be_deleted\": \"(Empty folder will be automatically deleted, next time you need to manually input)\",\n      \"folder_name\": \"Folder name\",\n      \"please_input_folder_name\": \"Please input folder name\",\n      \"delete_download\": \"Delete download\",\n      \"delete_selected_download\": \"Delete selected download?\",\n      \"multi_select\": \"Multi select\"\n    },\n    \"download_only_import\": {\n      \"importing\": \"Importing\",\n      \"import_success\": \"Import success\",\n      \"import_failed\": \"Import failed\",\n      \"click_import_file\": \"Click import file\",\n      \"importing_please_wait\": \"Importing, please wait\"\n    },\n    \"favourite_paper\": {\n      \"favourite\": \"Favourite\"\n    },\n    \"forgot_password\": {\n      \"title\": \"Password Recovery\",\n      \"username\": \"Username\",\n      \"not_set\": \"Not set\",\n      \"confirm\": \"Confirm\",\n      \"please_enter_username\": \"Please enter username\",\n      \"question_1\": \"Question 1\",\n      \"question_2\": \"Question 2\",\n      \"question_3\": \"Question 3\",\n      \"answer_1\": \"Answer 1\",\n      \"answer_2\": \"Answer 2\",\n      \"answer_3\": \"Answer 3\",\n      \"please_enter_answer_1\": \"Please enter answer 1\",\n      \"please_enter_answer_2\": \"Please enter answer 2\",\n      \"please_enter_answer_3\": \"Please enter answer 3\",\n      \"use_answer_1_recover\": \"Use answer 1 to recover password\",\n      \"use_answer_2_recover\": \"Use answer 2 to recover password\",\n      \"use_answer_3_recover\": \"Use answer 3 to recover password\",\n      \"please_enter_answer\": \"Please enter answer\",\n      \"new_password_copied\": \"New password is being copied to clipboard\",\n      \"answer_incorrect\": \"Answer is incorrect\",\n      \"password\": \"Password\"\n    },\n    \"game_download\": {\n      \"title\": \"Download\",\n      \"download_links_obtained\": \"Download links obtained, you just need to choose one of them\"\n    },\n    \"game_info\": {\n      \"download\": \"Download\",\n      \"details\": \"Details\",\n      \"comments\": \"Comments\"\n    },\n    \"games\": {\n      \"title\": \"Games\"\n    },\n    \"import_from_off\": {\n      \"title\": \"Import\",\n      \"import_success\": \"Import success\",\n      \"import_failed\": \"Import failed\"\n    },\n    \"modify_password\": {\n      \"title\": \"Modify password\",\n      \"please_wait\": \"Please wait\",\n      \"old_password\": \"Old password\",\n      \"new_password\": \"New password\",\n      \"repeat_new_password\": \"Repeat new password\",\n      \"not_filled\": \"Not filled\",\n      \"please_enter_old_password\": \"Please enter old password\",\n      \"please_enter_new_password\": \"Please enter new password\",\n      \"please_repeat_new_password\": \"Please repeat new password\",\n      \"new_password_mismatch\": \"New passwords do not match\",\n      \"modify_success\": \"Modify success\",\n      \"failed\": \"Failed\",\n      \"confirm\": \"Confirm\"\n    },\n    \"network_settings\": {\n      \"title\": \"Network Settings\"\n    },\n    \"pkz_reader\": {\n      \"reading_downloaded_comic\": \"You are reading a downloaded comic\"\n    },\n    \"pro\": {\n      \"title\": \"Power Center\",\n      \"power_center\": \"Power Center\",\n      \"power_status\": \"Power Status\",\n      \"powered\": \"Powered\",\n      \"not_powered\": \"Not Powered\",\n      \"pat_membership\": \"PAT Membership\",\n      \"pat_status\": \"PAT Status\",\n      \"pat_normal\": \"PAT Normal\",\n      \"pat_bind_hint\": \"Please click here to bind to current account for power\",\n      \"pat_rebind_hint\": \"Please click to rebind to current account for power\",\n      \"pat_not_detected\": \"No membership detected, please go to download page to join\",\n      \"i_have_powered\": \"I have powered before\",\n      \"i_just_powered\": \"I just powered\",\n      \"enter_code\": \"Enter code\",\n      \"power_method\": \"Power Method\",\n      \"wind_power\": \"Wind Power\",\n      \"hydro_power\": \"Hydro Power\",\n      \"solar_power\": \"Solar Power\",\n      \"nuclear_power\": \"Nuclear Power\",\n      \"choose_power_method\": \"Choose power method\",\n      \"sign_in_exchange\": \"Sign in/Exchange\",\n      \"click_pat_to_change\": \"Click PAT membership below to change\",\n      \"update_pat_status\": \"Update PAT power status\",\n      \"bind_to_account\": \"Bind to this account\",\n      \"change_pat_key\": \"Change PAT key\",\n      \"clear_pat_info\": \"Clear PAT info\",\n      \"click_to_bind\": \"Click to bind\",\n      \"enter_auth_code\": \"Please enter authorization code\",\n      \"please_wait\": \"Please wait\",\n      \"key_recorded\": \"Key: Recorded\",\n      \"pat_account\": \"PAT Account\",\n      \"bind_pika_account\": \"Bind PIKA Account\",\n      \"bind_account_time\": \"Bind account time\",\n      \"rebind_time\": \"Rebind available time\",\n      \"power_features\": \"Power features: Multi-thread download / Batch import/export download\",\n      \"power_guide\": \"Go to \\\"About\\\" page to find maintenance address for power guide\\n\\n  \\\"I have powered before\\\" can sync corresponding power status\\n  \\\"I just powered\\\" exchange mysterious code\\n  \\\"Power method\\\" can be changed when network is not working\\n  \\\"PAT membership\\\" is independent power method\"\n    },\n    \"rankings\": {\n      \"title\": \"Rankings\",\n      \"day\": \"Day\",\n      \"week\": \"Week\",\n      \"month\": \"Month\",\n      \"knight\": \"Knight\",\n      \"refresh\": \"Refresh\",\n      \"comics_count\": \"comics\"\n    },\n    \"random_comics\": {\n      \"title\": \"Random Comics\"\n    },\n    \"register\": {\n      \"title\": \"Register\",\n      \"registering\": \"Registering\",\n      \"register_success\": \"Register Success\",\n      \"register_failed\": \"Register Failed\",\n      \"account_exists\": \"Account already exists\",\n      \"name_exists\": \"Name already exists\",\n      \"check_form\": \"Please check form, no empty fields allowed\",\n      \"account\": \"Account\",\n      \"password\": \"Password\",\n      \"nickname\": \"Nickname\",\n      \"gender\": \"Gender\",\n      \"birthday\": \"Birthday\",\n      \"question_1\": \"Question 1\",\n      \"answer_1\": \"Answer 1\",\n      \"question_2\": \"Question 2\",\n      \"answer_2\": \"Answer 2\",\n      \"question_3\": \"Question 3\",\n      \"answer_3\": \"Answer 3\",\n      \"not_set\": \"Not set\",\n      \"please_enter_account\": \"Please enter account\",\n      \"please_enter_password\": \"Please enter password\",\n      \"please_enter_nickname\": \"Please enter nickname\",\n      \"please_enter_question_1\": \"Please enter question 1\",\n      \"please_enter_answer_1\": \"Please enter answer 1\",\n      \"please_enter_question_2\": \"Please enter question 2\",\n      \"please_enter_answer_2\": \"Please enter answer 2\",\n      \"please_enter_question_3\": \"Please enter question 3\",\n      \"please_enter_answer_3\": \"Please enter answer 3\",\n      \"account_desc\": \"(lowercase letters + numbers / for login)\",\n      \"password_desc\": \"(uppercase and lowercase letters + numbers / 8 or more characters)\",\n      \"nickname_desc\": \"(Chinese allowed / 2-50 characters)\",\n      \"choose_gender\": \"Choose your gender\",\n      \"futa\": \"Futa\",\n      \"male\": \"Male\",\n      \"female\": \"Female\",\n      \"register_success_desc\": \"You have registered successfully, please return to login\",\n      \"account_label\": \"Account\",\n      \"nickname_label\": \"Nickname\"\n    },\n    \"search\": {\n      \"title\": \"Search\",\n      \"search_hint\": \"Search\",\n      \"choose_category\": \"Please choose category\"\n    },\n    \"search_author\": {\n      \"title\": \"Search by Author\",\n      \"search_hint\": \"Search by author + \",\n      \"by_author\": \"By author: \"\n    },\n    \"space\": {\n      \"title\": \"My\",\n      \"logout\": \"Logout\",\n      \"logout_confirm\": \"Are you sure you want to logout from current account?\",\n      \"my_favourites\": \"My Favourites\",\n      \"view_history\": \"View History\",\n      \"my_downloads\": \"My Downloads\"\n    },\n    \"theme\": {\n      \"title\": \"Theme Settings\",\n      \"theme\": \"Theme\",\n      \"dark_mode_different_theme\": \"Use different theme in dark mode\",\n      \"dark_mode_theme\": \"Theme (Dark Mode)\"\n    },\n    \"view_logs\": {\n      \"title\": \"View History\",\n      \"clear_all\": \"Do you want to clear all view history?\",\n      \"clear_all_desc\": \"Reading progress will also be deleted!\",\n      \"clear_one\": \"Do you want to clear this view history?\",\n      \"clear_one_desc\": \"Reading progress will also be deleted!\",\n      \"clear_selected\": \"Do you want to clear selected view history?\",\n      \"clear_selected_desc\": \"Reading progress will also be deleted!\",\n      \"categories\": \"Categories\"\n    },\n    \"web_server\": {\n      \"title\": \"Download - Web Server\",\n      \"loading\": \"Loading\",\n      \"get_ip_failed\": \"Get IP failed\",\n      \"getting_ip\": \"Getting IP\",\n      \"port\": \"Port: 8080\",\n      \"usage_instruction\": \"Enter \\\"http://device_ip:8080/\\\" in browser to access downloaded comics\",\n      \"leave_notice\": \"Server will close after leaving this page\"\n    }\n  },\n  \"components\": {\n    \"comic_info_card\": {\n      \"categories\": \"Categories\",\n      \"finished\": \"Finished\",\n      \"viewed\": \"Viewed\"\n    },\n    \"comic_list\": {\n      \"shadow\": \"Shadowed Comics\"\n    },\n    \"common\": {\n      \"display_mode\": \"Display Mode\",\n      \"shadow_mode\": \"Shadow Mode\",\n      \"shadow_list\": \"Shadow List\",\n      \"batch_download\": \"Batch Download\"\n    },\n    \"image_reader\": {\n      \"already_at_the_end\": \"Already at the end\",\n      \"click_to_next_chapter\": \"Click to next chapter\",\n      \"reload_page\": \"Reload page\",\n      \"next_chapter\": \"Next chapter\",\n      \"end_reading\": \"End reading\",\n      \"reload_image\": \"Reload image\",\n      \"save_image_in_this_page\": \"Save image in this page\",\n      \"image_load_failed\": \"Image load failed\"\n    }\n  }\n}\n"
  },
  {
    "path": "lib/assets/translations/ja-JP.json",
    "content": "{\n  \"language\": {\n    \"title\": \"言語\",\n    \"name\": \"日本語 - 日本\"\n  },\n  \"app\": {\n    \"categories\": \"カテゴリ\",\n    \"my\": \"マイ\",\n    \"copied_to_clipboard\": \"クリップボードにコピーしました\",\n    \"not_supported_platform\": \"サポートされていないプラットフォーム\",\n    \"cancel\": \"キャンセル\",\n    \"confirm\": \"確認\",\n    \"save_cancel\": \"保存がキャンセルされました\",\n    \"save_success\": \"保存に成功しました\",\n    \"save_failed\": \"保存に失敗しました\",\n    \"pro\": \"プロ\",\n    \"pro_required\": \"この機能を使用するにはプロにアップグレードしてください\",\n    \"choose_folder\": \"ファイルを保存するフォルダを選択してください\",\n    \"permission_denied\": \"権限が拒否されました\",\n    \"loading\": \"読み込み中\",\n    \"error\": \"エラー\",\n    \"pat\": {\n      \"success\": \"スポンサーログインが成功しました。戻ってください\",\n      \"title\": \"PATアカウントを置き換え\"\n    },\n    \"previous_page\": \"前のページ\",\n    \"next_page\": \"次のページ\",\n    \"page\": \"ページ\",\n    \"please_enter_page_number\": \"ページ番号を入力してください：\",\n    \"select_all\": \"すべて選択\",\n    \"load_failed\": \"読み込みに失敗しました\",\n    \"all\": \"すべて\",\n    \"delete\": \"削除\",\n    \"save_image\": \"画像を保存\",\n    \"preview_image\": \"画像をプレビュー\",\n    \"please_select\": \"選択してください\",\n    \"refresh\": \"更新\",\n    \"initializing\": \"初期化中\",\n    \"like_failed\": \"いいねに失敗しました\",\n    \"network_error\": \"ネットワークエラーです。ネットワークを確認してください\",\n    \"no_permission\": \"権限がないか、パスが利用できません\",\n    \"check_device_time\": \"デバイスの時間を確認してください\",\n    \"resource_not_available\": \"リソースが利用できません\",\n    \"something_went_wrong\": \"何かが間違っています\",\n    \"click_refresh\": \"クリックして更新\",\n    \"pull_down_refresh\": \"下にプルして更新\",\n    \"continue_reading\": \"読み続ける\",\n    \"start_reading\": \"読み始める\",\n    \"image_crop\": \"画像トリミング\",\n    \"download\": \"ダウンロード\",\n    \"download_failed\": \"ダウンロードに失敗しました\",\n    \"download_finished\": \"ダウンロード完了\",\n    \"downloading\": \"ダウンロード中\",\n    \"queue\": \"キュー中\",\n    \"deleting\": \"削除中\",\n    \"please_select_comic\": \"漫画を選択してください\",\n    \"please_choose\": \"選択してください\",\n    \"last_viewed\": \"最後に見た\",\n    \"auto_punch\": \"自動チェックイン\",\n    \"yes\": \"はい\",\n    \"no\": \"いいえ\",\n    \"confirm_download\": \"ダウンロード確認\",\n    \"copy\": \"コピー\"\n  },\n  \"net\": {\n    \"no_address\": \"アドレスなし\",\n    \"address\": \"アドレス\",\n    \"address_sync\": \"アドレス同期\",\n    \"address_sync_from_server\": \"サーバーから最新のアドレスを取得\",\n    \"address_sync_reset\": \"アドレスをデフォルト値にリセット\",\n    \"address_sync_success\": \"アドレス同期に成功しました\",\n    \"address_sync_failed\": \"アドレス同期に失敗しました\",\n    \"address_sync_reset_success\": \"アドレス同期リセットに成功しました\",\n    \"address_sync_reset_failed\": \"アドレス同期リセットに失敗しました\",\n    \"choose_address\": \"アドレスを選択\",\n    \"image_address\": \"画像アドレス\",\n    \"use_api_load_image\": \"APIを使用して画像を読み込む\",\n    \"ping_testing\": \"テスト中\",\n    \"ping_failed\": \"失敗しました\"\n  },\n  \"categories\": {\n    \"all\": \"すべて\",\n    \"recommend\": \"おすすめ\",\n    \"rankings\": \"ランキング\",\n    \"random\": \"ランダム\",\n    \"game\": \"ゲーム\"\n  },\n  \"settings\": {\n    \"settings\": \"設定\",\n    \"interface\": \"インターフェース\",\n    \"network\": \"ネットワーク\",\n    \"seal\": \"封印\",\n    \"interaction\": \"インタラクション\",\n    \"reading\": \"読書\",\n    \"download\": \"ダウンロード\",\n    \"auto_download_on_favorite\": \"お気に入り時に自動ダウンロード\",\n    \"disable_auto_download_on_mobile\": \"モバイル通信時は自動ダウンロードしない\",\n    \"auto_delete_download_on_unfavorite\": \"お気に入り解除で自動削除\",\n    \"web_server\": \"ウェブサーバー\",\n    \"web_server_subtitle\": \"ローカルネットワーク内のデバイスがブラウザを通じてダウンロードした漫画を閲覧できるようにします\",\n    \"sync\": \"同期\",\n    \"account\": \"アカウント\",\n    \"modify_password\": \"Modify password\",\n    \"ebook\": \"電子書籍\",\n    \"system\": \"システム\",\n    \"clear_cache\": \"キャッシュをクリア\",\n    \"migrate\": \"移行\",\n    \"migrate_subtitle\": \"データフォルダをメモリカードに変更\",\n    \"migrate_confirm\": \"この機能はプログラムを再起動した後に保存されます。よろしいですか\",\n    \"app_orientation\": {\n      \"title\": \"アプリの向き\",\n      \"choose\": \"アプリの向きを選択\",\n      \"normal\": \"通常\",\n      \"landscape\": \"横向き\",\n      \"portrait\": \"縦向き\"\n    },\n    \"will_pop_notice\": \"戻るキーを2回連続で押すとアプリを終了します\",\n    \"android_secure_flag\": \"スクリーンショットを無効化/タスクビューでの表示を無効化\",\n    \"android_display_mode\": {\n      \"title\": \"画面リフレッシュレート(Android)\",\n      \"dialog_title\": \"Android画面リフレッシュレート \\n(省電力モードでは高リフレッシュレートになりません)\"\n    },\n    \"authentication\": \"アプリに入る際の認証（システムが既にパスワードまたは指紋を入力している場合）\",\n    \"set_password\": \"アプリケーションパスワードを設定\",\n    \"auto_clean\": {\n      \"title\": \"自動キャッシュクリア\",\n      \"one_month_ago\": \"1ヶ月前\",\n      \"one_week_ago\": \"1週間前\",\n      \"one_day_ago\": \"1日前\",\n      \"no_auto_clean\": \"自動クリアしない\"\n    },\n    \"categories_column_count\": {\n      \"title\": \"カテゴリの列数\",\n      \"choose\": \"カテゴリの列数を選択\",\n      \"auto\": \"自動\"\n    },\n    \"categories_sort\": {\n      \"title\": \"カテゴリソート\"\n    },\n    \"chooser_root\": {\n      \"title\": \"フォルダセレクタルートパス\",\n      \"hint\": \"フォルダセレクタルートパスを入力してください\",\n      \"desc\": \"エクスポート時にディレクトリを選択するデフォルトパス、またルートパスでもあります。エクスポートが正常に動作しない場合は、このオプションを設定してみてください。\"\n    },\n    \"content_failed_reload_action\": {\n      \"title\": \"コンテンツ読み込み失敗時の再読み込み方法\",\n      \"choose\": \"コンテンツ読み込み失敗時の再読み込み方法を選択\",\n      \"pull_down\": \"プルダウン\",\n      \"touch_loader\": \"画面タッチ\"\n    },\n    \"copy_full_name\": {\n      \"title\": \"漫画名コピー時にテンプレートを使用\"\n    },\n    \"copy_full_name_template\": {\n      \"title\": \"漫画名コピーテンプレート\",\n      \"hint\": \"漫画名コピーテンプレートを入力してください\"\n    },\n    \"copy_skip_confirm\": {\n      \"title\": \"長押しコピー時に確認しない\"\n    },\n    \"download_and_export_path\": {\n      \"title\": \"ダウンロードと同時にファイルシステムへエクスポート\",\n      \"confirm\": \"ダウンロードと同時にファイルシステムへエクスポート\",\n      \"desc\": \"ディレクトリを選択すると、ファイルシステムが書き込み可能な場合、ダウンロードと同時に自動的にエクスポートされます\"\n    },\n    \"download_cache_path\": {\n      \"title\": \"他のプログラムのキャッシュを使用してダウンロード加速\",\n      \"confirm\": \"他のプログラムのキャッシュを使用してダウンロード加速\",\n      \"desc\": \"ディレクトリを選択すると、このディレクトリは次のディレクトリからコピーされたものでなければ使用できません。ダウンロード時にキャッシュフォルダとして優先的に読み込まれます。\",\n      \"cancel_desc\": \"他のソフトウェアのダウンロードコンテンツ加速機能をキャンセルしますか？キャンセル後に再度設定することができます\",\n      \"import_view_log_from_off\": {\n        \"title\": \"他のプログラムの履歴記録をインポート\",\n        \"desc\": \"ファイルを選択すると、このファイルは次のパスからコピーされたものでなければ使用できません。\",\n        \"choose_file_dialog_title\": \"インポートするファイルを選択\"\n      }\n    },\n    \"download_thread_count\": {\n      \"title\": \"ダウンロードスレッド数\",\n      \"choose\": \"ダウンロードスレッド数を選択\"\n    },\n    \"ebook_scrolling\": {\n      \"title\": \"電子書籍モードスクロールUI\"\n    },\n    \"ebook_scrolling_range\": {\n      \"title\": \"電子書籍モードスクロールUI\",\n      \"desc\": \"スクロール範囲\",\n      \"screen_height\": \"画面の高さ\"\n    },\n    \"ebook_scrolling_trigger\": {\n      \"title\": \"電子書籍モードスクロールUI\",\n      \"desc\": \"トリガー距離\",\n      \"cm\": \"センチメートル\"\n    },\n    \"export_path\": {\n      \"ios_desc\": \"ファイルマネージャーでエクスポートしたコンテンツを見つけることができます\",\n      \"ios_desc2\": \"iOS デバイスを使用しています：\\nファイルにエクスポートしたコンテンツは、システム内蔵のファイルマネージャーを開いてご覧ください\",\n      \"export_path_desc\": \"エクスポートパス（クリックして変更）\",\n      \"android_desc\": \"Android デバイスを使用しています：\\nエクスポートが失敗して権限不足のエラーが発生した場合、Download または Document の下にサブディレクトリを作成してエクスポートを試してみてください\"\n    },\n    \"export_rename\": {\n      \"title\": \"エクスポート時に名前を変更\"\n    },\n    \"yes\": \"はい\",\n    \"no\": \"いいえ\",\n    \"full_screen_action\": {\n      \"title\": \"操作方法\",\n      \"choose\": \"操作方法を選択\",\n      \"touch_once\": \"画面を一度タッチしてフルスクリーン\",\n      \"controller\": \"コントローラーを使用してフルスクリーン\",\n      \"touch_double\": \"画面をダブルクリックしてフルスクリーン\",\n      \"touch_double_once_next\": \"画面をダブルクリックしてフルスクリーン + 一度クリックで次のページ\",\n      \"three_area\": \"画面を3つの領域に分割（前のページ、次のページ、フルスクリーン）\"\n    },\n    \"full_screen_ui\": {\n      \"title\": \"全屏UI\",\n      \"choose\": \"全屏UIを選択\",\n      \"no\": \"使用しない\",\n      \"hidden_bottom\": \"仮想コントローラーを削除\",\n      \"all\": \"フルスクリーン\"\n    },\n    \"auto_full_screen\": {\n      \"title\": \"リーダーに入ったら自動フルスクリーン\"\n    },\n    \"auto_full_screen_on_forward\": {\n      \"title\": \"前へ進むときに自動全画面表示\"\n    },\n    \"ignore_info_history\": {\n      \"title\": \"詳細ページの履歴記録を除外\"\n    },\n    \"icon_loading\": {\n      \"title\": \"UI アニメーションを最小化\"\n    },\n    \"ignore_upgrade_confirm\": {\n      \"title\": \"アップグレードポップアップを閉じる\"\n    },\n    \"hidden_fd_icon\": {\n      \"title\": \"個人スペースの電源アイコンを非表示\"\n    },\n    \"hidden_search_persion\": {\n      \"title\": \"作者別検索機能を非表示\"\n    },\n    \"hidden_viewed\": {\n      \"title\": \"読んだ漫画を非表示\"\n    },\n    \"hidden_sub_icon\": {\n      \"title\": \"購読機能を非表示\"\n    },\n    \"hide_online_favorite\": {\n      \"title\": \"オンライン收藏を隠す\",\n      \"desc\": \"オンライン收藏の入口と收藏ボタンを隠す\"\n    },\n    \"hidden_words\": {\n      \"title\": \"キーワードで非表示\",\n      \"clear_all\": \"クリア確認\",\n      \"clear_all_desc\": \"すべてのキーワードをクリアしますか？\",\n      \"input_hint\": \"非表示にするキーワードを入力\",\n      \"no_words\": \"キーワードなし\"\n    },\n    \"image_address\": {\n      \"title\": \"画像アドレス\",\n      \"pinging\": \"Ping中\",\n      \"failed\": \"失敗\"\n    },\n    \"image_filter\": {\n      \"title\": \"リーダー画像フィルタ\",\n      \"normal\": \"通常\",\n      \"gray\": \"グレー\",\n      \"brown\": \"ブラウン\",\n      \"choose\": \"リーダー画像フィルタを選択\"\n    },\n    \"import_notice\": {\n      \"android_desc\": \"Android デバイスを使用しています：\\nインポート/エクスポートができず、権限不足のエラーが発生した場合、Download または Document の下にサブディレクトリを作成してインポートを試してみてください\"\n    },\n    \"keyboard_controller\": {\n      \"title\": \"リーダーキーボードページ（PC のみ）\"\n    },\n    \"list_layout\": {\n      \"choose\": \"レイアウトを選択\",\n      \"info_card\": \"情報\",\n      \"only_image\": \"表紙\",\n      \"cover_and_title\": \"表紙 + タイトル\"\n    },\n    \"local_history_sync\": {\n      \"sync_to_local\": \"履歴をローカルに同期\",\n      \"not_set\": \"設定されていません\",\n      \"sync_success\": \"同期成功\",\n      \"sync_failed\": \"同期失敗\",\n      \"auto_sync\": \"履歴をローカルに自動同期\",\n      \"auto_sync_desc\": \"アプリケーションを開いた後、履歴が自動的にバックアップされます\",\n      \"choose_dir\": \"ディレクトリを選択\",\n      \"clear_path\": \"パスをクリア\",\n      \"clear_path_desc\": \"パスをクリアしますか？\"\n    },\n    \"history_sync\": \"履歴同期\",\n    \"local_favorite_sync_title\": \"ローカル收藏同期\",\n    \"use_local_favorite\": \"ローカル收藏を使用\",\n    \"use_local_favorite_desc\": \"ローカルで收藏を管理し、フォルダー分類に対応\",\n    \"local_favorite_sync\": {\n      \"auto_sync\": \"ローカル收藏を自動同期\",\n      \"auto_sync_desc\": \"WebDAVでローカル收藏を自動同期\",\n      \"manual_sync\": \"ローカル收藏を手動同期\",\n      \"sync_success\": \"同期成功\",\n      \"sync_failed\": \"同期失敗\"\n    },\n    \"no_animation\": {\n      \"title\": \"ページアニメーションをキャンセル（画面タップ、音量キー、キーボード）\"\n    },\n    \"pager_action\": {\n      \"title\": \"リストページの読み込み方法\",\n      \"choose\": \"リストページの読み込み方法を選択\",\n      \"controller\": \"ボタンを使用\",\n      \"stream\": \"ストリーム\"\n    },\n    \"proxy\": {\n      \"title\": \"プロキシサーバー\",\n      \"hint\": \"プロキシサーバーを入力\",\n      \"desc\": \" ( 例：socks5://127.0.0.1:1080/ ) \",\n      \"no_proxy\": \"設定されていません\"\n    },\n    \"quality\": {\n      \"title\": \"閲覧時の画像品質\",\n      \"choose\": \"画像品質を選択\",\n      \"original\": \"オリジナル\",\n      \"low\": \"低\",\n      \"medium\": \"中\",\n      \"high\": \"高\"\n    },\n    \"reader_background_color\": {\n      \"title\": \"リーダー背景色\",\n      \"choose\": \"リーダー背景色を選択\",\n      \"black\": \"黒\",\n      \"gray\": \"グレー\",\n      \"white\": \"白\"\n    },\n    \"reader_direction\": {\n      \"title\": \"リーダー方向\",\n      \"choose\": \"リーダー方向を選択\",\n      \"top_to_bottom\": \"上から下へ\",\n      \"left_to_right\": \"左から右へ\",\n      \"right_to_left\": \"右から左へ\"\n    },\n    \"reader_scroll_by_screen_percentage\": {\n      \"title\": \"距離でページをめくる長さ\",\n      \"screen_size\": \"画面サイズ\"\n    },\n    \"web_toon_scroll_mode\": {\n      \"title\": \"WebToon ページめくりモード\",\n      \"choose\": \"WebToon ページめくりモードを選択\",\n      \"image\": \"画像\",\n      \"screen\": \"距離\"\n    },\n    \"reader_zoom\": {\n      \"out_title\": \"縮小倍率（最小ズーム）\",\n      \"in_title\": \"拡大倍率（最大ズーム）\",\n      \"double_tap_title\": \"ダブルタップズーム倍率\"\n    },\n    \"drag_region_lock\": {\n      \"title\": \"ドラッグ境界をロック\"\n    },\n    \"gesture_speed\": {\n      \"title\": \"ジェスチャー速度倍率\"\n    },\n    \"reader_slider_position\": {\n      \"title\": \"スライダー位置\",\n      \"choose\": \"スライダー位置を選択\",\n      \"bottom\": \"下\",\n      \"right\": \"右\",\n      \"left\": \"左\"\n    },\n    \"reader_two_page_direction\": {\n      \"title\": \"2ページリーダーコンテンツ配置\",\n      \"choose\": \"2ページリーダーコンテンツ配置を選択\",\n      \"close_to\": \"近づく\",\n      \"pull_away\": \"離れる\",\n      \"each_centered\": \"それぞれ中央\"\n    },\n    \"reader_type\": {\n      \"title\": \"リーダーモード\",\n      \"choose\": \"リーダーモードを選択\",\n      \"web_toon\": \"WebToon（デフォルト）\",\n      \"web_toon_zoom\": \"WebToon（ダブルクリックでズーム）\",\n      \"gallery\": \"ギャラリー\",\n      \"web_toon_free_zoom\": \"WebToon（ListViewダブルクリックでズーム）\\n（このモードはプログレスバーが無効）\",\n      \"two_page_gallery\": \"2ページモード\\n（実験的）\",\n      \"left_to_right\": \"左から右へ\",\n      \"right_to_left\": \"右から左へ\",\n      \"two_page_direction\": \"2ページ方向\",\n      \"two_page_direction_choose\": \"2ページ方向を選択\"\n    },\n    \"shadow_categories\": {\n      \"title\": \"封印\",\n      \"search_hint\": \"検索\"\n    },\n    \"shadow_categories_mode\": {\n      \"title\": \"封印モード\",\n      \"black_list\": \"ブラックリスト\",\n      \"white_list\": \"ホワイトリスト\"\n    },\n    \"startup_pic\": {\n      \"title\": \"起動画面設定\",\n      \"subtitle\": \"アプリケーション起動時に表示される画像を設定\",\n      \"clear_title\": \"起動画面をクリア\",\n      \"clear_subtitle\": \"アプリケーション起動時に表示される画像をクリア\",\n      \"clear_success\": \"起動画面がクリアされました\",\n      \"update_success\": \"起動画面が更新されました\"\n    },\n    \"show_comment_at_download\": {\n      \"title\": \"ダウンロードでコメントを表示\"\n    },\n    \"font\": {\n      \"title\": \"フォント\",\n      \"hint\": \"フォントを入力してください\",\n      \"input_hint\": \"フォント名を入力し、英語のカンマで区切ってください。例：\\\"MS ゴシック, メイリオ\\\"、保存後に変更されない場合は、フォントが使用できないか名前が間違っていることを意味します。C:\\\\Windows\\\\Fonts を参照してフォントを見つけることができます。flutter2 エンジンバージョンを使用している場合、最初のフォントのみが有効になります。\",\n      \"choose_hint\": \"背景エリアをクリックするまで複数のフォントを選択する必要があります\"\n    },\n    \"theme\": {\n      \"origin\": \"オリジナル\",\n      \"pink\": \"ピンク\",\n      \"black\": \"ブラック\",\n      \"dark\": \"ダーク\",\n      \"dusty_blue\": \"ダスティブルー\",\n      \"dark_black\": \"ダークブラック\",\n      \"choose_theme\": \"テーマを選択\",\n      \"book\": \"本\",\n      \"enable_status_bar_color\": \"ステータスバーの色を有効にする\",\n      \"enable_status_restart_hint\": \"無効になった場合、ステータスバーの色を更新するにはアプリケーションを再起動する必要があります\"\n    },\n    \"three_keep_right\": {\n      \"title\": \"3領域モードページを常に右に\"\n    },\n    \"time_zone\": {\n      \"title\": \"タイムゾーン\"\n    },\n    \"timeout_lock\": {\n      \"title\": \"自動ロック\",\n      \"notice\": \"注意：自動ロックは、デスクトップでは最小化後のタイムアウトのみをサポートし、モバイルではバックグラウンドおよび画面ロック後のタイムアウトをサポートします。パスワードが設定されていない場合、自動ロックは無効です。Android およびデスクトップはデスクトップのみをロックし、ダウンロードはロックしません。iOS はテストされていないため、バックグラウンドアクティビティを手動で有効にする必要があります。\",\n      \"1_hour\": \"1時間\",\n      \"10_minutes\": \"10分\",\n      \"3_minutes\": \"3分\",\n      \"1_minute\": \"1分\",\n      \"10_seconds\": \"10秒\",\n      \"1_second\": \"1秒\",\n      \"no_lock\": \"ロックしない\"\n    },\n    \"using_right_click_pop\": {\n      \"title\": \"マウス右クリックで前のページに戻る\"\n    },\n    \"volume_controller\": {\n      \"title\": \"リーダー音量ボタンページめくり\"\n    },\n    \"volume_next_chapter\": {\n      \"title\": \"音量/キーボード/コントローラーのダブルクリックで次のチャプター\"\n    },\n    \"webdav\": {\n      \"title\": \"WebDav\",\n      \"not_set\": \"未設定\",\n      \"path\": \"WebDav パス\",\n      \"path_hint\": \"WebDav パスを入力してください\",\n      \"username\": \"WebDav ユーザー名\",\n      \"username_hint\": \"WebDav ユーザー名を入力してください\",\n      \"password\": \"WebDav パスワード\",\n      \"password_hint\": \"WebDav パスワードを入力してください\",\n      \"auto_sync_history_to_webdav\": \"履歴を WebDav に自動同期\",\n      \"sync_history_to_webdav\": \"履歴を WebDAV に同期\",\n      \"upload_history_to_webdav\": \"WebDAV の履歴を上書き\",\n      \"upload_history_to_webdav_desc\": \"複数のデバイスがある場合、自動同期機能に注意してください\",\n      \"sync_success\": \"同期成功\",\n      \"sync_failed\": \"同期失敗\"\n    }\n  },\n  \"local_favorite\": {\n    \"title\": \"ローカル收藏\",\n    \"all_folders\": \"すべて\",\n    \"new_folder\": \"新しいフォルダー\",\n    \"select_mode\": \"選択\",\n    \"cancel_select_mode\": \"選択を終了\",\n    \"select_all\": \"すべて選択\",\n    \"delete_folder\": \"フォルダーを削除\",\n    \"move_to_folder\": \"フォルダーへ移動\",\n    \"remove_selected\": \"選択した收藏を削除\",\n    \"remove_selected_confirm\": \"選択した漫画をローカル收藏から削除しますか？\",\n    \"remove_selected_success\": \"削除しました\",\n    \"remove_selected_failed\": \"削除に失敗しました\",\n    \"select_comics\": \"先に漫画を選択してください\",\n    \"folder_limit_reached\": \"無料版は最大3つのフォルダーまでです。Proにアップグレードすると無制限になります\",\n    \"batch_download\": \"一括ダウンロード\",\n    \"select_folder\": \"フォルダーを選択\",\n    \"folder_name\": \"フォルダー名\",\n    \"delete_confirm\": \"フォルダーを削除しますか？\",\n    \"empty_folder\": \"まだ收藏がありません\",\n    \"no_folders\": \"フォルダーがありません。先に作成してください\",\n    \"remove_confirm_title\": \"收藏から削除\",\n    \"remove_confirm_content\": \"この漫画をローカル收藏から削除しますか？\",\n    \"remove_failed\": \"削除に失敗しました\",\n    \"load_failed\": \"読み込みに失敗しました\",\n    \"add_success\": \"ローカル收藏に追加しました\",\n    \"add_failed\": \"追加に失敗しました\",\n    \"create_folder_failed\": \"フォルダー作成に失敗しました\",\n    \"create_success\": \"作成しました\",\n    \"delete_success\": \"削除しました\",\n    \"delete_failed\": \"削除に失敗しました\",\n    \"move_success\": \"移動しました\",\n    \"move_failed\": \"移動に失敗しました\",\n    \"select_comics_to_download\": \"ダウンロードする漫画を選択してください\",\n    \"download_started\": \"ダウンロードを開始しました\",\n    \"download_failed\": \"ダウンロードに失敗しました\"\n  },\n  \"screen\": {\n    \"about\": {\n      \"title\": \"バージョン情報\",\n      \"version\": \"ソフトウェアバージョン\",\n      \"check_update\": \"アップデートを確認\",\n      \"tips\": \"ヒント : \\n1. 詳細ページの作者/アップローダー/カテゴリ/タグはクリックできます\\n2. 詳細ページの作者/アップローダー/タイトルは長押しでコピーできます\\n3. ウォーターフォールフローの代わりにページネーションを使用すると、すばやくページをめくることができます\\n4. ダウンロードはローカルにキャッシュすることを意味し、共有するにはエクスポートする必要があります\\n5. ダウンロードは長押しで削除できます\",\n      \"download_new_version\": \"チャネルから新しいバージョンをダウンロードしてください\",\n      \"no_new_version\": \"新しいバージョンは検出されませんでした\",\n      \"download_release_version\": \"RELEASE版をダウンロード\",\n      \"update_content\": \"更新内容\",\n      \"go_to_release_repository\": \"RELEASEリポジトリへ移動\"\n    },\n    \"account\": {\n      \"title\": \"アカウント\",\n      \"username\": \"ユーザー名\",\n      \"username_hint\": \"ユーザー名を入力してください\",\n      \"password\": \"パスワード\",\n      \"password_hint\": \"パスワードを入力してください\",\n      \"no_account_register\": \"アカウントがありません、登録したいです\",\n      \"password_reset\": \"パスワードをリセット\",\n      \"check_username_password_or_network\": \"ユーザー名とパスワードまたはネットワーク環境を確認してください\",\n      \"check_device_time\": \"デバイスの時刻を確認してください\",\n      \"username_or_password_error\": \"ユーザー名またはパスワードが間違っています\",\n      \"login_failed\": \"ログインに失敗しました\",\n      \"not_set\": \"未設定\"\n    },\n    \"app\": {\n      \"will_pop_notice\": \"戻るキーを2回連続で押すとアプリを終了します\"\n    },\n    \"categories\": {\n      \"search_hint\": \"検索\"\n    },\n    \"clean\": {\n      \"title\": \"クリーン\",\n      \"cleaning\": \"クリーニング中\",\n      \"clean_network_cache\": \"ネットワークキャッシュをクリーン\",\n      \"clean_image_cache\": \"画像キャッシュをクリーン\",\n      \"clean_all_cache\": \"すべてのキャッシュをクリーン\",\n      \"clean_success\": \"クリーン成功\",\n      \"clean_failed\": \"クリーン失敗\"\n    },\n    \"close_app\": {\n      \"title\": \"ヒント\",\n      \"close_app\": \"アプリを閉じて再度開いてください\"\n    },\n    \"comic_collections\": {\n      \"no_resource\": \"ここにはリソースがありません\"\n    },\n    \"comic_info\": {\n      \"chapter\": \"チャプター\",\n      \"comment\": \"コメント\",\n      \"recommend\": \"おすすめ\"\n    },\n    \"comics\": {\n      \"search_hint\": \"カテゴリを検索\",\n      \"choose_category\": \"カテゴリを選択してください\"\n    },\n    \"comic_subscribes\": {\n      \"update_reminder\": \"更新リマインダー\",\n      \"check_update\": \"更新を確認\",\n      \"cancel_all_update_reminder\": \"すべての更新リマインダーをキャンセル\"\n    },\n    \"comment\": {\n      \"title\": \"コメント\",\n      \"hint_text\": \"コメント内容を入力してください\",\n      \"success\": \"コメント成功\",\n      \"i_have_something_to_say\": \"言いたいことがあります\",\n      \"please_enter_comment\": \"コメント内容を入力してください\"\n    },\n    \"desktop_authentication\": {\n      \"current_password\": \"現在のパスワード\",\n      \"password_error\": \"パスワードが間違っています\",\n      \"password_initialization\": \"パスワードの初期化\",\n      \"password\": \"パスワード\",\n      \"re_enter_password\": \"パスワードを再入力\",\n      \"password_mismatch\": \"入力された2つのパスワードが異なります\",\n      \"set_password\": \"パスワードを設定\"\n    },\n    \"download_confirm\": {\n      \"please_select_ep\": \"ダウンロードするEPを選択してください\",\n      \"already_added_to_download_list\": \"すでにダウンロードリストに追加されています\"\n    },\n    \"download_export_group\": {\n      \"title\": \"一括エクスポート\",\n      \"please_select_content\": \"エクスポートするコンテンツを選択してください\",\n      \"exporting\": \"エクスポート中\",\n      \"export_failed\": \"エクスポート失敗\",\n      \"export_success\": \"エクスポート成功\",\n      \"export_to_pkz\": \"PKZにエクスポート\\n(暗号化モード、ウェブ検出を防止、pikapikaで開くことができます)\",\n      \"export_to_pki\": \"PKIにエクスポート\\n(暗号化モード、ウェブ検出を防止、pikapikaでインポートできます)\",\n      \"export_to_zip\": \"ZIPにエクスポート\\n(非暗号化モード、pikapikaでインポートまたは表示できます)\",\n      \"export_to_jpeg_zip\": \"ZIP+JPEGにエクスポート\\n(他のリーダーで直接使用できますが、再インポートはできません)\",\n      \"export_to_jpeg_folder\": \"フォルダ+JPEGにエクスポート\",\n      \"export_to_pdf\": \"PDFにエクスポート\",\n      \"export_to_epub\": \"EPUBにエクスポート\",\n      \"export_to_pdf_folder\": \"フォルダにエクスポート、各章ごとに1つのPDF\",\n      \"export_to_cbz\": \"cbzにエクスポート\",\n      \"after_power_use\": \"パワー使用後\",\n      \"input_save_name\": \"保存名を入力してください\",\n      \"export_confirm\": \"エクスポートの確認\",\n      \"export_to_pkz_title\": \"選択した漫画をPKZにエクスポート\",\n      \"export_to_pki_title\": \"選択した漫画を個別のPKIにエクスポート\",\n      \"please_power_up\": \"最初にパワーアップしてください\",\n      \"export_to_zip_title\": \"選択した漫画をZIPにエクスポート\",\n      \"export_to_jpeg_zip_title\": \"選択した漫画をZIP+JPEGにエクスポート\",\n      \"export_to_jpeg_zip_title_not_down_over\": \"選択した漫画をZIP+JPEGにエクスポート\\n(ダウンロードが成功しなくても使用できます)\",\n      \"export_to_jpeg_folder_title\": \"選択した漫画をフォルダ+JPEGにエクスポート\",\n      \"export_to_pdf_title\": \"選択した漫画をPDFにエクスポート\",\n      \"export_to_epub_title\": \"選択した漫画をEPUBにエクスポート\",\n      \"export_to_pdf_folder_title\": \"選択した漫画をフォルダにエクスポート、各章ごとに1つのPDF\",\n      \"export_to_cbz_title\": \"選択した漫画をcbzにエクスポート\",\n      \"exporting_please_wait\": \"エクスポート中です、しばらくお待ちください\"\n    },\n    \"download_export_to_file\": {\n      \"title\": \"エクスポート\",\n      \"transfer_to_other_device\": \"他のデバイスに転送\",\n      \"input_save_name\": \"保存名を入力してください\",\n      \"export_confirm\": \"エクスポートの確認\",\n      \"export_to_pkz_title\": \"選択した漫画をPKZにエクスポート\",\n      \"export_to_pkz_desc\": \"xxx.pkzにエクスポート\\n(暗号化モード、ウェブ検出を防止、pikapikaで開くことができます)\",\n      \"export_to_pki_title\": \"選択した漫画を個別のPKIにエクスポート\",\n      \"export_to_pki_desc\": \"xxx.pkiにエクスポート\\n(暗号化モード、ウェブ検出を防止、pikapikaでインポートできます)\",\n      \"export_to_zip_title\": \"選択した漫画をZIPにエクスポート\",\n      \"export_to_zip_desc\": \"xxx.zipにエクスポート\\n(非暗号化モード、pikapikaでインポートまたは表示できます)\",\n      \"export_to_jpeg_zip_title\": \"選択した漫画をZIP+JPEGにエクスポート\",\n      \"export_to_jpeg_zip_desc\": \"xxx.jpegにエクスポート\\n(他のリーダーで直接使用できますが、再インポートはできません)\",\n      \"export_to_pdf_title\": \"選択した漫画をPDFにエクスポート\",\n      \"export_to_pdf_desc\": \"xxx.pdfにエクスポート\\n(ダウンロードが成功しなくても使用でき、失敗した画像はスキップされます)\\n(フォトアルバムで直接開くことができます)\",\n      \"export_to_pdf_folder_title\": \"選択した漫画をフォルダにエクスポート、各章ごとに1つのPDF\",\n      \"export_to_pdf_folder_desc\": \"xxx.pdfにエクスポート\\n(ダウンロードが成功しなくても使用でき、失敗した画像はスキップされます)\\n(フォトアルバムで直接開くことができます)\",\n      \"export_to_epub_title\": \"選択した漫画をEPUBにエクスポート\",\n      \"export_to_epub_desc\": \"xxx.epubにエクスポート\\n(リーダーで直接開くことができます)\",\n      \"export_to_jpeg_folder_title\": \"選択した漫画をJPEGS.zipにエクスポート\",\n      \"export_to_jpeg_folder_desc\": \"JPGS.zipにエクスポート\\n(再インポートはできません)\",\n      \"export_to_cbz_title\": \"選択した漫画をcbk.zipにエクスポート\",\n      \"export_to_cbz_desc\": \"xxx.cbzにエクスポート、リーダーで直接使用できます（再インポートはできません）\"\n    },\n    \"download_export_to_socket\": {\n      \"title\": \"ネットワークエクスポート\",\n      \"loading\": \"読み込み中\",\n      \"tips\": \"転送が成功するまでページを終了しないでください。一度に1つのデバイスしかエクスポートできず、2つのデバイスは同じネットワークセグメントまたは無限LANにある必要があります。他のデバイスでIP:portを入力してください。IPが1つしかない場合は、無限LANのIPを選択してください。通常は192.168で始まります\",\n      \"get_ip_failed\": \"IP取得失敗\",\n      \"getting_ip\": \"IP取得中\",\n      \"port\": \"ポート\"\n    },\n    \"download_import\": {\n      \"title\": \"インポート\",\n      \"open_file\": \"ファイルを開く\",\n      \"select_file\": \"インポートするファイルを選択\",\n      \"import_success\": \"インポート成功\",\n      \"import_failed\": \"インポート失敗\",\n      \"select_file_desc\": \"インポートするzipファイルを選択\\nインポートするpkiファイルを選択\\n読むpkzファイルを選択\",\n      \"input_address\": \"エクスポートデバイスから提供されたアドレスを入力してください\\n例：「192.168.1.2:50000」\",\n      \"import_from_other_device\": \"他のデバイスからインポート\",\n      \"select_folder_desc\": \"フォルダを選択\\n(フォルダ内のすべてのzip/pkiをインポート)\\n(パワー使用後)\"\n    },\n    \"download_info\": {\n      \"loading\": \"読み込み中\",\n      \"chapter\": \"チャプター\",\n      \"comment\": \"コメント\",\n      \"recommend\": \"おすすめ\"\n    },\n    \"download_list\": {\n      \"search_download\": \"ダウンロードを検索\",\n      \"multi_select_operation\": \"複数選択操作\",\n      \"download_list\": \"ダウンロードリスト\",\n      \"search\": \"検索\",\n      \"select_folder\": \"フォルダーを選択\",\n      \"download_already_in_delete_queue\": \"ダウンロードはすでに削除キューにあります\",\n      \"import\": \"インポート\",\n      \"export\": \"エクスポート\",\n      \"file\": \"ファイル\",\n      \"download_task\": \"ダウンロードタスク\",\n      \"pause_download\": \"ダウンロードを一時停止しますか？\",\n      \"start_download\": \"ダウンロードを開始しますか？\",\n      \"resume_failed\": \"失敗したタスクを再開\",\n      \"resume_failed_desc\": \"すべての失敗したダウンロードが再開されました\",\n      \"downloading\": \"ダウンロード中\",\n      \"paused\": \"一時停止\",\n      \"move_download\": \"ダウンロードを移動\",\n      \"select_download_to_move\": \"移動するダウンロードを選択してください\",\n      \"select_download_to_delete\": \"削除するダウンロードを選択してください\",\n      \"input_name\": \"==> 名前を入力 <==\",\n      \"empty_folder_will_be_deleted\": \"(空のフォルダは自動的に削除されます、次回は手動で入力する必要があります)\",\n      \"folder_name\": \"フォルダ名\",\n      \"please_input_folder_name\": \"フォルダ名を入力してください\",\n      \"delete_download\": \"ダウンロードを削除\",\n      \"delete_selected_download\": \"選択したダウンロードを削除しますか？\",\n      \"multi_select\": \"複数選択\"\n    },\n    \"download_only_import\": {\n      \"importing\": \"インポート中\",\n      \"import_success\": \"インポート成功\",\n      \"import_failed\": \"インポート失敗\",\n      \"click_import_file\": \"インポートファイルをクリック\",\n      \"importing_please_wait\": \"インポート中です、しばらくお待ちください\"\n    },\n    \"favourite_paper\": {\n      \"favourite\": \"お気に入り\"\n    },\n    \"forgot_password\": {\n      \"title\": \"パスワードの回復\",\n      \"username\": \"ユーザー名\",\n      \"not_set\": \"未設定\",\n      \"confirm\": \"確認\",\n      \"please_enter_username\": \"ユーザー名を入力してください\",\n      \"question_1\": \"質問1\",\n      \"question_2\": \"質問2\",\n      \"question_3\": \"質問3\",\n      \"answer_1\": \"回答1\",\n      \"answer_2\": \"回答2\",\n      \"answer_3\": \"回答3\",\n      \"please_enter_answer_1\": \"回答1を入力してください\",\n      \"please_enter_answer_2\": \"回答2を入力してください\",\n      \"please_enter_answer_3\": \"回答3を入力してください\",\n      \"use_answer_1_recover\": \"回答1を使用してパスワードを回復\",\n      \"use_answer_2_recover\": \"回答2を使用してパスワードを回復\",\n      \"use_answer_3_recover\": \"回答3を使用してパスワードを回復\",\n      \"please_enter_answer\": \"回答を入力してください\",\n      \"new_password_copied\": \"新しいパスワードがクリップボードにコピーされました\",\n      \"answer_incorrect\": \"回答が間違っています\",\n      \"password\": \"パスワード\"\n    },\n    \"game_download\": {\n      \"title\": \"ダウンロード\",\n      \"download_links_obtained\": \"ダウンロードリンクが取得されました、そのうちの1つを選択するだけです\"\n    },\n    \"game_info\": {\n      \"download\": \"ダウンロード\",\n      \"details\": \"詳細\",\n      \"comments\": \"コメント\"\n    },\n    \"games\": {\n      \"title\": \"ゲーム\"\n    },\n    \"import_from_off\": {\n      \"title\": \"インポート\",\n      \"import_success\": \"インポート成功\",\n      \"import_failed\": \"インポート失敗\"\n    },\n    \"modify_password\": {\n      \"title\": \"パスワードを変更\",\n      \"please_wait\": \"お待ちください\",\n      \"old_password\": \"古いパスワード\",\n      \"new_password\": \"新しいパスワード\",\n      \"repeat_new_password\": \"新しいパスワードを繰り返す\",\n      \"not_filled\": \"未入力\",\n      \"please_enter_old_password\": \"古いパスワードを入力してください\",\n      \"please_enter_new_password\": \"新しいパスワードを入力してください\",\n      \"please_repeat_new_password\": \"新しいパスワードを再度入力してください\",\n      \"new_password_mismatch\": \"新しいパスワードが一致しません\",\n      \"modify_success\": \"変更成功\",\n      \"failed\": \"失敗\",\n      \"confirm\": \"確認\"\n    },\n    \"network_settings\": {\n      \"title\": \"ネットワーク設定\"\n    },\n    \"pkz_reader\": {\n      \"reading_downloaded_comic\": \"ダウンロードした漫画を読んでいます\"\n    },\n    \"pro\": {\n      \"title\": \"パワーセンター\",\n      \"power_center\": \"パワーセンター\",\n      \"power_status\": \"パワーステータス\",\n      \"powered\": \"パワーオン\",\n      \"not_powered\": \"パワーオフ\",\n      \"pat_membership\": \"PATメンバーシップ\",\n      \"pat_status\": \"PATステータス\",\n      \"pat_normal\": \"PAT正常\",\n      \"pat_bind_hint\": \"現在のアカウントにバインドしてパワーを得るにはここをクリックしてください\",\n      \"pat_rebind_hint\": \"現在のアカウントに再バインドしてパワーを得るにはクリックしてください\",\n      \"pat_not_detected\": \"メンバーシップが検出されません、ダウンロードページに移動して参加してください\",\n      \"i_have_powered\": \"以前にパワーオンしました\",\n      \"i_just_powered\": \"ちょうどパワーオンしました\",\n      \"enter_code\": \"コードを入力\",\n      \"power_method\": \"パワー方法\",\n      \"wind_power\": \"風力発電\",\n      \"hydro_power\": \"水力発電\",\n      \"solar_power\": \"太陽光発電\",\n      \"nuclear_power\": \"原子力発電\",\n      \"choose_power_method\": \"パワー方法を選択\",\n      \"sign_in_exchange\": \"サインイン/交換\",\n      \"click_pat_to_change\": \"変更するには下のPATメンバーシップをクリックしてください\",\n      \"update_pat_status\": \"PATパワーステータスを更新\",\n      \"bind_to_account\": \"このアカウントにバインド\",\n      \"change_pat_key\": \"PATキーを変更\",\n      \"clear_pat_info\": \"PAT情報をクリア\",\n      \"click_to_bind\": \"クリックしてバインド\",\n      \"enter_auth_code\": \"認証コードを入力してください\",\n      \"please_wait\": \"お待ちください\",\n      \"key_recorded\": \"キー：記録済み\",\n      \"pat_account\": \"PATアカウント\",\n      \"bind_pika_account\": \"PIKAアカウントをバインド\",\n      \"bind_account_time\": \"アカウントのバインド時間\",\n      \"rebind_time\": \"再バインド可能時間\",\n      \"power_features\": \"パワー機能：マルチスレッドダウンロード/一括インポート/エクスポートダウンロード\",\n      \"power_guide\": \"「バージョン情報」ページに移動して、パワーガイドのメンテナンスアドレスを見つけてください\\n\\n  「以前にパワーオンしました」は対応するパワーステータスを同期できます\\n  「ちょうどパワーオンしました」は神秘的なコードを交換します\\n  「パワー方法」はネットワークが機能していないときに変更できます\\n  「PATメンバーシップ」は独立したパワー方法です\"\n    },\n    \"rankings\": {\n      \"title\": \"ランキング\",\n      \"day\": \"日\",\n      \"week\": \"週\",\n      \"month\": \"月\",\n      \"knight\": \"ナイト\",\n      \"refresh\": \"更新\",\n      \"comics_count\": \"漫画\"\n    },\n    \"random_comics\": {\n      \"title\": \"ランダム漫画\"\n    },\n    \"register\": {\n      \"title\": \"登録\",\n      \"registering\": \"登録中\",\n      \"register_success\": \"登録成功\",\n      \"register_failed\": \"登録失敗\",\n      \"account_exists\": \"アカウントはすでに存在します\",\n      \"name_exists\": \"名前はすでに存在します\",\n      \"check_form\": \"フォームを確認してください、空のフィールドは許可されていません\",\n      \"account\": \"アカウント\",\n      \"password\": \"パスワード\",\n      \"nickname\": \"ニックネーム\",\n      \"gender\": \"性別\",\n      \"birthday\": \"誕生日\",\n      \"question_1\": \"質問1\",\n      \"answer_1\": \"回答1\",\n      \"question_2\": \"質問2\",\n      \"answer_2\": \"回答2\",\n      \"question_3\": \"質問3\",\n      \"answer_3\": \"回答3\",\n      \"not_set\": \"未設定\",\n      \"please_enter_account\": \"アカウントを入力してください\",\n      \"please_enter_password\": \"パスワードを入力してください\",\n      \"please_enter_nickname\": \"ニックネームを入力してください\",\n      \"please_enter_question_1\": \"質問1を入力してください\",\n      \"please_enter_answer_1\": \"回答1を入力してください\",\n      \"please_enter_question_2\": \"質問2を入力してください\",\n      \"please_enter_answer_2\": \"回答2を入力してください\",\n      \"please_enter_question_3\": \"質問3を入力してください\",\n      \"please_enter_answer_3\": \"回答3を入力してください\",\n      \"account_desc\": \"（小文字+数字/ログイン用）\",\n      \"password_desc\": \"（大文字と小文字+数字/ 8文字以上）\",\n      \"nickname_desc\": \"（日本語可/ 2-50文字）\",\n      \"choose_gender\": \"性別を選択してください\",\n      \"futa\": \"ふたなり\",\n      \"male\": \"男性\",\n      \"female\": \"女性\",\n      \"register_success_desc\": \"登録に成功しました、ログインに戻ってください\",\n      \"account_label\": \"アカウント\",\n      \"nickname_label\": \"ニックネーム\"\n    },\n    \"search\": {\n      \"title\": \"検索\",\n      \"search_hint\": \"検索\",\n      \"choose_category\": \"カテゴリを選択してください\"\n    },\n    \"search_author\": {\n      \"title\": \"作者で検索\",\n      \"search_hint\": \"作者で検索+\",\n      \"by_author\": \"作者：\"\n    },\n    \"space\": {\n      \"title\": \"マイページ\",\n      \"logout\": \"ログアウト\",\n      \"logout_confirm\": \"現在のアカウントからログアウトしますか？\",\n      \"my_favourites\": \"お気に入り\",\n      \"view_history\": \"閲覧履歴\",\n      \"my_downloads\": \"マイダウンロード\"\n    },\n    \"theme\": {\n      \"title\": \"テーマ設定\",\n      \"theme\": \"テーマ\",\n      \"dark_mode_different_theme\": \"ダークモードで異なるテーマを使用\",\n      \"dark_mode_theme\": \"テーマ（ダークモード）\"\n    },\n    \"view_logs\": {\n      \"title\": \"閲覧履歴\",\n      \"clear_all\": \"すべての閲覧履歴をクリアしますか？\",\n      \"clear_all_desc\": \"読書の進捗も削除されます！\",\n      \"clear_one\": \"この閲覧履歴をクリアしますか？\",\n      \"clear_one_desc\": \"読書の進捗も削除されます！\",\n      \"clear_selected\": \"選択した閲覧履歴をクリアしますか？\",\n      \"clear_selected_desc\": \"読書の進捗も削除されます！\",\n      \"categories\": \"カテゴリ\"\n    },\n    \"web_server\": {\n      \"title\": \"ダウンロード - Webサーバー\",\n      \"loading\": \"読み込み中\",\n      \"get_ip_failed\": \"IP取得失敗\",\n      \"getting_ip\": \"IP取得中\",\n      \"port\": \"ポート：8080\",\n      \"usage_instruction\": \"ブラウザで「http://device_ip:8080/」と入力して、ダウンロードした漫画にアクセスします\",\n      \"leave_notice\": \"このページを離れるとサーバーが閉じます\"\n    }\n  },\n  \"components\": {\n    \"comic_info_card\": {\n      \"categories\": \"カテゴリ\",\n      \"finished\": \"完結\",\n      \"viewed\": \"閲覧済み\"\n    },\n    \"comic_list\": {\n      \"shadow\": \"シャドウコミック\"\n    },\n    \"common\": {\n      \"display_mode\": \"表示モード\",\n      \"shadow_mode\": \"シャドウモード\",\n      \"shadow_list\": \"シャドウリスト\",\n      \"batch_download\": \"一括ダウンロード\"\n    },\n    \"image_reader\": {\n      \"already_at_the_end\": \"すでに最後に達しています\",\n      \"click_to_next_chapter\": \"クリックして次の章へ\",\n      \"reload_page\": \"ページを再読み込み\",\n      \"next_chapter\": \"次の章\",\n      \"end_reading\": \"読書を終了\",\n      \"reload_image\": \"画像を再読み込み\",\n      \"save_image_in_this_page\": \"このページの画像を保存\",\n      \"image_load_failed\": \"画像の読み込みに失敗しました\"\n    }\n  }\n}\n"
  },
  {
    "path": "lib/assets/translations/ko-KR.json",
    "content": "{\n  \"language\": {\n    \"title\": \"언어\",\n    \"name\": \"한국어 - 대한민국\"\n  },\n  \"app\": {\n    \"categories\": \"카테고리\",\n    \"my\": \"내 정보\",\n    \"copied_to_clipboard\": \"클립보드에 복사되었습니다\",\n    \"not_supported_platform\": \"지원되지 않는 플랫폼\",\n    \"cancel\": \"취소\",\n    \"confirm\": \"확인\",\n    \"save_cancel\": \"저장이 취소되었습니다\",\n    \"save_success\": \"저장에 성공했습니다\",\n    \"save_failed\": \"저장에 실패했습니다\",\n    \"pro\": \"프로\",\n    \"pro_required\": \"이 기능을 사용하려면 프로로 업그레이드하세요\",\n    \"choose_folder\": \"파일을 저장할 폴더를 선택하세요\",\n    \"permission_denied\": \"권한이 거부되었습니다\",\n    \"loading\": \"로딩 중\",\n    \"error\": \"오류\",\n    \"pat\": {\n      \"success\": \"스폰서 로그인이 성공했습니다. 돌아가세요\",\n      \"title\": \"PAT 계정 교체\"\n    },\n    \"previous_page\": \"이전 페이지\",\n    \"next_page\": \"다음 페이지\",\n    \"page\": \"페이지\",\n    \"please_enter_page_number\": \"페이지 번호를 입력하세요：\",\n    \"select_all\": \"모두 선택\",\n    \"load_failed\": \"로딩에 실패했습니다\",\n    \"all\": \"모두\",\n    \"delete\": \"삭제\",\n    \"save_image\": \"이미지 저장\",\n    \"preview_image\": \"이미지 미리보기\",\n    \"please_select\": \"선택하세요\",\n    \"refresh\": \"새로고침\",\n    \"initializing\": \"초기화 중\",\n    \"like_failed\": \"좋아요에 실패했습니다\",\n    \"network_error\": \"네트워크 오류입니다. 네트워크를 확인하세요\",\n    \"no_permission\": \"권한이 없거나 경로를 사용할 수 없습니다\",\n    \"check_device_time\": \"장치 시간을 확인하세요\",\n    \"resource_not_available\": \"리소스를 사용할 수 없습니다\",\n    \"something_went_wrong\": \"문제가 발생했습니다\",\n    \"click_refresh\": \"클릭하여 새로고침\",\n    \"pull_down_refresh\": \"아래로 당겨서 새로고침\",\n    \"continue_reading\": \"계속 읽기\",\n    \"start_reading\": \"읽기 시작\",\n    \"image_crop\": \"이미지 자르기\",\n    \"download\": \"다운로드\",\n    \"download_failed\": \"다운로드에 실패했습니다\",\n    \"download_finished\": \"다운로드 완료\",\n    \"downloading\": \"다운로드 중\",\n    \"queue\": \"대기열\",\n    \"deleting\": \"삭제 중\",\n    \"please_select_comic\": \"만화를 선택하세요\",\n    \"please_choose\": \"선택하세요\",\n    \"last_viewed\": \"마지막으로 본\",\n    \"auto_punch\": \"자동 체크인\",\n    \"yes\": \"예\",\n    \"no\": \"아니오\",\n    \"confirm_download\": \"다운로드 확인\",\n    \"copy\": \"복사\"\n  },\n  \"net\": {\n    \"no_address\": \"주소 없음\",\n    \"address\": \"주소\",\n    \"address_sync\": \"주소 동기화\",\n    \"address_sync_from_server\": \"서버에서 최신 주소 가져오기\",\n    \"address_sync_reset\": \"주소를 기본값으로 재설정\",\n    \"address_sync_success\": \"주소 동기화에 성공했습니다\",\n    \"address_sync_failed\": \"주소 동기화에 실패했습니다\",\n    \"address_sync_reset_success\": \"주소 동기화 재설정에 성공했습니다\",\n    \"address_sync_reset_failed\": \"주소 동기화 재설정에 실패했습니다\",\n    \"choose_address\": \"주소 선택\",\n    \"image_address\": \"이미지 주소\",\n    \"use_api_load_image\": \"API를 사용하여 이미지 로드\",\n    \"ping_testing\": \"테스트 중\",\n    \"ping_failed\": \"실패했습니다\"\n  },\n  \"categories\": {\n    \"all\": \"모두\",\n    \"recommend\": \"추천\",\n    \"rankings\": \"랭킹\",\n    \"random\": \"랜덤\",\n    \"game\": \"게임\"\n  },\n  \"settings\": {\n    \"settings\": \"설정\",\n    \"interface\": \"인터페이스\",\n    \"network\": \"네트워크\",\n    \"seal\": \"봉인\",\n    \"interaction\": \"상호작용\",\n    \"reading\": \"읽기\",\n    \"download\": \"다운로드\",\n    \"auto_download_on_favorite\": \"즐겨찾기 시 자동 다운로드\",\n    \"disable_auto_download_on_mobile\": \"모바일 데이터에서는 자동 다운로드 안 함\",\n    \"auto_delete_download_on_unfavorite\": \"즐겨찾기 해제 시 자동 삭제\",\n    \"web_server\": \"웹 서버\",\n    \"web_server_subtitle\": \"로컬 네트워크의 장치가 브라우저를 통해 다운로드한 만화를 볼 수 있게 합니다\",\n    \"sync\": \"동기화\",\n    \"account\": \"계정\",\n    \"modify_password\": \"Modify password\",\n    \"ebook\": \"전자책\",\n    \"system\": \"시스템\",\n    \"clear_cache\": \"캐시 지우기\",\n    \"migrate\": \"이전\",\n    \"migrate_subtitle\": \"데이터 폴더를 메모리 카드로 변경\",\n    \"migrate_confirm\": \"이 기능은 프로그램을 다시 시작한 후에 저장됩니다. 확실합니까\",\n    \"app_orientation\": {\n      \"title\": \"앱 방향\",\n      \"choose\": \"앱 방향 선택\",\n      \"normal\": \"일반\",\n      \"landscape\": \"가로\",\n      \"portrait\": \"세로\"\n    },\n    \"will_pop_notice\": \"뒤로 키를 연속으로 두 번 눌러 앱을 종료합니다\",\n    \"android_secure_flag\": \"스크린샷 비활성화/작업 보기에서 표시 비활성화\",\n    \"android_display_mode\": {\n      \"title\": \"화면 새로고침 속도(Android)\",\n      \"dialog_title\": \"Android 화면 새로고침 속도 \\n(절전 모드에서는 고주사율이 적용되지 않음)\"\n    },\n    \"authentication\": \"앱에 들어갈 때 인증 (시스템이 이미 비밀번호나 지문을 입력한 경우)\",\n    \"set_password\": \"애플리케이션 비밀번호 설정\",\n    \"auto_clean\": {\n      \"title\": \"자동 캐시 정리\",\n      \"one_month_ago\": \"한 달 전\",\n      \"one_week_ago\": \"일주일 전\",\n      \"one_day_ago\": \"하루 전\",\n      \"no_auto_clean\": \"자동 정리 안 함\"\n    },\n    \"categories_column_count\": {\n      \"title\": \"카테고리 열 수\",\n      \"choose\": \"카테고리 열 수 선택\",\n      \"auto\": \"자동\"\n    },\n    \"categories_sort\": {\n      \"title\": \"카테고리 정렬\"\n    },\n    \"chooser_root\": {\n      \"title\": \"폴더 선택기 루트 경로\",\n      \"hint\": \"폴더 선택기 루트 경로를 입력하세요\",\n      \"desc\": \"내보낼 때 디렉토리 선택의 기본 경로이며, 루트 경로이기도 합니다. 내보내기가 정상적으로 작동하지 않을 때 이 옵션을 설정해 볼 수 있습니다.\"\n    },\n    \"content_failed_reload_action\": {\n      \"title\": \"콘텐츠 로드 실패 시 새로고침 방식\",\n      \"choose\": \"콘텐츠 로드 실패 시 새로고침 방식 선택\",\n      \"pull_down\": \"아래로 당기기\",\n      \"touch_loader\": \"화면 터치\"\n    },\n    \"copy_full_name\": {\n      \"title\": \"만화 이름 복사 시 템플릿 사용\"\n    },\n    \"copy_full_name_template\": {\n      \"title\": \"만화 이름 복사 템플릿\",\n      \"hint\": \"만화 이름 복사 템플릿을 입력하세요\"\n    },\n    \"copy_skip_confirm\": {\n      \"title\": \"긴 터치 복사 시 확인 안 함\"\n    },\n    \"download_and_export_path\": {\n      \"title\": \"다운로드와 동시에 파일 시스템으로 내보내기\",\n      \"confirm\": \"다운로드와 동시에 파일 시스템으로 내보내기\",\n      \"desc\": \"디렉토리를 선택하면, 파일 시스템이 쓰기 가능한 경우 다운로드와 동시에 자동으로 내보내집니다\"\n    },\n    \"download_cache_path\": {\n      \"title\": \"다른 프로그램의 캐시를 사용하여 다운로드 가속화\",\n      \"confirm\": \"다른 프로그램의 캐시를 사용하여 다운로드 가속화\",\n      \"desc\": \"디렉토리를 선택하면, 이 디렉토리는 다음 디렉토리에서 복사된 것이어야 사용할 수 있습니다. 다운로드 시 캐시 폴더로 우선 읽어집니다.\",\n      \"cancel_desc\": \"다른 소프트웨어의 다운로드 콘텐츠 가속화 기능을 취소하시겠습니까? 취소 후 다시 설정할 수 있습니다\",\n      \"import_view_log_from_off\": {\n        \"title\": \"다른 프로그램의 기록 가져오기\",\n        \"desc\": \"파일을 선택하면, 이 파일은 다음 경로에서 복사된 것이어야 사용할 수 있습니다.\",\n        \"choose_file_dialog_title\": \"가져올 파일 선택\"\n      }\n    },\n    \"download_thread_count\": {\n      \"title\": \"다운로드 스레드 수\",\n      \"choose\": \"다운로드 스레드 수 선택\"\n    },\n    \"ebook_scrolling\": {\n      \"title\": \"전자책 모드 스크롤 UI\"\n    },\n    \"ebook_scrolling_range\": {\n      \"title\": \"전자책 모드 스크롤 UI\",\n      \"desc\": \"스크롤 범위\",\n      \"screen_height\": \"화면 높이\"\n    },\n    \"ebook_scrolling_trigger\": {\n      \"title\": \"전자책 모드 스크롤 UI\",\n      \"desc\": \"트리거 거리\",\n      \"cm\": \"센티미터\"\n    },\n    \"export_path\": {\n      \"ios_desc\": \"파일 관리자에서 내보낸 콘텐츠를 찾을 수 있습니다\",\n      \"ios_desc2\": \"iOS 장치를 사용하고 있습니다:\\n파일로 내보낸 콘텐츠는 시스템 내장 파일 관리자를 열어서 찾아보세요\",\n      \"export_path_desc\": \"내보내기 경로 (클릭하여 수정)\",\n      \"android_desc\": \"Android 장치를 사용하고 있습니다:\\n내보내기가 실패하고 권한 부족 오류가 발생하면, Download 또는 Document 하위에 서브디렉토리를 만들어 내보내기를 시도해 보세요\"\n    },\n    \"export_rename\": {\n      \"title\": \"내보낼 때 이름 바꾸기\"\n    },\n    \"yes\": \"예\",\n    \"no\": \"아니오\",\n    \"full_screen_action\": {\n      \"title\": \"조작 방식\",\n      \"choose\": \"조작 방식 선택\",\n      \"touch_once\": \"화면 한 번 터치로 전체 화면\",\n      \"controller\": \"컨트롤러 사용으로 전체 화면\",\n      \"touch_double\": \"화면 두 번 터치로 전체 화면\",\n      \"touch_double_once_next\": \"화면 두 번 터치로 전체 화면 + 한 번 터치로 다음 페이지\",\n      \"three_area\": \"화면을 세 영역으로 나누기 (이전 페이지, 다음 페이지, 전체 화면)\"\n    },\n    \"full_screen_ui\": {\n      \"title\": \"전체 화면 UI\",\n      \"choose\": \"전체 화면 UI 선택\",\n      \"no\": \"사용 안 함\",\n      \"hidden_bottom\": \"가상 컨트롤러 제거\",\n      \"all\": \"전체 화면\"\n    },\n    \"auto_full_screen\": {\n      \"title\": \"리더 진입 시 자동 전체 화면\"\n    },\n    \"auto_full_screen_on_forward\": {\n      \"title\": \"앞으로 갈 때 자동 전체 화면\"\n    },\n    \"ignore_info_history\": {\n      \"title\": \"상세 페이지 기록 제외\"\n    },\n    \"icon_loading\": {\n      \"title\": \"UI 애니메이션 최소화\"\n    },\n    \"ignore_upgrade_confirm\": {\n      \"title\": \"업그레이드 팝업 닫기\"\n    },\n    \"hidden_fd_icon\": {\n      \"title\": \"개인 공간의 전원 아이콘 숨기기\"\n    },\n    \"hidden_search_persion\": {\n      \"title\": \"작가별 검색 기능 숨기기\"\n    },\n    \"hidden_viewed\": {\n      \"title\": \"읽은 만화 숨기기\"\n    },\n    \"hidden_sub_icon\": {\n      \"title\": \"구독 기능 숨기기\"\n    },\n    \"hide_online_favorite\": {\n      \"title\": \"온라인 즐겨찾기 숨기기\",\n      \"desc\": \"온라인 즐겨찾기 진입 및 즐겨찾기 버튼 숨기기\"\n    },\n    \"hidden_words\": {\n      \"title\": \"키워드로 숨기기\",\n      \"clear_all\": \"지우기 확인\",\n      \"clear_all_desc\": \"모든 키워드를 지우시겠습니까?\",\n      \"input_hint\": \"숨길 키워드를 입력하세요\",\n      \"no_words\": \"키워드 없음\"\n    },\n    \"image_address\": {\n      \"title\": \"이미지 주소\",\n      \"pinging\": \"핑 테스트 중\",\n      \"failed\": \"실패\"\n    },\n    \"image_filter\": {\n      \"title\": \"리더 이미지 필터\",\n      \"normal\": \"일반\",\n      \"gray\": \"회색\",\n      \"brown\": \"갈색\",\n      \"choose\": \"리더 이미지 필터 선택\"\n    },\n    \"import_notice\": {\n      \"android_desc\": \"Android 장치를 사용하고 있습니다:\\n가져오기/내보내기가 안 되고 권한 부족 오류가 발생하면, Download 또는 Document 하위에 서브디렉토리를 만들어 가져오기를 시도해 보세요\"\n    },\n    \"keyboard_controller\": {\n      \"title\": \"리더 키보드 페이지 (PC만 해당)\"\n    },\n    \"list_layout\": {\n      \"choose\": \"레이아웃 선택\",\n      \"info_card\": \"정보\",\n      \"only_image\": \"표지\",\n      \"cover_and_title\": \"표지 + 제목\"\n    },\n    \"local_history_sync\": {\n      \"sync_to_local\": \"기록을 로컬에 동기화\",\n      \"not_set\": \"설정되지 않음\",\n      \"sync_success\": \"동기화 성공\",\n      \"sync_failed\": \"동기화 실패\",\n      \"auto_sync\": \"기록을 로컬에 자동 동기화\",\n      \"auto_sync_desc\": \"애플리케이션을 열 때마다 기록이 자동으로 백업됩니다\",\n      \"choose_dir\": \"디렉터리 선택\",\n      \"clear_path\": \"경로 지우기\",\n      \"clear_path_desc\": \"경로를 지우시겠습니까?\"\n    },\n    \"history_sync\": \"기록 동기화\",\n    \"local_favorite_sync_title\": \"로컬 즐겨찾기 동기화\",\n    \"use_local_favorite\": \"로컬 즐겨찾기 사용\",\n    \"use_local_favorite_desc\": \"즐겨찾기를 로컬에서 관리하고 폴더 분류를 지원합니다\",\n    \"local_favorite_sync\": {\n      \"auto_sync\": \"로컬 즐겨찾기 자동 동기화\",\n      \"auto_sync_desc\": \"WebDAV로 로컬 즐겨찾기를 자동 동기화\",\n      \"manual_sync\": \"로컬 즐겨찾기 수동 동기화\",\n      \"sync_success\": \"동기화 성공\",\n      \"sync_failed\": \"동기화 실패\"\n    },\n    \"no_animation\": {\n      \"title\": \"페이지 애니메이션 취소 (화면 터치, 볼륨 키, 키보드)\"\n    },\n    \"pager_action\": {\n      \"title\": \"목록 페이지 로딩 방법\",\n      \"choose\": \"목록 페이지 로딩 방법 선택\",\n      \"controller\": \"버튼 사용\",\n      \"stream\": \"스트림\"\n    },\n    \"proxy\": {\n      \"title\": \"프록시 서버\",\n      \"hint\": \"프록시 서버를 입력하세요\",\n      \"desc\": \" ( 예: socks5://127.0.0.1:1080/ ) \",\n      \"no_proxy\": \"설정되지 않음\"\n    },\n    \"quality\": {\n      \"title\": \"탐색 시 이미지 품질\",\n      \"choose\": \"이미지 품질 선택\",\n      \"original\": \"원본\",\n      \"low\": \"낮음\",\n      \"medium\": \"보통\",\n      \"high\": \"높음\"\n    },\n    \"reader_background_color\": {\n      \"title\": \"리더 배경색\",\n      \"choose\": \"리더 배경색 선택\",\n      \"black\": \"검은색\",\n      \"gray\": \"회색\",\n      \"white\": \"흰색\"\n    },\n    \"reader_direction\": {\n      \"title\": \"리더 방향\",\n      \"choose\": \"리더 방향 선택\",\n      \"top_to_bottom\": \"위에서 아래로\",\n      \"left_to_right\": \"왼쪽에서 오른쪽으로\",\n      \"right_to_left\": \"오른쪽에서 왼쪽으로\"\n    },\n    \"reader_scroll_by_screen_percentage\": {\n      \"title\": \"거리별 페이지 넘김 길이\",\n      \"screen_size\": \"화면 크기\"\n    },\n    \"web_toon_scroll_mode\": {\n      \"title\": \"WebToon 페이지 넘김 모드\",\n      \"choose\": \"WebToon 페이지 넘김 모드 선택\",\n      \"image\": \"이미지\",\n      \"screen\": \"거리\"\n    },\n    \"reader_zoom\": {\n      \"out_title\": \"축소 배율 (최소 확대)\",\n      \"in_title\": \"확대 배율 (최대 확대)\",\n      \"double_tap_title\": \"더블탭 확대 배율\"\n    },\n    \"drag_region_lock\": {\n      \"title\": \"드래그 경계 잠금\"\n    },\n    \"gesture_speed\": {\n      \"title\": \"제스처 속도 배율\"\n    },\n    \"reader_slider_position\": {\n      \"title\": \"슬라이더 위치\",\n      \"choose\": \"슬라이더 위치 선택\",\n      \"bottom\": \"아래\",\n      \"right\": \"오른쪽\",\n      \"left\": \"왼쪽\"\n    },\n    \"reader_two_page_direction\": {\n      \"title\": \"두 페이지 리더 콘텐츠 배열\",\n      \"choose\": \"두 페이지 리더 콘텐츠 배열 선택\",\n      \"close_to\": \"가깝게\",\n      \"pull_away\": \"멀리\",\n      \"each_centered\": \"각각 중앙\"\n    },\n    \"reader_type\": {\n      \"title\": \"리더 모드\",\n      \"choose\": \"리더 모드 선택\",\n      \"web_toon\": \"웹툰 (기본)\",\n      \"web_toon_zoom\": \"웹툰 (더블 클릭으로 확대)\",\n      \"gallery\": \"갤러리\",\n      \"web_toon_free_zoom\": \"웹툰 (ListView 더블 클릭으로 확대)\\n(이 모드는 진행률 표시줄이 무효)\",\n      \"two_page_gallery\": \"두 페이지 모드\\n(실험적)\",\n      \"left_to_right\": \"왼쪽에서 오른쪽으로\",\n      \"right_to_left\": \"오른쪽에서 왼쪽으로\",\n      \"two_page_direction\": \"두 페이지 방향\",\n      \"two_page_direction_choose\": \"두 페이지 방향 선택\"\n    },\n    \"shadow_categories\": {\n      \"title\": \"봉인\",\n      \"search_hint\": \"검색\"\n    },\n    \"shadow_categories_mode\": {\n      \"title\": \"봉인 모드\",\n      \"black_list\": \"블랙 리스트\",\n      \"white_list\": \"화이트 리스트\"\n    },\n    \"startup_pic\": {\n      \"title\": \"시작 화면 설정\",\n      \"subtitle\": \"애플리케이션 시작 시 표시되는 이미지 설정\",\n      \"clear_title\": \"시작 화면 지우기\",\n      \"clear_subtitle\": \"애플리케이션 시작 시 표시되는 이미지 지우기\",\n      \"clear_success\": \"시작 화면이 지워졌습니다\",\n      \"update_success\": \"시작 화면이 업데이트되었습니다\"\n    },\n    \"show_comment_at_download\": {\n      \"title\": \"다운로드에서 댓글 표시\"\n    },\n    \"font\": {\n      \"title\": \"글꼴\",\n      \"hint\": \"글꼴을 입력하세요\",\n      \"input_hint\": \"글꼴 이름을 입력하고 영어 쉼표로 구분하세요. 예: \\\"굴림, 돋움\\\", 저장 후 변경되지 않으면 글꼴을 사용할 수 없거나 이름이 잘못되었음을 의미합니다. C:\\\\Windows\\\\Fonts를 참조하여 글꼴을 찾을 수 있습니다. flutter2 엔진 버전을 사용하는 경우 첫 번째 글꼴만 적용됩니다.\",\n      \"choose_hint\": \"배경 영역을 클릭할 때까지 여러 글꼴을 선택해야 합니다\"\n    },\n    \"theme\": {\n      \"origin\": \"원본\",\n      \"pink\": \"핑크\",\n      \"black\": \"검은색\",\n      \"dark\": \"다크\",\n      \"dusty_blue\": \"먼지 파란색\",\n      \"dark_black\": \"진한 검은색\",\n      \"choose_theme\": \"테마 선택\",\n      \"book\": \"책\",\n      \"enable_status_bar_color\": \"상태 표시줄 색상 활성화\",\n      \"enable_status_restart_hint\": \"비활성화된 경우 상태 표시줄 색상을 새로 고치려면 애플리케이션을 다시 시작해야 합니다\"\n    },\n    \"three_keep_right\": {\n      \"title\": \"세 영역 모드 페이지를 항상 오른쪽으로\"\n    },\n    \"time_zone\": {\n      \"title\": \"시간대\"\n    },\n    \"timeout_lock\": {\n      \"title\": \"자동 잠금\",\n      \"notice\": \"참고: 자동 잠금은 데스크톱에서 최소화 후 타임아웃만 지원하며, 모바일에서는 백그라운드 및 화면 잠금 후 타임아웃을 지원합니다. 비밀번호가 설정되지 않은 경우 자동 잠금이 무효입니다. Android와 데스크톱은 데스크톱만 잠그고 다운로드는 잠그지 않으며, iOS는 테스트되지 않았으므로 백그라운드 활동을 수동으로 활성화해야 합니다.\",\n      \"1_hour\": \"1시간\",\n      \"10_minutes\": \"10분\",\n      \"3_minutes\": \"3분\",\n      \"1_minute\": \"1분\",\n      \"10_seconds\": \"10초\",\n      \"1_second\": \"1초\",\n      \"no_lock\": \"잠금 안 함\"\n    },\n    \"using_right_click_pop\": {\n      \"title\": \"마우스 오른쪽 클릭으로 이전 페이지로 돌아가기\"\n    },\n    \"volume_controller\": {\n      \"title\": \"리더 볼륨 버튼 페이지 넘기기\"\n    },\n    \"volume_next_chapter\": {\n      \"title\": \"볼륨/키보드/컨트롤러 더블 클릭으로 다음 챕터\"\n    },\n    \"webdav\": {\n      \"title\": \"WebDav\",\n      \"not_set\": \"설정되지 않음\",\n      \"path\": \"WebDav 경로\",\n      \"path_hint\": \"WebDav 경로를 입력하세요\",\n      \"username\": \"WebDav 사용자명\",\n      \"username_hint\": \"WebDav 사용자명을 입력하세요\",\n      \"password\": \"WebDav 비밀번호\",\n      \"password_hint\": \"WebDav 비밀번호를 입력하세요\",\n      \"auto_sync_history_to_webdav\": \"WebDav에 기록 자동 동기화\",\n      \"sync_history_to_webdav\": \"기록을 WebDAV에 동기화\",\n      \"upload_history_to_webdav\": \"WebDAV의 기록 덮어쓰기\",\n      \"upload_history_to_webdav_desc\": \"여러 장치가 있는 경우 자동 동기화 기능에 유의하세요\",\n      \"sync_success\": \"동기화 성공\",\n      \"sync_failed\": \"동기화 실패\"\n    }\n  },\n  \"local_favorite\": {\n    \"title\": \"로컬 즐겨찾기\",\n    \"all_folders\": \"전체\",\n    \"new_folder\": \"새 폴더\",\n    \"select_mode\": \"선택\",\n    \"cancel_select_mode\": \"선택 취소\",\n    \"select_all\": \"전체 선택\",\n    \"delete_folder\": \"폴더 삭제\",\n    \"move_to_folder\": \"폴더로 이동\",\n    \"remove_selected\": \"선택 항목 제거\",\n    \"remove_selected_confirm\": \"선택한 만화를 로컬 즐겨찾기에서 제거할까요?\",\n    \"remove_selected_success\": \"제거됨\",\n    \"remove_selected_failed\": \"제거 실패\",\n    \"select_comics\": \"먼저 만화를 선택하세요\",\n    \"folder_limit_reached\": \"무료 버전은 폴더를 최대 3개까지 만들 수 있습니다. Pro로 업그레이드하면 무제한입니다\",\n    \"batch_download\": \"일괄 다운로드\",\n    \"select_folder\": \"폴더 선택\",\n    \"folder_name\": \"폴더 이름\",\n    \"delete_confirm\": \"폴더를 삭제하시겠습니까?\",\n    \"empty_folder\": \"아직 즐겨찾기가 없습니다\",\n    \"no_folders\": \"폴더가 없습니다. 먼저 폴더를 만들어주세요\",\n    \"remove_confirm_title\": \"즐겨찾기에서 제거\",\n    \"remove_confirm_content\": \"이 만화를 로컬 즐겨찾기에서 제거하시겠습니까?\",\n    \"remove_failed\": \"제거 실패\",\n    \"load_failed\": \"불러오기 실패\",\n    \"add_success\": \"로컬 즐겨찾기에 추가됨\",\n    \"add_failed\": \"추가 실패\",\n    \"create_folder_failed\": \"폴더 생성 실패\",\n    \"create_success\": \"생성됨\",\n    \"delete_success\": \"삭제됨\",\n    \"delete_failed\": \"삭제 실패\",\n    \"move_success\": \"이동됨\",\n    \"move_failed\": \"이동 실패\",\n    \"select_comics_to_download\": \"다운로드할 만화를 선택하세요\",\n    \"download_started\": \"다운로드 시작됨\",\n    \"download_failed\": \"다운로드 실패\"\n  },\n  \"screen\": {\n    \"about\": {\n      \"title\": \"정보\",\n      \"version\": \"소프트웨어 버전\",\n      \"check_update\": \"업데이트 확인\",\n      \"tips\": \"팁 : \\n1. 상세 페이지의 작가/업로더/카테고리/태그는 클릭 가능합니다\\n2. 상세 페이지의 작가/업로더/제목은 길게 눌러 복사할 수 있습니다\\n3. 폭포수 흐름 대신 페이지네이션을 사용하면 빠르게 페이지를 넘길 수 있습니다\\n4. 다운로드는 로컬에 캐시하는 것을 의미하며, 공유하려면 내보내야 합니다\\n5. 다운로드는 길게 눌러 삭제할 수 있습니다\",\n      \"download_new_version\": \"채널에서 새 버전을 다운로드하십시오\",\n      \"no_new_version\": \"새 버전이 없습니다\",\n      \"download_release_version\": \"RELEASE 버전 다운로드\",\n      \"update_content\": \"업데이트 내용\",\n      \"go_to_release_repository\": \"RELEASE 저장소로 이동\"\n    },\n    \"account\": {\n      \"title\": \"계정\",\n      \"username\": \"사용자 이름\",\n      \"username_hint\": \"사용자 이름을 입력하세요\",\n      \"password\": \"비밀번호\",\n      \"password_hint\": \"비밀번호를 입력하세요\",\n      \"no_account_register\": \"계정이 없습니다, 등록하고 싶습니다\",\n      \"password_reset\": \"비밀번호 재설정\",\n      \"check_username_password_or_network\": \"사용자 이름과 비밀번호 또는 네트워크 환경을 확인하세요\",\n      \"check_device_time\": \"장치 시간을 확인하세요\",\n      \"username_or_password_error\": \"사용자 이름 또는 비밀번호 오류\",\n      \"login_failed\": \"로그인 실패\",\n      \"not_set\": \"설정되지 않음\"\n    },\n    \"app\": {\n      \"will_pop_notice\": \"뒤로 키를 연속으로 두 번 눌러 앱을 종료합니다\"\n    },\n    \"categories\": {\n      \"search_hint\": \"검색\"\n    },\n    \"clean\": {\n      \"title\": \"정리\",\n      \"cleaning\": \"정리 중\",\n      \"clean_network_cache\": \"네트워크 캐시 정리\",\n      \"clean_image_cache\": \"이미지 캐시 정리\",\n      \"clean_all_cache\": \"모든 캐시 정리\",\n      \"clean_success\": \"정리 성공\",\n      \"clean_failed\": \"정리 실패\"\n    },\n    \"close_app\": {\n      \"title\": \"팁\",\n      \"close_app\": \"앱을 닫고 다시 열어주세요\"\n    },\n    \"comic_collections\": {\n      \"no_resource\": \"여기에 리소스가 없습니다\"\n    },\n    \"comic_info\": {\n      \"chapter\": \"챕터\",\n      \"comment\": \"댓글\",\n      \"recommend\": \"추천\"\n    },\n    \"comics\": {\n      \"search_hint\": \"카테고리 검색\",\n      \"choose_category\": \"카테고리를 선택하세요\"\n    },\n    \"comic_subscribes\": {\n      \"update_reminder\": \"업데이트 알림\",\n      \"check_update\": \"업데이트 확인\",\n      \"cancel_all_update_reminder\": \"모든 업데이트 알림 취소\"\n    },\n    \"comment\": {\n      \"title\": \"댓글\",\n      \"hint_text\": \"댓글 내용을 입력하세요\",\n      \"success\": \"댓글 성공\",\n      \"i_have_something_to_say\": \"할 말이 있습니다\",\n      \"please_enter_comment\": \"댓글 내용을 입력하세요\"\n    },\n    \"desktop_authentication\": {\n      \"current_password\": \"현재 비밀번호\",\n      \"password_error\": \"비밀번호 오류\",\n      \"password_initialization\": \"비밀번호 초기화\",\n      \"password\": \"비밀번호\",\n      \"re_enter_password\": \"비밀번호 다시 입력\",\n      \"password_mismatch\": \"입력한 두 비밀번호가 다릅니다\",\n      \"set_password\": \"비밀번호 설정\"\n    },\n    \"download_confirm\": {\n      \"please_select_ep\": \"다운로드할 EP를 선택하세요\",\n      \"already_added_to_download_list\": \"이미 다운로드 목록에 추가되었습니다\"\n    },\n    \"download_export_group\": {\n      \"title\": \"일괄 내보내기\",\n      \"please_select_content\": \"내보낼 콘텐츠를 선택하세요\",\n      \"exporting\": \"내보내는 중\",\n      \"export_failed\": \"내보내기 실패\",\n      \"export_success\": \"내보내기 성공\",\n      \"export_to_pkz\": \"PKZ로 내보내기\\n(암호화 모드, 웹 감지 방지, pikapika로 열 수 있음)\",\n      \"export_to_pki\": \"PKI로 내보내기\\n(암호화 모드, 웹 감지 방지, pikapika로 가져올 수 있음)\",\n      \"export_to_zip\": \"ZIP으로 내보내기\\n(암호화되지 않은 모드, pikapika로 가져오거나 볼 수 있음)\",\n      \"export_to_jpeg_zip\": \"ZIP+JPEG로 내보내기\\n(다른 리더에서 직접 사용할 수 있으며, 다시 가져올 수 없음)\",\n      \"export_to_jpeg_folder\": \"폴더+JPEG로 내보내기\",\n      \"export_to_pdf\": \"PDF로 내보내기\",\n      \"export_to_epub\": \"EPUB로 내보내기\",\n      \"export_to_pdf_folder\": \"폴더로 내보내기, 각 챕터마다 PDF 하나씩\",\n      \"export_to_cbz\": \"cbz로 내보내기\",\n      \"after_power_use\": \"전원 사용 후\",\n      \"input_save_name\": \"저장할 이름을 입력하세요\",\n      \"export_confirm\": \"내보내기 확인\",\n      \"export_to_pkz_title\": \"선택한 만화를 PKZ로 내보내기\",\n      \"export_to_pki_title\": \"선택한 만화를 별도의 PKI로 내보내기\",\n      \"please_power_up\": \"먼저 전원을 켜주세요\",\n      \"export_to_zip_title\": \"선택한 만화를 ZIP으로 내보내기\",\n      \"export_to_jpeg_zip_title\": \"선택한 만화를 ZIP+JPEG로 내보내기\",\n      \"export_to_jpeg_zip_title_not_down_over\": \"선택한 만화를 ZIP+JPEG로 내보내기\\n(다운로드가 성공하지 않아도 사용할 수 있음)\",\n      \"export_to_jpeg_folder_title\": \"선택한 만화를 폴더+JPEG로 내보내기\",\n      \"export_to_pdf_title\": \"선택한 만화를 PDF로 내보내기\",\n      \"export_to_epub_title\": \"선택한 만화를 EPUB로 내보내기\",\n      \"export_to_pdf_folder_title\": \"선택한 만화를 폴더로 내보내기, 각 챕터마다 PDF 하나씩\",\n      \"export_to_cbz_title\": \"선택한 만화를 cbz로 내보내기\",\n      \"exporting_please_wait\": \"내보내는 중, 잠시만 기다려주세요\"\n    },\n    \"download_export_to_file\": {\n      \"title\": \"내보내기\",\n      \"transfer_to_other_device\": \"다른 장치로 전송\",\n      \"input_save_name\": \"저장할 이름을 입력하세요\",\n      \"export_confirm\": \"내보내기 확인\",\n      \"export_to_pkz_title\": \"선택한 만화를 PKZ로 내보내기\",\n      \"export_to_pkz_desc\": \"xxx.pkz로 내보내기\\n(암호화 모드, 웹 감지 방지, pikapika로 열 수 있음)\",\n      \"export_to_pki_title\": \"선택한 만화를 별도의 PKI로 내보내기\",\n      \"export_to_pki_desc\": \"xxx.pki로 내보내기\\n(암호화 모드, 웹 감지 방지, pikapika로 가져올 수 있음)\",\n      \"export_to_zip_title\": \"선택한 만화를 ZIP으로 내보내기\",\n      \"export_to_zip_desc\": \"xxx.zip으로 내보내기\\n(암호화되지 않은 모드, pikapika로 가져오거나 볼 수 있음)\",\n      \"export_to_jpeg_zip_title\": \"선택한 만화를 ZIP+JPEG로 내보내기\",\n      \"export_to_jpeg_zip_desc\": \"xxx.jpeg로 내보내기\\n(다른 리더에서 직접 사용할 수 있으며, 다시 가져올 수 없음)\",\n      \"export_to_pdf_title\": \"선택한 만화를 PDF로 내보내기\",\n      \"export_to_pdf_desc\": \"xxx.pdf로 내보내기\\n(다운로드가 성공하지 않아도 사용할 수 있으며, 실패한 이미지는 건너뜁니다)\\n(사진 앨범에서 직접 열 수 있음)\",\n      \"export_to_pdf_folder_title\": \"선택한 만화를 폴더로 내보내기, 각 챕터마다 PDF 하나씩\",\n      \"export_to_pdf_folder_desc\": \"xxx.pdf로 내보내기\\n(다운로드가 성공하지 않아도 사용할 수 있으며, 실패한 이미지는 건너뜁니다)\\n(사진 앨범에서 직접 열 수 있음)\",\n      \"export_to_epub_title\": \"선택한 만화를 EPUB로 내보내기\",\n      \"export_to_epub_desc\": \"xxx.epub로 내보내기\\n(리더에서 직접 열 수 있음)\",\n      \"export_to_jpeg_folder_title\": \"선택한 만화를 JPEGS.zip으로 내보내기\",\n      \"export_to_jpeg_folder_desc\": \"JPGS.zip으로 내보내기\\n(다시 가져올 수 없음)\",\n      \"export_to_cbz_title\": \"선택한 만화를 cbk.zip으로 내보내기\",\n      \"export_to_cbz_desc\": \"xxx.cbz로 내보내기, 리더에서 직접 사용할 수 있음 (다시 가져올 수 없음)\"\n    },\n    \"download_export_to_socket\": {\n      \"title\": \"네트워크 내보내기\",\n      \"loading\": \"로딩 중\",\n      \"tips\": \"전송이 성공하기 전에 페이지를 나가지 마십시오. 한 번에 하나의 장치만 내보낼 수 있으며, 두 장치는 동일한 네트워크 세그먼트 또는 무한 LAN에 있어야 합니다. 다른 장치에서 IP:port를 입력하십시오. IP가 하나만 있는 경우 무한 LAN의 IP를 선택하십시오. 일반적으로 192.168로 시작합니다\",\n      \"get_ip_failed\": \"IP 가져오기 실패\",\n      \"getting_ip\": \"IP 가져오는 중\",\n      \"port\": \"포트\"\n    },\n    \"download_import\": {\n      \"title\": \"가져오기\",\n      \"open_file\": \"파일 열기\",\n      \"select_file\": \"가져올 파일 선택\",\n      \"import_success\": \"가져오기 성공\",\n      \"import_failed\": \"가져오기 실패\",\n      \"select_file_desc\": \"가져올 zip 파일 선택\\n가져올 pki 파일 선택\\n읽을 pkz 파일 선택\",\n      \"input_address\": \"내보내기 장치에서 제공한 주소를 입력하세요\\n예: \\\"192.168.1.2:50000\\\"\",\n      \"import_from_other_device\": \"다른 장치에서 가져오기\",\n      \"select_folder_desc\": \"폴더 선택\\n(폴더의 모든 zip/pki 가져오기)\\n(전원 사용 후)\"\n    },\n    \"download_info\": {\n      \"loading\": \"로딩 중\",\n      \"chapter\": \"챕터\",\n      \"comment\": \"댓글\",\n      \"recommend\": \"추천\"\n    },\n    \"download_list\": {\n      \"search_download\": \"다운로드 검색\",\n      \"multi_select_operation\": \"다중 선택 작업\",\n      \"download_list\": \"다운로드 목록\",\n      \"search\": \"검색\",\n      \"select_folder\": \"폴더 선택\",\n      \"download_already_in_delete_queue\": \"다운로드가 이미 삭제 대기열에 있습니다\",\n      \"import\": \"가져오기\",\n      \"export\": \"내보내기\",\n      \"file\": \"파일\",\n      \"download_task\": \"다운로드 작업\",\n      \"pause_download\": \"다운로드를 일시 중지하시겠습니까?\",\n      \"start_download\": \"다운로드를 시작하시겠습니까?\",\n      \"resume_failed\": \"실패한 작업 다시 시작\",\n      \"resume_failed_desc\": \"모든 실패한 다운로드가 다시 시작되었습니다\",\n      \"downloading\": \"다운로드 중\",\n      \"paused\": \"일시 중지됨\",\n      \"move_download\": \"다운로드 이동\",\n      \"select_download_to_move\": \"이동할 다운로드를 선택하세요\",\n      \"select_download_to_delete\": \"삭제할 다운로드를 선택하세요\",\n      \"input_name\": \"==> 이름 입력 <==\",\n      \"empty_folder_will_be_deleted\": \"(빈 폴더는 자동으로 삭제되며, 다음에 수동으로 입력해야 합니다)\",\n      \"folder_name\": \"폴더 이름\",\n      \"please_input_folder_name\": \"폴더 이름을 입력하세요\",\n      \"delete_download\": \"다운로드 삭제\",\n      \"delete_selected_download\": \"선택한 다운로드를 삭제하시겠습니까?\",\n      \"multi_select\": \"다중 선택\"\n    },\n    \"download_only_import\": {\n      \"importing\": \"가져오는 중\",\n      \"import_success\": \"가져오기 성공\",\n      \"import_failed\": \"가져오기 실패\",\n      \"click_import_file\": \"가져올 파일 클릭\",\n      \"importing_please_wait\": \"가져오는 중, 잠시만 기다려주세요\"\n    },\n    \"favourite_paper\": {\n      \"favourite\": \"즐겨찾기\"\n    },\n    \"forgot_password\": {\n      \"title\": \"비밀번호 복구\",\n      \"username\": \"사용자 이름\",\n      \"not_set\": \"설정되지 않음\",\n      \"confirm\": \"확인\",\n      \"please_enter_username\": \"사용자 이름을 입력하세요\",\n      \"question_1\": \"질문 1\",\n      \"question_2\": \"질문 2\",\n      \"question_3\": \"질문 3\",\n      \"answer_1\": \"답변 1\",\n      \"answer_2\": \"답변 2\",\n      \"answer_3\": \"답변 3\",\n      \"please_enter_answer_1\": \"답변 1을 입력하세요\",\n      \"please_enter_answer_2\": \"답변 2를 입력하세요\",\n      \"please_enter_answer_3\": \"답변 3을 입력하세요\",\n      \"use_answer_1_recover\": \"답변 1을 사용하여 비밀번호 복구\",\n      \"use_answer_2_recover\": \"답변 2를 사용하여 비밀번호 복구\",\n      \"use_answer_3_recover\": \"답변 3을 사용하여 비밀번호 복구\",\n      \"please_enter_answer\": \"답변을 입력하세요\",\n      \"new_password_copied\": \"새 비밀번호가 클립보드에 복사되었습니다\",\n      \"answer_incorrect\": \"답변이 올바르지 않습니다\",\n      \"password\": \"비밀번호\"\n    },\n    \"game_download\": {\n      \"title\": \"다운로드\",\n      \"download_links_obtained\": \"다운로드 링크를 얻었습니다. 그 중 하나를 선택하면 됩니다\"\n    },\n    \"game_info\": {\n      \"download\": \"다운로드\",\n      \"details\": \"상세 정보\",\n      \"comments\": \"댓글\"\n    },\n    \"games\": {\n      \"title\": \"게임\"\n    },\n    \"import_from_off\": {\n      \"title\": \"가져오기\",\n      \"import_success\": \"가져오기 성공\",\n      \"import_failed\": \"가져오기 실패\"\n    },\n    \"modify_password\": {\n      \"title\": \"비밀번호 수정\",\n      \"please_wait\": \"잠시만 기다려주세요\",\n      \"old_password\": \"이전 비밀번호\",\n      \"new_password\": \"새 비밀번호\",\n      \"repeat_new_password\": \"새 비밀번호 반복\",\n      \"not_filled\": \"채워지지 않음\",\n      \"please_enter_old_password\": \"이전 비밀번호를 입력하세요\",\n      \"please_enter_new_password\": \"새 비밀번호를 입력하세요\",\n      \"please_repeat_new_password\": \"새 비밀번호를 다시 입력하세요\",\n      \"new_password_mismatch\": \"새 비밀번호가 일치하지 않습니다\",\n      \"modify_success\": \"수정 성공\",\n      \"failed\": \"실패\",\n      \"confirm\": \"확인\"\n    },\n    \"network_settings\": {\n      \"title\": \"네트워크 설정\"\n    },\n    \"pkz_reader\": {\n      \"reading_downloaded_comic\": \"다운로드한 만화를 읽고 있습니다\"\n    },\n    \"pro\": {\n      \"title\": \"파워 센터\",\n      \"power_center\": \"파워 센터\",\n      \"power_status\": \"파워 상태\",\n      \"powered\": \"전원 켜짐\",\n      \"not_powered\": \"전원 꺼짐\",\n      \"pat_membership\": \"PAT 멤버십\",\n      \"pat_status\": \"PAT 상태\",\n      \"pat_normal\": \"PAT 정상\",\n      \"pat_bind_hint\": \"전원을 위해 현재 계정에 바인딩하려면 여기를 클릭하세요\",\n      \"pat_rebind_hint\": \"전원을 위해 현재 계정에 다시 바인딩하려면 클릭하세요\",\n      \"pat_not_detected\": \"멤버십이 감지되지 않았습니다. 다운로드 페이지로 이동하여 가입하세요\",\n      \"i_have_powered\": \"이전에 전원을 켰습니다\",\n      \"i_just_powered\": \"방금 전원을 켰습니다\",\n      \"enter_code\": \"코드 입력\",\n      \"power_method\": \"전원 방식\",\n      \"wind_power\": \"풍력 발전\",\n      \"hydro_power\": \"수력 발전\",\n      \"solar_power\": \"태양광 발전\",\n      \"nuclear_power\": \"원자력 발전\",\n      \"choose_power_method\": \"전원 방식 선택\",\n      \"sign_in_exchange\": \"로그인/교환\",\n      \"click_pat_to_change\": \"변경하려면 아래의 PAT 멤버십을 클릭하세요\",\n      \"update_pat_status\": \"PAT 전원 상태 업데이트\",\n      \"bind_to_account\": \"이 계정에 바인딩\",\n      \"change_pat_key\": \"PAT 키 변경\",\n      \"clear_pat_info\": \"PAT 정보 지우기\",\n      \"click_to_bind\": \"바인딩하려면 클릭\",\n      \"enter_auth_code\": \"인증 코드를 입력하세요\",\n      \"please_wait\": \"잠시만 기다려주세요\",\n      \"key_recorded\": \"키: 기록됨\",\n      \"pat_account\": \"PAT 계정\",\n      \"bind_pika_account\": \"PIKA 계정 바인딩\",\n      \"bind_account_time\": \"계정 바인딩 시간\",\n      \"rebind_time\": \"재바인딩 가능 시간\",\n      \"power_features\": \"파워 기능: 다중 스레드 다운로드 / 일괄 가져오기/내보내기 다운로드\",\n      \"power_guide\": \"\\\"정보\\\" 페이지로 이동하여 파워 가이드를 위한 유지 관리 주소를 찾으십시오\\n\\n  \\\"이전에 전원을 켰습니다\\\"는 해당 파워 상태를 동기화할 수 있습니다\\n  \\\"방금 전원을 켰습니다\\\"는 신비한 코드를 교환합니다\\n  \\\"전원 방식\\\"은 네트워크가 작동하지 않을 때 변경할 수 있습니다\\n  \\\"PAT 멤버십\\\"은 독립적인 전원 방식입니다\"\n    },\n    \"rankings\": {\n      \"title\": \"순위\",\n      \"day\": \"일\",\n      \"week\": \"주\",\n      \"month\": \"월\",\n      \"knight\": \"기사\",\n      \"refresh\": \"새로고침\",\n      \"comics_count\": \"만화\"\n    },\n    \"random_comics\": {\n      \"title\": \"랜덤 만화\"\n    },\n    \"register\": {\n      \"title\": \"등록\",\n      \"registering\": \"등록 중\",\n      \"register_success\": \"등록 성공\",\n      \"register_failed\": \"등록 실패\",\n      \"account_exists\": \"계정이 이미 존재합니다\",\n      \"name_exists\": \"이름이 이미 존재합니다\",\n      \"check_form\": \"양식을 확인하세요, 빈 필드는 허용되지 않습니다\",\n      \"account\": \"계정\",\n      \"password\": \"비밀번호\",\n      \"nickname\": \"닉네임\",\n      \"gender\": \"성별\",\n      \"birthday\": \"생일\",\n      \"question_1\": \"질문 1\",\n      \"answer_1\": \"답변 1\",\n      \"question_2\": \"질문 2\",\n      \"answer_2\": \"답변 2\",\n      \"question_3\": \"질문 3\",\n      \"answer_3\": \"답변 3\",\n      \"not_set\": \"설정되지 않음\",\n      \"please_enter_account\": \"계정을 입력하세요\",\n      \"please_enter_password\": \"비밀번호를 입력하세요\",\n      \"please_enter_nickname\": \"닉네임을 입력하세요\",\n      \"please_enter_question_1\": \"질문 1을 입력하세요\",\n      \"please_enter_answer_1\": \"답변 1을 입력하세요\",\n      \"please_enter_question_2\": \"질문 2를 입력하세요\",\n      \"please_enter_answer_2\": \"답변 2를 입력하세요\",\n      \"please_enter_question_3\": \"질문 3을 입력하세요\",\n      \"please_enter_answer_3\": \"답변 3을 입력하세요\",\n      \"account_desc\": \"(소문자 + 숫자 / 로그인용)\",\n      \"password_desc\": \"(대소문자 + 숫자 / 8자 이상)\",\n      \"nickname_desc\": \"(한글 허용 / 2-50자)\",\n      \"choose_gender\": \"성별을 선택하세요\",\n      \"futa\": \"후타\",\n      \"male\": \"남성\",\n      \"female\": \"여성\",\n      \"register_success_desc\": \"성공적으로 등록되었습니다. 로그인으로 돌아가세요\",\n      \"account_label\": \"계정\",\n      \"nickname_label\": \"닉네임\"\n    },\n    \"search\": {\n      \"title\": \"검색\",\n      \"search_hint\": \"검색\",\n      \"choose_category\": \"카테고리를 선택하세요\"\n    },\n    \"search_author\": {\n      \"title\": \"작가별 검색\",\n      \"search_hint\": \"작가별 검색 + \",\n      \"by_author\": \"작가별: \"\n    },\n    \"space\": {\n      \"title\": \"내 정보\",\n      \"logout\": \"로그아웃\",\n      \"logout_confirm\": \"현재 계정에서 로그아웃하시겠습니까?\",\n      \"my_favourites\": \"내 즐겨찾기\",\n      \"view_history\": \"조회 기록\",\n      \"my_downloads\": \"내 다운로드\"\n    },\n    \"theme\": {\n      \"title\": \"테마 설정\",\n      \"theme\": \"테마\",\n      \"dark_mode_different_theme\": \"다크 모드에서 다른 테마 사용\",\n      \"dark_mode_theme\": \"테마 (다크 모드)\"\n    },\n    \"view_logs\": {\n      \"title\": \"조회 기록\",\n      \"clear_all\": \"모든 조회 기록을 지우시겠습니까?\",\n      \"clear_all_desc\": \"읽기 진행 상황도 삭제됩니다!\",\n      \"clear_one\": \"이 조회 기록을 지우시겠습니까?\",\n      \"clear_one_desc\": \"읽기 진행 상황도 삭제됩니다!\",\n      \"clear_selected\": \"선택한 조회 기록을 지우시겠습니까?\",\n      \"clear_selected_desc\": \"읽기 진행 상황도 삭제됩니다!\",\n      \"categories\": \"카테고리\"\n    },\n    \"web_server\": {\n      \"title\": \"다운로드 - 웹 서버\",\n      \"loading\": \"로딩 중\",\n      \"get_ip_failed\": \"IP 가져오기 실패\",\n      \"getting_ip\": \"IP 가져오는 중\",\n      \"port\": \"포트: 8080\",\n      \"usage_instruction\": \"브라우저에 \\\"http://device_ip:8080/\\\"를 입력하여 다운로드한 만화에 액세스\",\n      \"leave_notice\": \"이 페이지를 나가면 서버가 닫힙니다\"\n    }\n  },\n  \"components\": {\n    \"comic_info_card\": {\n      \"categories\": \"카테고리\",\n      \"finished\": \"완결\",\n      \"viewed\": \"조회함\"\n    },\n    \"comic_list\": {\n      \"shadow\": \"그림자 만화\"\n    },\n    \"common\": {\n      \"display_mode\": \"표시 모드\",\n      \"shadow_mode\": \"그림자 모드\",\n      \"shadow_list\": \"그림자 목록\",\n      \"batch_download\": \"일괄 다운로드\"\n    },\n    \"image_reader\": {\n      \"already_at_the_end\": \"이미 끝에 도달했습니다\",\n      \"click_to_next_chapter\": \"다음 챕터로 이동하려면 클릭\",\n      \"reload_page\": \"페이지 새로고침\",\n      \"next_chapter\": \"다음 챕터\",\n      \"end_reading\": \"읽기 종료\",\n      \"reload_image\": \"이미지 새로고침\",\n      \"save_image_in_this_page\": \"이 페이지의 이미지 저장\",\n      \"image_load_failed\": \"이미지 로드 실패\"\n    }\n  }\n}\n"
  },
  {
    "path": "lib/assets/translations/zh-CN.json",
    "content": "{\n  \"language\": {\n    \"title\": \"语言\",\n    \"name\": \"简体中文 - 中国大陆\"\n  },\n  \"app\": {\n    \"categories\": \"分类\",\n    \"my\": \"我的\",\n    \"copied_to_clipboard\": \"已复制到剪切板\",\n    \"not_supported_platform\": \"暂不支持该平台\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"确定\",\n    \"save_cancel\": \"保存取消\",\n    \"save_success\": \"保存成功\",\n    \"save_failed\": \"保存失败\",\n    \"pro\": \"发电\",\n    \"pro_required\": \"请先发电再使用\",\n    \"choose_folder\": \"选择一个文件夹, 将文件保存到这里\",\n    \"permission_denied\": \"申请权限被拒绝\",\n    \"loading\": \"加载中\",\n    \"error\": \"错误\",\n    \"pat\": {\n      \"success\": \"您的赞助登录成功, 请返回\",\n      \"title\": \"更换PAT账户\"\n    },\n    \"previous_page\": \"上一页\",\n    \"next_page\": \"下一页\",\n    \"page\": \"页\",\n    \"please_enter_page_number\": \"请输入页数：\",\n    \"select_all\": \"全选\",\n    \"load_failed\": \"加载失败\",\n    \"all\": \"全部\",\n    \"delete\": \"删除\",\n    \"save_image\": \"保存图片\",\n    \"preview_image\": \"预览图片\",\n    \"please_select\": \"请选择\",\n    \"refresh\": \"刷新\",\n    \"initializing\": \"初始化\",\n    \"like_failed\": \"点赞失败\",\n    \"network_error\": \"连接不上啦, 请检查网络\",\n    \"no_permission\": \"没有权限或路径不可用\",\n    \"check_device_time\": \"请检查设备时间\",\n    \"resource_not_available\": \"资源未审核或不可用\",\n    \"something_went_wrong\": \"啊哦, 被玩坏了\",\n    \"click_refresh\": \"点击刷新\",\n    \"pull_down_refresh\": \"下拉刷新\",\n    \"continue_reading\": \"继续阅读\",\n    \"start_reading\": \"开始阅读\",\n    \"image_crop\": \"图片裁剪\",\n    \"download\": \"下载\",\n    \"download_failed\": \"下载失败\",\n    \"download_finished\": \"下载完成\",\n    \"downloading\": \"下载中\",\n    \"queue\": \"队列中\",\n    \"deleting\": \"删除中\",\n    \"please_select_comic\": \"请选择漫画\",\n    \"please_choose\": \"请选择\",\n    \"last_viewed\": \"上次观看到\",\n    \"auto_punch\": \"自动打卡\",\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"confirm_download\": \"确认下载\",\n    \"copy\": \"复制\"\n  },\n  \"net\": {\n    \"no_address\": \"不分流\",\n    \"address\": \"分流\",\n    \"address_sync\": \"分流同步\",\n    \"address_sync_from_server\": \"从服务器获取最新的分流地址\",\n    \"address_sync_reset\": \"重制分流为默认值\",\n    \"address_sync_success\": \"分流同步成功\",\n    \"address_sync_failed\": \"分流同步失败\",\n    \"address_sync_reset_success\": \"分流重制成功\",\n    \"address_sync_reset_failed\": \"分流重制失败\",\n    \"choose_address\": \"选择分流\",\n    \"image_address\": \"图片分流\",\n    \"use_api_load_image\": \"用API加载图片\",\n    \"ping_testing\": \"测速中\",\n    \"ping_failed\": \"失败\"\n  },\n  \"categories\": {\n    \"all\": \"全分类\",\n    \"recommend\": \"推荐\",\n    \"rankings\": \"排行榜\",\n    \"random\": \"随机本子\",\n    \"game\": \"游戏专区\"\n  },\n  \"settings\": {\n    \"settings\": \"设置\",\n    \"interface\": \"界面\",\n    \"network\": \"网络\",\n    \"seal\": \"封印\",\n    \"interaction\": \"交互\",\n    \"reading\": \"阅读\",\n    \"download\": \"下载\",\n    \"auto_download_on_favorite\": \"收藏时自动下载\",\n    \"disable_auto_download_on_mobile\": \"移动网络下收藏时不自动下载\",\n    \"auto_delete_download_on_unfavorite\": \"取消收藏时自动删除下载\",\n    \"web_server\": \"启动Web服务器\",\n    \"web_server_subtitle\": \"让局域网内的设备通过浏览器看下载的漫画\",\n    \"sync\": \"同步\",\n    \"history_sync\": \"历史记录同步\",\n    \"local_favorite_sync_title\": \"本地收藏夹同步\",\n    \"use_local_favorite\": \"使用本地收藏夹\",\n    \"use_local_favorite_desc\": \"在本地管理收藏，支持文件夹分类\",\n    \"account\": \"账户\",\n    \"modify_password\": \"修改密码\",\n    \"ebook\": \"电纸书\",\n    \"system\": \"系统\",\n    \"clear_cache\": \"清除缓存\",\n    \"migrate\": \"文件迁移\",\n    \"migrate_subtitle\": \"更换您的数据文件夹到内存卡\",\n    \"migrate_confirm\": \"此功能菜单保存后, 需要重启程序, 您确认吗\",\n    \"app_orientation\": {\n      \"title\": \"APP方向\",\n      \"choose\": \"请选择APP方向\",\n      \"normal\": \"正常\",\n      \"landscape\": \"横屏\",\n      \"portrait\": \"竖屏\"\n    },\n    \"will_pop_notice\": \"在首页连续按两下返回键才能退出APP\",\n    \"android_secure_flag\": \"禁止截图/禁止显示在任务视图\",\n    \"android_display_mode\": {\n      \"title\": \"屏幕刷新率(安卓)\",\n      \"dialog_title\": \"安卓屏幕刷新率 \\n(省电模式下不会高刷)\"\n    },\n    \"authentication\": \"进入APP时验证身份(如果系统已经录入密码或指纹)\",\n    \"set_password\": \"设置应用程序密码\",\n    \"auto_clean\": {\n      \"title\": \"自动清理缓存\",\n      \"one_month_ago\": \"一个月前\",\n      \"one_week_ago\": \"一周前\",\n      \"one_day_ago\": \"一天前\",\n      \"no_auto_clean\": \"不自动清理\"\n    },\n    \"categories_column_count\": {\n      \"title\": \"首页分类展示列数\",\n      \"choose\": \"选择首页分类展示列数\",\n      \"auto\": \"自动\"\n    },\n    \"categories_sort\": {\n      \"title\": \"首页分类排序\"\n    },\n    \"chooser_root\": {\n      \"title\": \"文件夹选择器根路径\",\n      \"hint\": \"请输入文件夹选择器根路径\",\n      \"desc\": \"导出时选择目录的默认路径, 同时也是根路径, 不能正常导出时也可以尝试设置此选项。\"\n    },\n    \"content_failed_reload_action\": {\n      \"title\": \"页面加载失败刷新方式\",\n      \"choose\": \"选择页面加载失败刷新方式\",\n      \"pull_down\": \"下拉刷新\",\n      \"touch_loader\": \"点击屏幕刷新\"\n    },\n    \"copy_full_name\": {\n      \"title\": \"复制漫画名称时使用模版\"\n    },\n    \"copy_full_name_template\": {\n      \"title\": \"复制漫画名称模版\",\n      \"hint\": \"模版内容\"\n    },\n    \"copy_skip_confirm\": {\n      \"title\": \"长按复制不需要确认\"\n    },\n    \"download_and_export_path\": {\n      \"title\": \"下载的同时导出到文件系统\",\n      \"confirm\": \"下载的同时导出到文件系统\",\n      \"desc\": \"您即将选择一个目录, 如果文件系统可写, 下载的同时会为您自动导出一份\"\n    },\n    \"download_cache_path\": {\n      \"title\": \"使用其他程序的缓存下载加速\",\n      \"confirm\": \"使用其他程序的缓存下载加速\",\n      \"desc\": \"您即将选择一个目录, 这个目录拷贝自以下目录才能使用。下载时将会作为缓存文件夹优先读取。 \",\n      \"cancel_desc\": \"您确定取消使用其他软件的下载内容加速的功能吗? 取消之后您可以再次点击设置\",\n      \"import_view_log_from_off\": {\n        \"title\": \"导入其他程序的历史记录\",\n        \"desc\": \"您即将选择一个文件, 这个文件拷贝自以下路径才能使用。\",\n        \"choose_file_dialog_title\": \"选择要导入的文件\"\n      }\n    },\n    \"download_thread_count\": {\n      \"title\": \"下载线程数\",\n      \"choose\": \"选择下载线程数\"\n    },\n    \"ebook_scrolling\": {\n      \"title\": \"电子书模式滚动UI\"\n    },\n    \"ebook_scrolling_range\": {\n      \"title\": \"电子书模式滚动UI\",\n      \"desc\": \"滚动幅度\",\n      \"screen_height\": \"屏幕高度\"\n    },\n    \"ebook_scrolling_trigger\": {\n      \"title\": \"电子书模式滚动UI\",\n      \"desc\": \"触发距离\",\n      \"cm\": \"厘米\"\n    },\n    \"export_path\": {\n      \"ios_desc\": \"随后可在文件管理中找到导出的内容\",\n      \"ios_desc2\": \"您正在使用iOS设备:\\n导出到文件的内容请打开系统自带文件管理进行浏览\",\n      \"export_path_desc\": \"导出路径 (点击可修改)\",\n      \"android_desc\": \"您正在使用安卓设备:\\n如果不能成功导出并且提示权限不足, 可以尝试在Download或Document下建立子目录进行导出\"\n    },\n    \"export_rename\": {\n      \"title\": \"导出时进行重命名\"\n    },\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"full_screen_action\": {\n      \"title\": \"操控方式\",\n      \"choose\": \"选择操控方式\",\n      \"touch_once\": \"点击屏幕一次全屏\",\n      \"controller\": \"使用控制器全屏\",\n      \"touch_double\": \"双击屏幕全屏\",\n      \"touch_double_once_next\": \"双击屏幕全屏 + 单击屏幕下一页\",\n      \"three_area\": \"将屏幕划分成三个区域 (上一页, 下一页, 全屏)\"\n    },\n    \"full_screen_ui\": {\n      \"title\": \"全屏UI\",\n      \"choose\": \"选择全屏UI\",\n      \"no\": \"不使用\",\n      \"hidden_bottom\": \"去除虚拟控制器\",\n      \"all\": \"全屏\"\n    },\n    \"auto_full_screen\": {\n      \"title\": \"进入阅读器自动全屏\"\n    },\n    \"auto_full_screen_on_forward\": {\n      \"title\": \"前进时自动全屏\"\n    },\n    \"ignore_info_history\": {\n      \"title\": \"详情页不计入历史记录\"\n    },\n    \"icon_loading\": {\n      \"title\": \"尽量减少UI动画\"\n    },\n    \"ignore_upgrade_confirm\": {\n      \"title\": \"关闭升级弹窗\"\n    },\n    \"hidden_fd_icon\": {\n      \"title\": \"隐藏个人空间的发电图标\"\n    },\n    \"hidden_search_persion\": {\n      \"title\": \"隐藏按作者搜索功能\"\n    },\n    \"hidden_viewed\": {\n      \"title\": \"隐藏阅读过的漫画\"\n    },\n    \"hidden_sub_icon\": {\n      \"title\": \"隐藏订阅功能\"\n    },\n    \"hide_online_favorite\": {\n      \"title\": \"隐藏在线收藏\",\n      \"desc\": \"隐藏在线收藏入口与收藏按钮\"\n    },\n    \"hidden_words\": {\n      \"title\": \"根据关键词隐藏\",\n      \"clear_all\": \"确认清空\",\n      \"clear_all_desc\": \"确定要清空所有关键词吗？\",\n      \"input_hint\": \"输入要隐藏的关键词\",\n      \"no_words\": \"暂无关键词\"\n    },\n    \"image_address\": {\n      \"title\": \"图片分流\",\n      \"pinging\": \"测速中\",\n      \"failed\": \"失败\"\n    },\n    \"image_filter\": {\n      \"title\": \"阅读器图片滤镜\",\n      \"normal\": \"正常\",\n      \"gray\": \"灰度\",\n      \"brown\": \"棕褐色\",\n      \"choose\": \"选择阅读器图片滤镜\"\n    },\n    \"import_notice\": {\n      \"android_desc\": \"您正在使用安卓设备:\\n如果不能导入导出并且提示权限不足, 可以尝试在Download或Document下建立子目录进行导入\"\n    },\n    \"keyboard_controller\": {\n      \"title\": \"阅读器键盘翻页(仅PC)\"\n    },\n    \"list_layout\": {\n      \"choose\": \"请选择布局\",\n      \"info_card\": \"详情\",\n      \"only_image\": \"封面\",\n      \"cover_and_title\": \"封面+标题\"\n    },\n    \"local_history_sync\": {\n      \"sync_to_local\": \"同步历史记录到本地\",\n      \"not_set\": \"未设置\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失败\",\n      \"auto_sync\": \"自动同步历史记录到本地\",\n      \"auto_sync_desc\": \"开启后每次打开应用会自动备份历史记录\",\n      \"choose_dir\": \"选择目录\",\n      \"clear_path\": \"清除路径\",\n      \"clear_path_desc\": \"确定要清除路径吗？\"\n    },\n    \"local_favorite_sync\": {\n      \"auto_sync\": \"自动同步本地收藏夹\",\n      \"auto_sync_desc\": \"使用WebDAV自动同步本地收藏夹\",\n      \"manual_sync\": \"手动同步本地收藏夹\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失败\"\n    },\n    \"no_animation\": {\n      \"title\": \"取消翻页动画（点按屏幕、音量键、键盘）\"\n    },\n    \"pager_action\": {\n      \"title\": \"列表页加载方式\",\n      \"choose\": \"选择列表页加载方式\",\n      \"controller\": \"使用按钮\",\n      \"stream\": \"瀑布流\"\n    },\n    \"proxy\": {\n      \"title\": \"代理服务器\",\n      \"hint\": \"请输入代理服务器\",\n      \"desc\": \" ( 例如 socks5://127.0.0.1:1080/ ) \",\n      \"no_proxy\": \"未设置\"\n    },\n    \"quality\": {\n      \"title\": \"浏览时的图片质量\",\n      \"choose\": \"请选择图片质量\",\n      \"original\": \"原图\",\n      \"low\": \"低\",\n      \"medium\": \"中\",\n      \"high\": \"高\"\n    },\n    \"reader_background_color\": {\n      \"title\": \"阅读器背景色\",\n      \"choose\": \"选择阅读器背景色\",\n      \"black\": \"黑色\",\n      \"gray\": \"灰度\",\n      \"white\": \"白色\"\n    },\n    \"reader_direction\": {\n      \"title\": \"阅读器方向\",\n      \"choose\": \"选择翻页方向\",\n      \"top_to_bottom\": \"从上到下\",\n      \"left_to_right\": \"从左到右\",\n      \"right_to_left\": \"从右到左\"\n    },\n    \"reader_scroll_by_screen_percentage\": {\n      \"title\": \"按距离翻页长度\",\n      \"screen_size\": \"屏幕尺寸\"\n    },\n    \"web_toon_scroll_mode\": {\n      \"title\": \"WebToon 翻页模式\",\n      \"choose\": \"选择 WebToon 翻页模式\",\n      \"image\": \"图片\",\n      \"screen\": \"距离\"\n    },\n    \"reader_zoom\": {\n      \"out_title\": \"缩小倍数（最小缩放）\",\n      \"in_title\": \"放大倍数（最大缩放）\",\n      \"double_tap_title\": \"双击缩放倍数\"\n    },\n    \"drag_region_lock\": {\n      \"title\": \"锁定拖动边界\"\n    },\n    \"gesture_speed\": {\n      \"title\": \"手势速度倍率\"\n    },\n    \"reader_slider_position\": {\n      \"title\": \"滚动条的位置\",\n      \"choose\": \"选择滑动条位置\",\n      \"bottom\": \"下方\",\n      \"right\": \"右侧\",\n      \"left\": \"左侧\"\n    },\n    \"reader_two_page_direction\": {\n      \"title\": \"双页阅读器内容排列\",\n      \"choose\": \"选择双页阅读器内容排列\",\n      \"close_to\": \"靠近\",\n      \"pull_away\": \"远离\",\n      \"each_centered\": \"各自居中\"\n    },\n    \"reader_type\": {\n      \"title\": \"阅读器模式\",\n      \"choose\": \"选择阅读器模式\",\n      \"web_toon\": \"WebToon (默认)\",\n      \"web_toon_zoom\": \"WebToon (双击放大)\",\n      \"gallery\": \"相册\",\n      \"web_toon_free_zoom\": \"WebToon (ListView双击放大)\\n(此模式进度条无效)\",\n      \"two_page_gallery\": \"双页模式\\n(实验)\",\n      \"left_to_right\": \"从左到右\",\n      \"right_to_left\": \"从右到左\",\n      \"two_page_direction\": \"双页方向\",\n      \"two_page_direction_choose\": \"选择双页方向\"\n    },\n    \"shadow_categories\": {\n      \"title\": \"封印\",\n      \"search_hint\": \"搜索\"\n    },\n    \"shadow_categories_mode\": {\n      \"title\": \"封印模式\",\n      \"black_list\": \"黑名单\",\n      \"white_list\": \"白名单\"\n    },\n    \"startup_pic\": {\n      \"title\": \"设置启动图片\",\n      \"subtitle\": \"设置应用启动时显示的图片\",\n      \"clear_title\": \"清除启动图片\",\n      \"clear_subtitle\": \"清除应用启动时显示的图片\",\n      \"clear_success\": \"启动图片已清除\",\n      \"update_success\": \"启动图片已更新\"\n    },\n    \"show_comment_at_download\": {\n      \"title\": \"在下载显示评论区\"\n    },\n    \"font\": {\n      \"title\": \"字体\",\n      \"hint\": \"请输入字体\",\n      \"input_hint\": \"请输入字体的名称且用英文逗号分隔, 例如 \\\"宋体,黑体\\\", 如果您保存后没有发生变化, 说明字体无法使用或名称错误, 可以去参考C:\\\\Windows\\\\Fonts寻找您的字体。若您使用的是flutter2引擎的版本，只有第一个字体生效。\",\n      \"choose_hint\": \"需要您选择多个字体，直至您点击背景区域\"\n    },\n    \"theme\": {\n      \"origin\": \"原生\",\n      \"pink\": \"粉色\",\n      \"black\": \"酷黑\",\n      \"dark\": \"暗黑\",\n      \"dusty_blue\": \"灰蓝\",\n      \"dark_black\": \"纯黑\",\n      \"choose_theme\": \"选择主题\",\n      \"book\": \"书本\",\n      \"enable_status_bar_color\": \"启用状态栏颜色\",\n      \"enable_status_restart_hint\": \"关闭时需要重新启动应用程序刷新状态栏颜色\"\n    },\n    \"three_keep_right\": {\n      \"title\": \"三区域模式翻页始终为右侧下一页\"\n    },\n    \"time_zone\": {\n      \"title\": \"时区\"\n    },\n    \"timeout_lock\": {\n      \"title\": \"自动锁定\",\n      \"notice\": \"注意：自动锁定在桌面端仅支持最小化后超时，手机端支持后台以及锁屏后超时。如果没有设置密码，自动锁定无效。安卓以及桌面端只会锁定桌面，不会锁定下载，iOS未测试，需要手动开启后台活动。\",\n      \"1_hour\": \"一小时\",\n      \"10_minutes\": \"十分钟\",\n      \"3_minutes\": \"三分钟\",\n      \"1_minute\": \"一分钟\",\n      \"10_seconds\": \"十秒\",\n      \"1_second\": \"一秒\",\n      \"no_lock\": \"不锁定\"\n    },\n    \"using_right_click_pop\": {\n      \"title\": \"鼠标右键返回上一页\"\n    },\n    \"volume_controller\": {\n      \"title\": \"阅读器音量键翻页\"\n    },\n    \"volume_next_chapter\": {\n      \"title\": \"双击 音量/键盘/控制器 下一章节\"\n    },\n    \"webdav\": {\n      \"title\": \"WebDav\",\n      \"not_set\": \"未设置\",\n      \"path\": \"WebDav 路径\",\n      \"path_hint\": \"请输入WebDav 路径\",\n      \"username\": \"WebDav 用户名\",\n      \"username_hint\": \"请输入WebDav 用户名\",\n      \"password\": \"WebDav 密码\",\n      \"password_hint\": \"请输入WebDav 密码\",\n      \"auto_sync_history_to_webdav\": \"开启时自动同步浏览记录到WebDav\",\n      \"sync_history_to_webdav\": \"立即同步浏览记录到WebDAV\",\n      \"upload_history_to_webdav\": \"覆盖WebDAV中的浏览记录\",\n      \"upload_history_to_webdav_desc\": \"如有多台设备，请注意自动同步功能\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失败\"\n    }    \n  },\n  \"local_favorite\": {\n    \"title\": \"本地收藏夹\",\n    \"all_folders\": \"全部\",\n    \"new_folder\": \"新建文件夹\",\n    \"select_mode\": \"多选\",\n    \"cancel_select_mode\": \"退出多选\",\n    \"select_all\": \"全选\",\n    \"delete_folder\": \"删除文件夹\",\n    \"move_to_folder\": \"移动到文件夹\",\n    \"remove_selected\": \"移除选中收藏\",\n    \"remove_selected_confirm\": \"确认从本地收藏夹中移除选中的漫画？\",\n    \"remove_selected_success\": \"已移除\",\n    \"remove_selected_failed\": \"移除失败\",\n    \"select_comics\": \"请先选择漫画\",\n    \"folder_limit_reached\": \"免费版最多创建3个文件夹，升级Pro解锁无限制\",\n    \"batch_download\": \"批量下载\",\n    \"select_folder\": \"选择文件夹\",\n    \"folder_name\": \"文件夹名称\",\n    \"delete_confirm\": \"确认删除文件夹？\",\n    \"empty_folder\": \"暂无收藏\",\n    \"no_folders\": \"还没有文件夹，请先创建\",\n    \"remove_confirm_title\": \"取消收藏\",\n    \"remove_confirm_content\": \"确定要从本地收藏夹中移除这部漫画吗？\",\n    \"remove_failed\": \"取消收藏失败\",\n    \"load_failed\": \"加载失败\",\n    \"add_success\": \"已添加到本地收藏夹\",\n    \"add_failed\": \"添加失败\",\n    \"create_folder_failed\": \"创建文件夹失败\",\n    \"create_success\": \"创建成功\",\n    \"delete_success\": \"删除成功\",\n    \"delete_failed\": \"删除失败\",\n    \"move_success\": \"移动成功\",\n    \"move_failed\": \"移动失败\",\n    \"select_comics_to_download\": \"请选择要下载的漫画\",\n    \"download_started\": \"开始下载\",\n    \"download_failed\": \"下载失败\"\n  },\n  \"screen\": {\n    \"about\": {\n      \"title\": \"关于\",\n      \"version\": \"软件版本\",\n      \"check_update\": \"检查更新\",\n      \"tips\": \"提示 : \\n1. 详情页的作者/上传者/分类/标签都可以点击\\n2. 详情页的作者/上传者/标题长按可以复制\\n3. 使用分页而不是瀑布流点击页码可以快速翻页\\n4. 下载指的是缓存到本地, 需要导出才可以分享\\n5. 下载长按可以删除\",\n      \"download_new_version\": \"请从获取渠道下载新版\",\n      \"no_new_version\": \"未检测到新版本\",\n      \"download_release_version\": \"下载RELEASE版\",\n      \"update_content\": \"更新内容\",\n      \"go_to_release_repository\": \"去RELEASE仓库\"\n    },\n    \"account\": {\n      \"title\": \"账户\",\n      \"username\": \"账号\",\n      \"username_hint\": \"请输入账号\",\n      \"password\": \"密码\",\n      \"password_hint\": \"请输入密码\",\n      \"no_account_register\": \"没有账号,我要注册\",\n      \"password_reset\": \"密码找回\",\n      \"check_username_password_or_network\": \"请检查账号密码或网络环境\",\n      \"check_device_time\": \"请检查设备时间\",\n      \"username_or_password_error\": \"账号或密码错误\",\n      \"login_failed\": \"登录失败\",\n      \"not_set\": \"未设置\"\n    },\n    \"app\": {\n      \"will_pop_notice\": \"再次返回将会退出应用程序\"\n    },\n    \"categories\": {\n      \"search_hint\": \"搜索\"\n    },\n    \"clean\": {\n      \"title\": \"清理\",\n      \"cleaning\": \"清理中\",\n      \"clean_network_cache\": \"清理网络缓存\",\n      \"clean_image_cache\": \"清理图片缓存\",\n      \"clean_all_cache\": \"清理全部缓存\",\n      \"clean_success\": \"清理成功\",\n      \"clean_failed\": \"清理失败\"\n    },\n    \"close_app\": {\n      \"title\": \"提示\",\n      \"close_app\": \"请关闭应用重新打开\"\n    },\n    \"comic_collections\": {\n      \"no_resource\": \"这里没有资源呀\"\n    },\n    \"comic_info\": {\n      \"chapter\": \"章节\",\n      \"comment\": \"评论\",\n      \"recommend\": \"推荐\"\n    },\n    \"comics\": {\n      \"search_hint\": \"搜索分类\",\n      \"choose_category\": \"请选择分类\"\n    },\n    \"comic_subscribes\": {\n      \"update_reminder\": \"更新提醒\",\n      \"check_update\": \"检查更新\",\n      \"cancel_all_update_reminder\": \"取消所有更新提醒\"\n    },\n    \"comment\": {\n      \"title\": \"评论\",\n      \"hint_text\": \"请输入评论内容\",\n      \"success\": \"评论成功\",\n      \"i_have_something_to_say\": \"我有话要讲\",\n      \"please_enter_comment\": \"请输入评论内容\"\n    },\n    \"desktop_authentication\": {\n      \"current_password\": \"当前密码\",\n      \"password_error\": \"密码错误\",\n      \"password_initialization\": \"密码初始化\",\n      \"password\": \"密码\",\n      \"re_enter_password\": \"再次输入密码\",\n      \"password_mismatch\": \"两次输入的密码不一致\",\n      \"set_password\": \"设置密码\"\n    },\n    \"download_confirm\": {\n      \"please_select_ep\": \"请选择下载的EP\",\n      \"already_added_to_download_list\": \"已经加入下载列表\"\n    },\n    \"download_export_group\": {\n      \"title\": \"批量导出\",\n      \"please_select_content\": \"请选择导出的内容\",\n      \"exporting\": \"正在导出\",\n      \"export_failed\": \"导出失败\",\n      \"export_success\": \"导出成功\",\n      \"export_to_pkz\": \"导出成一个PKZ\\n(加密模式,防止网盘检测,能用pikapika打开观看)\",\n      \"export_to_pki\": \"每部漫画都打包一个PKI\\n(加密模式,防止网盘检测,能用pikapika导入)\",\n      \"export_to_zip\": \"每部漫画都打包一个ZIP\\n(不加密模式,能用pikapika导入或网页浏览器观看)\",\n      \"export_to_jpeg_zip\": \"每部漫画都打包一个ZIP+JPEG\\n(能直接使用其他阅读器看,不可再导入)\",\n      \"export_to_jpeg_folder\": \"每部漫画都导出成文件夹+JPEG\",\n      \"export_to_pdf\": \"每部漫画都导出成PDF\",\n      \"export_to_epub\": \"每部漫画都导出成EPUB\",\n      \"export_to_pdf_folder\": \"每部漫画都导出到文件夹, 每个章节一个PDF\",\n      \"export_to_cbz\": \"每部漫画都导出成cbz\",\n      \"after_power_use\": \"发电后使用\",\n      \"input_save_name\": \"请输入保存后的名称\",\n      \"export_confirm\": \"导出确认\",\n      \"export_to_pkz_title\": \"将导出您所选的漫画为一个PKZ\",\n      \"export_to_pki_title\": \"将您所选的漫画分别导出成单独的PKI\",\n      \"please_power_up\": \"请先发电鸭\",\n      \"export_to_zip_title\": \"将您所选的漫画分别导出成ZIP\",\n      \"export_to_jpeg_zip_title\": \"将您所选的漫画分别导出成ZIP+JPEG\",\n      \"export_to_jpeg_zip_title_not_down_over\": \"将您所选的漫画分别导出成ZIP+JPEG\\n(即便没有下载完成也可以使用)\",\n      \"export_to_jpeg_folder_title\": \"将您所选的漫画分别导出成文件夹+JPEG\",\n      \"export_to_pdf_title\": \"将您所选的漫画分别导出成PDF\",\n      \"export_to_epub_title\": \"将您所选的漫画分别导出成EPUB\",\n      \"export_to_pdf_folder_title\": \"将您所选的漫画分别导出到文件夹, 每个章节一个PDF\",\n      \"export_to_cbz_title\": \"将您所选的漫画分别导出成cbz\",\n      \"exporting_please_wait\": \"导出中, 请稍后\"\n    },\n    \"download_export_to_file\": {\n      \"title\": \"导出\",\n      \"transfer_to_other_device\": \"传输到其他设备\",\n      \"input_save_name\": \"请输入保存后的名称\",\n      \"export_confirm\": \"导出确认\",\n      \"export_to_pkz_title\": \"将您所选的漫画导出PKZ\",\n      \"export_to_pkz_desc\": \"导出到xxx.pkz\\n(可直接打开观看的格式,不支持导入)\\n(可以躲避网盘或者聊天软件的扫描)\",\n      \"export_to_pki_title\": \"将您所选的漫画导出PKI\",\n      \"export_to_pki_desc\": \"导出到xxx.pki\\n(只支持导入, 不支持直接阅读)\\n(可以躲避网盘或者聊天软件的扫描)\\n(后期版本可能支持直接阅读)\",\n      \"export_to_zip_title\": \"将您所选的漫画导出HTML+ZIP\",\n      \"export_to_zip_desc\": \"导出到xxx.zip\\n(可从其他设备导入 / 解压后可阅读)\",\n      \"export_to_jpeg_zip_title\": \"将您所选的漫画导出HTML+JPEG\",\n      \"export_to_jpeg_zip_desc\": \"导出到xxx.jpeg\\n(可直接在相册中打开观看)\",\n      \"export_to_pdf_title\": \"将您所选的漫画导出PDF\",\n      \"export_to_pdf_desc\": \"导出到xxx.pdf\\n(即使没有下载成功也可以使用、未成功下载的图片将会跳过)\\n(可直接在相册中打开观看)\",\n      \"export_to_pdf_folder_title\": \"将您所选的漫画导出到文件夹, 每个章节一个PDF\",\n      \"export_to_pdf_folder_desc\": \"导出到xxx.pdf\\n(即使没有下载成功也可以使用、未成功下载的图片将会跳过)\\n(可直接在相册中打开观看)\",\n      \"export_to_epub_title\": \"将您所选的漫画导出EPUB\",\n      \"export_to_epub_desc\": \"导出到xxx.epub\\n(可直接在阅读器中打开观看)\",\n      \"export_to_jpeg_folder_title\": \"将您所选的漫画导出JPEGS.zip\",\n      \"export_to_jpeg_folder_desc\": \"导出阅读器用JPGS.zip\\n(不可再导入)\",\n      \"export_to_cbz_title\": \"将您所选的漫画导出cbk.zip\",\n      \"export_to_cbz_desc\": \"导出到xxx.cbz, 阅读器可以直接使用(不可再导入)\"\n    },\n    \"download_export_to_socket\": {\n      \"title\": \"网络导出\",\n      \"loading\": \"加载中\",\n      \"tips\": \"传输成功之前请不要退出页面, 一次只能导出到一个设备, 两台设备需要在同一网段或无限局域网中, 请另外一台设备输入 IP:端口 , 有一个IP时请选择无限局域网的IP, 通常是192.168开头\",\n      \"get_ip_failed\": \"获取IP失败\",\n      \"getting_ip\": \"正在获取IP\",\n      \"port\": \"端口号\"\n    },\n    \"download_import\": {\n      \"title\": \"导入\",\n      \"open_file\": \"打开文件\",\n      \"select_file\": \"选择要导入的文件\",\n      \"import_success\": \"导入成功\",\n      \"import_failed\": \"导入失败\",\n      \"select_file_desc\": \"选择zip文件进行导入\\n选择pki文件进行导入\\n选择pkz文件进行阅读\",\n      \"input_address\": \"请输入导出设备提供的地址\\n例如 \\\"192.168.1.2:50000\\\"\",\n      \"import_from_other_device\": \"从其他设备导入\",\n      \"select_folder_desc\": \"选择文件夹\\n(导入里面所有的zip/pki)\\n(发电后使用)\"\n    },\n    \"download_info\": {\n      \"loading\": \"加载中\",\n      \"chapter\": \"章节\",\n      \"comment\": \"评论\",\n      \"recommend\": \"推荐\"\n    },\n    \"download_list\": {\n      \"search_download\": \"搜索下载\",\n      \"multi_select_operation\": \"多选操作\",\n      \"download_list\": \"下载列表\",\n      \"search\": \"搜索\",\n      \"select_folder\": \"选择文件夹\",\n      \"download_already_in_delete_queue\": \"该下载已经在删除队列中\",\n      \"import\": \"导入\",\n      \"export\": \"导出\",\n      \"file\": \"文件\",\n      \"download_task\": \"下载任务\",\n      \"pause_download\": \"暂停下载吗?\",\n      \"start_download\": \"启动下载吗?\",\n      \"resume_failed\": \"恢复失败任务\",\n      \"resume_failed_desc\": \"所有失败的下载已经恢复\",\n      \"downloading\": \"下载中\",\n      \"paused\": \"暂停中\",\n      \"move_download\": \"移动下载\",\n      \"select_download_to_move\": \"请选择要移动下载\",\n      \"select_download_to_delete\": \"请选择要删除下载\",\n      \"input_name\": \"==> 输入名称 <==\",\n      \"empty_folder_will_be_deleted\": \"（空文件夹将会自动删除，下次需要手动输入）\",\n      \"folder_name\": \"文件夹名称\",\n      \"please_input_folder_name\": \"请输入文件夹名称\",\n      \"delete_download\": \"删除下载\",\n      \"delete_selected_download\": \"删除选中的下载吗?\",\n      \"multi_select\": \"多选\"\n    },\n    \"download_only_import\": {\n      \"importing\": \"正在导入\",\n      \"import_success\": \"导入成功\",\n      \"import_failed\": \"导入失败\",\n      \"click_import_file\": \"点击导入文件\",\n      \"importing_please_wait\": \"导入中, 请稍后\"\n    },\n    \"favourite_paper\": {\n      \"favourite\": \"收藏\"\n    },\n    \"forgot_password\": {\n      \"title\": \"找回密码\",\n      \"username\": \"账号\",\n      \"not_set\": \"未设置\",\n      \"confirm\": \"确认\",\n      \"please_enter_username\": \"请输入账号\",\n      \"question_1\": \"问题1\",\n      \"question_2\": \"问题2\",\n      \"question_3\": \"问题3\",\n      \"answer_1\": \"回答1\",\n      \"answer_2\": \"回答2\",\n      \"answer_3\": \"回答3\",\n      \"please_enter_answer_1\": \"请输入回答1\",\n      \"please_enter_answer_2\": \"请输入回答2\",\n      \"please_enter_answer_3\": \"请输入回答3\",\n      \"use_answer_1_recover\": \"使用回答1找回密码\",\n      \"use_answer_2_recover\": \"使用回答2找回密码\",\n      \"use_answer_3_recover\": \"使用回答3找回密码\",\n      \"please_enter_answer\": \"请输入答案\",\n      \"new_password_copied\": \"新密码正在复制到剪切板\",\n      \"answer_incorrect\": \"答案不正确\",\n      \"password\": \"密码\"\n    },\n    \"game_download\": {\n      \"title\": \"下载\",\n      \"download_links_obtained\": \"获取到下载链接, 您只需要选择其中一个\"\n    },\n    \"game_info\": {\n      \"download\": \"下载\",\n      \"details\": \"详情\",\n      \"comments\": \"评论\"\n    },\n    \"games\": {\n      \"title\": \"游戏\"\n    },\n    \"import_from_off\": {\n      \"title\": \"导入\",\n      \"import_success\": \"导入成功\",\n      \"import_failed\": \"导入失败\"\n    },\n    \"modify_password\": {\n      \"title\": \"修改密码\",\n      \"please_wait\": \"请稍后\",\n      \"old_password\": \"旧密码\",\n      \"new_password\": \"新密码\",\n      \"repeat_new_password\": \"重复输入新密码\",\n      \"not_filled\": \"未填写\",\n      \"please_enter_old_password\": \"请输入旧密码\",\n      \"please_enter_new_password\": \"请输入新密码\",\n      \"please_repeat_new_password\": \"请重复输入新密码\",\n      \"new_password_mismatch\": \"新密码不匹配\",\n      \"modify_success\": \"修改成功\",\n      \"failed\": \"失败\",\n      \"confirm\": \"确认\"\n    },\n    \"network_settings\": {\n      \"title\": \"网络设置\"\n    },\n    \"pkz_reader\": {\n      \"reading_downloaded_comic\": \"您阅读的是下载漫画\"\n    },\n    \"pro\": {\n      \"title\": \"发电中心\",\n      \"power_center\": \"发电中心\",\n      \"power_status\": \"发电状态\",\n      \"powered\": \"发电中\",\n      \"not_powered\": \"未发电\",\n      \"pat_membership\": \"PAT入会\",\n      \"pat_status\": \"PAT状态\",\n      \"pat_normal\": \"PAT正常\",\n      \"pat_bind_hint\": \"请点击这里绑定到当前账号发电\",\n      \"pat_rebind_hint\": \"请点换绑到当前账号发电\",\n      \"pat_not_detected\": \"未检测到入会, 请到下载页入会\",\n      \"i_have_powered\": \"我曾经发过电\",\n      \"i_just_powered\": \"我刚才发了电\",\n      \"enter_code\": \"输入代码\",\n      \"power_method\": \"发电方式\",\n      \"wind_power\": \"风力发电\",\n      \"hydro_power\": \"水力发电\",\n      \"solar_power\": \"光伏发电\",\n      \"nuclear_power\": \"核能发电\",\n      \"choose_power_method\": \"选择发电方式\",\n      \"sign_in_exchange\": \"签到/兑换\",\n      \"click_pat_to_change\": \"点击下面的PAT会籍进行变更\",\n      \"update_pat_status\": \"更新PAT发电状态\",\n      \"bind_to_account\": \"绑定到此账号\",\n      \"change_pat_key\": \"更换PAT密钥\",\n      \"clear_pat_info\": \"清除PAT信息\",\n      \"click_to_bind\": \"点击绑定\",\n      \"enter_auth_code\": \"请输入授权代码\",\n      \"please_wait\": \"请稍后\",\n      \"key_recorded\": \"密钥 : 已录入\",\n      \"pat_account\": \"PAT账号\",\n      \"bind_pika_account\": \"绑定PIKA账号\",\n      \"bind_account_time\": \"绑定账号时间\",\n      \"rebind_time\": \"可以换绑时间\",\n      \"power_features\": \"发电小功能: 多线程下载 / 批量导入导出下载\",\n      \"power_guide\": \"去\\\"关于\\\"界面找到维护地址可获得发电指引\\n\\n  \\\"我曾经发过电\\\"可同步相应发电状态\\n  \\\"我刚才发了电\\\"兑换神秘代码\\n  \\\"发电方式\\\"可以在网络不通时尝试更换\\n  \\\"PAT入会\\\"是独立的发电方式\"\n    },\n    \"rankings\": {\n      \"title\": \"排行榜\",\n      \"day\": \"天\",\n      \"week\": \"周\",\n      \"month\": \"月\",\n      \"knight\": \"骑\",\n      \"refresh\": \"刷新\",\n      \"comics_count\": \"本\"\n    },\n    \"random_comics\": {\n      \"title\": \"随机本子\"\n    },\n    \"register\": {\n      \"title\": \"注册\",\n      \"registering\": \"注册中\",\n      \"register_success\": \"注册成功\",\n      \"register_failed\": \"注册失败\",\n      \"account_exists\": \"账号已存在\",\n      \"name_exists\": \"昵称已存在\",\n      \"check_form\": \"请检查表单, 不允许留空\",\n      \"account\": \"账号\",\n      \"password\": \"密码\",\n      \"nickname\": \"昵称\",\n      \"gender\": \"性别\",\n      \"birthday\": \"生日\",\n      \"question_1\": \"问题1\",\n      \"answer_1\": \"回答1\",\n      \"question_2\": \"问题2\",\n      \"answer_2\": \"回答2\",\n      \"question_3\": \"问题3\",\n      \"answer_3\": \"回答3\",\n      \"not_set\": \"未设置\",\n      \"please_enter_account\": \"请输入账号\",\n      \"please_enter_password\": \"请输入密码\",\n      \"please_enter_nickname\": \"请输入昵称\",\n      \"please_enter_question_1\": \"请输入问题1\",\n      \"please_enter_answer_1\": \"请输入回答1\",\n      \"please_enter_question_2\": \"请输入问题2\",\n      \"please_enter_answer_2\": \"请输入回答2\",\n      \"please_enter_question_3\": \"请输入问题3\",\n      \"please_enter_answer_3\": \"请输入回答3\",\n      \"account_desc\": \"(小写字母+数字/登录使用)\",\n      \"password_desc\": \"(大小写字母+数字/8位或以上)\",\n      \"nickname_desc\": \"(可使用中文/2-50字)\",\n      \"choose_gender\": \"选择您的性别\",\n      \"futa\": \"扶她\",\n      \"male\": \"公\",\n      \"female\": \"母\",\n      \"register_success_desc\": \"您已经注册成功, 请返回登录\",\n      \"account_label\": \"账号\",\n      \"nickname_label\": \"昵称\"\n    },\n    \"search\": {\n      \"title\": \"搜索\",\n      \"search_hint\": \"搜索\",\n      \"choose_category\": \"请选择分类\"\n    },\n    \"search_author\": {\n      \"title\": \"按作者搜索\",\n      \"search_hint\": \"搜索 按作者 + \",\n      \"by_author\": \"按作者: \"\n    },\n    \"space\": {\n      \"title\": \"我的\",\n      \"logout\": \"退出登录\",\n      \"logout_confirm\": \"您确认要退出当前账号吗?\",\n      \"my_favourites\": \"我的收藏\",\n      \"view_history\": \"浏览记录\",\n      \"my_downloads\": \"我的下载\"\n    },\n    \"theme\": {\n      \"title\": \"主题设置\",\n      \"theme\": \"主题\",\n      \"dark_mode_different_theme\": \"深色模式下使用不同的主题\",\n      \"dark_mode_theme\": \"主题 (深色模式)\"\n    },\n    \"view_logs\": {\n      \"title\": \"浏览记录\",\n      \"clear_all\": \"您要清除所有浏览记录吗? \",\n      \"clear_all_desc\": \"将会同时删除浏览进度!\",\n      \"clear_one\": \"您要清除这条浏览记录吗? \",\n      \"clear_one_desc\": \"将会同时删除浏览进度!\",\n      \"clear_selected\": \"您要清除选中的浏览记录吗? \",\n      \"clear_selected_desc\": \"将会同时删除浏览进度!\",\n      \"categories\": \"分类\"\n    },\n    \"web_server\": {\n      \"title\": \"下载 - Web服务器\",\n      \"loading\": \"加载中\",\n      \"get_ip_failed\": \"获取IP失败\",\n      \"getting_ip\": \"正在获取IP\",\n      \"port\": \"端口号:8080\",\n      \"usage_instruction\": \"在浏览器中输入\\\"http://本设备ip:8080/\\\"访问下载的漫画\",\n      \"leave_notice\": \"离开页面后服务器将关闭\"\n    }\n  },\n  \"components\": {\n    \"comic_info_card\": {\n      \"categories\": \"分类\",\n      \"finished\": \"完结\",\n      \"viewed\": \"看过\"\n    },\n    \"comic_list\": {\n      \"shadow\": \"被封印的本子\"\n    },\n    \"common\": {\n      \"display_mode\": \"显示模式\",\n      \"shadow_mode\": \"封印模式\",\n      \"shadow_list\": \"封印列表\",\n      \"batch_download\": \"批量下载\"\n    },\n    \"image_reader\": {\n      \"already_at_the_end\": \"已经到头了\",\n      \"click_to_next_chapter\": \"再次点击跳转到下一章\",\n      \"reload_page\": \"重载页面\",\n      \"next_chapter\": \"下一章\",\n      \"end_reading\": \"结束阅读\",\n      \"reload_image\": \"重新加载图片\",\n      \"save_image_in_this_page\": \"保存本页的图片\",\n      \"image_load_failed\": \"图片加载失败\"\n    }\n  }\n}\n"
  },
  {
    "path": "lib/assets/translations/zh-TW.json",
    "content": "{\n  \"language\": {\n    \"title\": \"語言\",\n    \"name\": \"繁體中文 - 中國台灣\"\n  },\n  \"app\": {\n    \"categories\": \"分類\",\n    \"my\": \"我的\",\n    \"copied_to_clipboard\": \"已複製到剪貼簿\",\n    \"not_supported_platform\": \"不支援此平台\",\n    \"cancel\": \"取消\",\n    \"confirm\": \"確定\",\n    \"save_cancel\": \"保存取消\",\n    \"save_success\": \"保存成功\",\n    \"save_failed\": \"保存失败\",\n    \"pro\": \"發電\",\n    \"pro_required\": \"請先發電再使用\",\n    \"choose_folder\": \"選擇一個文件夾, 將文件保存到這裡\",\n    \"permission_denied\": \"申請權限被拒絕\",\n    \"loading\": \"加载中\",\n    \"error\": \"错误\",\n    \"pat\": {\n      \"success\": \"您的贊助登錄成功, 請返回\",\n      \"title\": \"更換PAT賬戶\"\n    },\n    \"previous_page\": \"上一頁\",\n    \"next_page\": \"下一頁\",\n    \"page\": \"頁\",\n    \"please_enter_page_number\": \"請輸入頁數：\",\n    \"select_all\": \"全選\",\n    \"load_failed\": \"加載失敗\",\n    \"all\": \"全部\",\n    \"delete\": \"刪除\",\n    \"save_image\": \"保存圖片\",\n    \"preview_image\": \"預覽圖片\",\n    \"please_select\": \"請選擇\",\n    \"refresh\": \"刷新\",\n    \"initializing\": \"初始化\",\n    \"like_failed\": \"點讚失敗\",\n    \"network_error\": \"連接不上啦, 請檢查網路\",\n    \"no_permission\": \"沒有權限或路徑不可用\",\n    \"check_device_time\": \"請檢查設備時間\",\n    \"resource_not_available\": \"資源未審核或不可用\",\n    \"something_went_wrong\": \"啊哦, 被玩壞了\",\n    \"click_refresh\": \"點擊刷新\",\n    \"pull_down_refresh\": \"下拉刷新\",\n    \"continue_reading\": \"繼續閱讀\",\n    \"start_reading\": \"開始閱讀\",\n    \"image_crop\": \"圖片裁剪\",\n    \"download\": \"下載\",\n    \"download_failed\": \"下載失敗\",\n    \"download_finished\": \"下載完成\",\n    \"downloading\": \"下載中\",\n    \"queue\": \"隊列中\",\n    \"deleting\": \"刪除中\",\n    \"please_select_comic\": \"請選擇漫畫\",\n    \"please_choose\": \"請選擇\",\n    \"last_viewed\": \"上次觀看到\",\n    \"auto_punch\": \"自動打卡\",\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"confirm_download\": \"確認下載\",\n    \"copy\": \"複製\"\n  },\n  \"net\": {\n    \"no_address\": \"不分流\",\n    \"address\": \"分流\",\n    \"address_sync\": \"分流同步\",\n    \"address_sync_from_server\": \"從伺服器獲取最新的分流地址\",\n    \"address_sync_reset\": \"重制分流為預設值\",\n    \"address_sync_success\": \"分流同步成功\",\n    \"address_sync_failed\": \"分流同步失敗\",\n    \"address_sync_reset_success\": \"分流重制成功\",\n    \"address_sync_reset_failed\": \"分流重制失敗\",\n    \"choose_address\": \"選擇分流\",\n    \"image_address\": \"圖片分流\",\n    \"use_api_load_image\": \"用API加載圖片\",\n    \"ping_testing\": \"測速中\",\n    \"ping_failed\": \"失敗\"\n  },\n  \"categories\": {\n    \"all\": \"全分類\",\n    \"recommend\": \"推薦\",\n    \"rankings\": \"排行榜\",\n    \"random\": \"隨機本子\",\n    \"game\": \"遊戲專區\"\n  },\n  \"settings\": {\n    \"settings\": \"設定\",\n    \"interface\": \"介面\",\n    \"network\": \"網路\",\n    \"seal\": \"封印\",\n    \"interaction\": \"交互\",\n    \"reading\": \"閱讀\",\n    \"download\": \"下載\",\n    \"auto_download_on_favorite\": \"收藏時自動下載\",\n    \"disable_auto_download_on_mobile\": \"行動網路下收藏時不自動下載\",\n    \"auto_delete_download_on_unfavorite\": \"取消收藏時自動刪除下載\",\n    \"web_server\": \"啟動Web伺服器\",\n    \"web_server_subtitle\": \"讓局域網內的設備通過瀏覽器看下載的漫畫\",\n    \"sync\": \"同步\",\n    \"account\": \"帳戶\",\n    \"modify_password\": \"修改密碼\",\n    \"ebook\": \"電子書\",\n    \"system\": \"系統\",\n    \"clear_cache\": \"清除快取\",\n    \"migrate\": \"文件遷移\",\n    \"migrate_subtitle\": \"更換您的資料文件夾到記憶卡\",\n    \"migrate_confirm\": \"此功能菜單保存後, 需要重啟程序, 您確認嗎\",\n    \"app_orientation\": {\n      \"title\": \"APP方向\",\n      \"choose\": \"請選擇APP方向\",\n      \"normal\": \"正常\",\n      \"landscape\": \"橫屏\",\n      \"portrait\": \"豎屏\"\n    },\n    \"will_pop_notice\": \"在首頁連續按兩下返回鍵才能退出APP\",\n    \"android_secure_flag\": \"禁止截圖/禁止顯示在任務視圖\",\n    \"android_display_mode\": {\n      \"title\": \"屏幕刷新率(安卓)\",\n      \"dialog_title\": \"安卓屏幕刷新率 \\n(省電模式下不會高刷)\"\n    },\n    \"authentication\": \"進入APP時驗證身份(如果系統已經錄入密碼或指紋)\",\n    \"set_password\": \"設置應用程序密碼\",\n    \"auto_clean\": {\n      \"title\": \"自動清理快取\",\n      \"one_month_ago\": \"一個月前\",\n      \"one_week_ago\": \"一周前\",\n      \"one_day_ago\": \"一天前\",\n      \"no_auto_clean\": \"不自動清理\"\n    },\n    \"categories_column_count\": {\n      \"title\": \"首頁分類展示列數\",\n      \"choose\": \"選擇首頁分類展示列數\",\n      \"auto\": \"自動\"\n    },\n    \"categories_sort\": {\n      \"title\": \"首頁分類排序\"\n    },\n    \"chooser_root\": {\n      \"title\": \"文件夾選擇器根路徑\",\n      \"hint\": \"請輸入文件夾選擇器根路徑\",\n      \"desc\": \"導出時選擇目錄的默認路徑, 同時也是根路徑, 不能正常導出時也可以嘗試設置此選項。\"\n    },\n    \"content_failed_reload_action\": {\n      \"title\": \"頁面加載失敗刷新方式\",\n      \"choose\": \"選擇頁面加載失敗刷新方式\",\n      \"pull_down\": \"下拉刷新\",\n      \"touch_loader\": \"點擊屏幕刷新\"\n    },\n    \"copy_full_name\": {\n      \"title\": \"複製漫畫名稱時使用模版\"\n    },\n    \"copy_full_name_template\": {\n      \"title\": \"複製漫畫名稱模版\",\n      \"hint\": \"模版內容\"\n    },\n    \"copy_skip_confirm\": {\n      \"title\": \"長按複製不需要確認\"\n    },\n    \"download_and_export_path\": {\n      \"title\": \"下載的同時導出到文件系統\",\n      \"confirm\": \"下載的同時導出到文件系統\",\n      \"desc\": \"您即將選擇一個目錄, 如果文件系統可寫, 下載的同時會為您自動導出一份\"\n    },\n    \"download_cache_path\": {\n      \"title\": \"使用其他程序的緩存下載加速\",\n      \"confirm\": \"使用其他程序的緩存下載加速\",\n      \"desc\": \"您即將選擇一個目錄, 這個目錄拷貝自以下目錄才能使用。下載時將會作為緩存文件夾優先讀取。 \",\n      \"cancel_desc\": \"您確定取消使用其他軟件的內容加速的功能嗎? 取消之後您可以再次點擊設置\",\n      \"import_view_log_from_off\": {\n        \"title\": \"導入其他程序的歷史記錄\",\n        \"desc\": \"您即將選擇一個文件, 這個文件拷貝自以下路徑才能使用。\",\n        \"choose_file_dialog_title\": \"選擇要導入的文件\"\n      }\n    },\n    \"download_thread_count\": {\n      \"title\": \"下載線程數\",\n      \"choose\": \"選擇下載線程數\"\n    },\n    \"ebook_scrolling\": {\n      \"title\": \"電子書模式滾動UI\"\n    },\n    \"ebook_scrolling_range\": {\n      \"title\": \"電子書模式滾動UI\",\n      \"desc\": \"滾動幅度\",\n      \"screen_height\": \"屏幕高度\"\n    },\n    \"ebook_scrolling_trigger\": {\n      \"title\": \"電子書模式滾動UI\",\n      \"desc\": \"觸發距離\",\n      \"cm\": \"厘米\"\n    },\n    \"export_path\": {\n      \"ios_desc\": \"隨後可在文件管理中找到導出的內容\",\n      \"ios_desc2\": \"您正在使用iOS設備:\\n導出到文件的內容請打開系統自帶文件管理進行瀏覽\",\n      \"export_path_desc\": \"導出路徑 (點擊可修改)\",\n      \"android_desc\": \"您正在使用安卓設備:\\n如果不能成功導出並且提示權限不足, 可以嘗試在Download或Document下建立子目錄進行導出\"\n    },\n    \"export_rename\": {\n      \"title\": \"導出時進行重命名\"\n    },\n    \"yes\": \"是\",\n    \"no\": \"否\",\n    \"full_screen_action\": {\n      \"title\": \"操控方式\",\n      \"choose\": \"選擇操控方式\",\n      \"touch_once\": \"點擊屏幕一次全屏\",\n      \"controller\": \"使用控制器全屏\",\n      \"touch_double\": \"雙擊屏幕全屏\",\n      \"touch_double_once_next\": \"雙擊屏幕全屏 + 單擊屏幕下一頁\",\n      \"three_area\": \"將屏幕劃分成三個區域 (上一頁, 下一頁, 全屏)\"\n    },\n    \"full_screen_ui\": {\n      \"title\": \"全屏UI\",\n      \"choose\": \"選擇全屏UI\",\n      \"no\": \"不使用\",\n      \"hidden_bottom\": \"去除虛擬控制器\",\n      \"all\": \"全屏\"\n    },\n    \"auto_full_screen\": {\n      \"title\": \"進入閱讀器自動全屏\"\n    },\n    \"auto_full_screen_on_forward\": {\n      \"title\": \"前進時自動全螢幕\"\n    },\n    \"ignore_info_history\": {\n      \"title\": \"詳情頁不計入歷史記錄\"\n    },\n    \"icon_loading\": {\n      \"title\": \"盡量減少UI動畫\"\n    },\n    \"ignore_upgrade_confirm\": {\n      \"title\": \"關閉升級彈窗\"\n    },\n    \"hidden_fd_icon\": {\n      \"title\": \"隱藏個人空間的發電圖標\"\n    },\n    \"hidden_search_persion\": {\n      \"title\": \"隱藏按作者搜索功能\"\n    },\n    \"hidden_viewed\": {\n      \"title\": \"隱藏閱讀過的漫畫\"\n    },\n    \"hidden_sub_icon\": {\n      \"title\": \"隱藏訂閱功能\"\n    },\n    \"hide_online_favorite\": {\n      \"title\": \"隱藏線上收藏\",\n      \"desc\": \"隱藏線上收藏入口與收藏按鈕\"\n    },\n    \"hidden_words\": {\n      \"title\": \"根據關鍵詞隱藏\",\n      \"clear_all\": \"確認清空\",\n      \"clear_all_desc\": \"確定要清空所有關鍵詞嗎？\",\n      \"input_hint\": \"輸入要隱藏的關鍵詞\",\n      \"no_words\": \"暫無關鍵詞\"\n    },\n    \"image_address\": {\n      \"title\": \"圖片分流\",\n      \"pinging\": \"測速中\",\n      \"failed\": \"失敗\"\n    },\n    \"image_filter\": {\n      \"title\": \"閱讀器圖片濾鏡\",\n      \"normal\": \"正常\",\n      \"gray\": \"灰度\",\n      \"brown\": \"棕褐色\",\n      \"choose\": \"選擇閱讀器圖片濾鏡\"\n    },\n    \"import_notice\": {\n      \"android_desc\": \"您正在使用安卓設備:\\n如果不能導入導出並且提示權限不足, 可以嘗試在Download或Document下建立子目錄進行導入\"\n    },\n    \"keyboard_controller\": {\n      \"title\": \"閱讀器鍵盤翻頁(僅PC)\"\n    },\n    \"list_layout\": {\n      \"choose\": \"請選擇布局\",\n      \"info_card\": \"詳情\",\n      \"only_image\": \"封面\",\n      \"cover_and_title\": \"封面+標題\"\n    },\n    \"local_history_sync\": {\n      \"sync_to_local\": \"同步歷史記錄到本地\",\n      \"not_set\": \"未設置\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失敗\",\n      \"auto_sync\": \"自動同步歷史記錄到本地\",\n      \"auto_sync_desc\": \"開啟後每次打開應用會自動備份歷史記錄\",\n      \"choose_dir\": \"選擇目錄\",\n      \"clear_path\": \"清除路徑\",\n      \"clear_path_desc\": \"確定要清除路徑嗎？\"\n    },\n    \"history_sync\": \"歷史記錄同步\",\n    \"local_favorite_sync_title\": \"本地收藏夾同步\",\n    \"use_local_favorite\": \"使用本地收藏夾\",\n    \"use_local_favorite_desc\": \"在本地管理收藏，支援資料夾分類\",\n    \"local_favorite_sync\": {\n      \"auto_sync\": \"自動同步本地收藏夾\",\n      \"auto_sync_desc\": \"使用WebDAV自動同步本地收藏夾\",\n      \"manual_sync\": \"手動同步本地收藏夾\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失敗\"\n    },\n    \"no_animation\": {\n      \"title\": \"取消翻頁動畫（點按屏幕、音量鍵、鍵盤）\"\n    },\n    \"pager_action\": {\n      \"title\": \"列表頁加載方式\",\n      \"choose\": \"選擇列表頁加載方式\",\n      \"controller\": \"使用按鈕\",\n      \"stream\": \"瀑布流\"\n    },\n    \"proxy\": {\n      \"title\": \"代理伺服器\",\n      \"hint\": \"請輸入代理伺服器\",\n      \"desc\": \" ( 例如 socks5://127.0.0.1:1080/ ) \",\n      \"no_proxy\": \"未設置\"\n    },\n    \"quality\": {\n      \"title\": \"瀏覽時的圖片質量\",\n      \"choose\": \"請選擇圖片質量\",\n      \"original\": \"原圖\",\n      \"low\": \"低\",\n      \"medium\": \"中\",\n      \"high\": \"高\"\n    },\n    \"reader_background_color\": {\n      \"title\": \"閱讀器背景色\",\n      \"choose\": \"選擇閱讀器背景色\",\n      \"black\": \"黑色\",\n      \"gray\": \"灰度\",\n      \"white\": \"白色\"\n    },\n    \"reader_direction\": {\n      \"title\": \"閱讀器方向\",\n      \"choose\": \"選擇翻頁方向\",\n      \"top_to_bottom\": \"從上到下\",\n      \"left_to_right\": \"從左到右\",\n      \"right_to_left\": \"從右到左\"\n    },\n    \"reader_scroll_by_screen_percentage\": {\n      \"title\": \"按距離翻頁長度\",\n      \"screen_size\": \"屏幕尺寸\"\n    },\n    \"web_toon_scroll_mode\": {\n      \"title\": \"WebToon 翻頁模式\",\n      \"choose\": \"選擇 WebToon 翻頁模式\",\n      \"image\": \"圖片\",\n      \"screen\": \"距離\"\n    },\n    \"reader_zoom\": {\n      \"out_title\": \"縮小倍數（最小縮放）\",\n      \"in_title\": \"放大倍數（最大縮放）\",\n      \"double_tap_title\": \"雙擊縮放倍數\"\n    },\n    \"drag_region_lock\": {\n      \"title\": \"鎖定拖動邊界\"\n    },\n    \"gesture_speed\": {\n      \"title\": \"手勢速度倍率\"\n    },\n    \"reader_slider_position\": {\n      \"title\": \"滾動條的位置\",\n      \"choose\": \"選擇滑動條位置\",\n      \"bottom\": \"下方\",\n      \"right\": \"右側\",\n      \"left\": \"左側\"\n    },\n    \"reader_two_page_direction\": {\n      \"title\": \"雙頁閱讀器內容排列\",\n      \"choose\": \"選擇雙頁閱讀器內容排列\",\n      \"close_to\": \"靠近\",\n      \"pull_away\": \"遠離\",\n      \"each_centered\": \"各自居中\"\n    },\n    \"reader_type\": {\n      \"title\": \"閱讀器模式\",\n      \"choose\": \"選擇閱讀器模式\",\n      \"web_toon\": \"WebToon (默認)\",\n      \"web_toon_zoom\": \"WebToon (雙擊放大)\",\n      \"gallery\": \"相冊\",\n      \"web_toon_free_zoom\": \"WebToon (ListView雙擊放大)\\n(此模式進度條無效)\",\n      \"two_page_gallery\": \"雙頁模式\\n(實驗)\",\n      \"left_to_right\": \"從左到右\",\n      \"right_to_left\": \"從右到左\",\n      \"two_page_direction\": \"雙頁方向\",\n      \"two_page_direction_choose\": \"選擇雙頁方向\"\n    },\n    \"shadow_categories\": {\n      \"title\": \"封印\",\n      \"search_hint\": \"搜索\"\n    },\n    \"shadow_categories_mode\": {\n      \"title\": \"封印模式\",\n      \"black_list\": \"黑名單\",\n      \"white_list\": \"白名單\"\n    },\n    \"startup_pic\": {\n      \"title\": \"設置啟動圖片\",\n      \"subtitle\": \"設置應用啟動時顯示的圖片\",\n      \"clear_title\": \"清除啟動圖片\",\n      \"clear_subtitle\": \"清除應用啟動時顯示的圖片\",\n      \"clear_success\": \"啟動圖片已清除\",\n      \"update_success\": \"啟動圖片已更新\"\n    },\n    \"show_comment_at_download\": {\n      \"title\": \"在下載顯示評論區\"\n    },\n    \"font\": {\n      \"title\": \"字體\",\n      \"hint\": \"請輸入字體\",\n      \"input_hint\": \"請輸入字體的名稱且用英文逗號分隔, 例如 \\\"宋體,黑體\\\", 如果您保存後沒有發生變化, 說明字體無法使用或名稱錯誤, 可以去參考C:\\\\Windows\\\\Fonts尋找您的字體。若您使用的是flutter2引擎的版本，只有第一個字體生效。\",\n      \"choose_hint\": \"需要您選擇多個字體，直至您點擊背景區域\"\n    },\n    \"theme\": {\n      \"origin\": \"原生\",\n      \"pink\": \"粉色\",\n      \"black\": \"酷黑\",\n      \"dark\": \"暗黑\",\n      \"dusty_blue\": \"灰蓝\",\n      \"dark_black\": \"纯黑\",\n      \"choose_theme\": \"選擇主題\",\n      \"book\": \"書本\",\n      \"enable_status_bar_color\": \"啟用狀態欄顏色\",\n      \"enable_status_restart_hint\": \"關閉時需要重新啟動應用程序刷新狀態欄顏色\"\n    },\n    \"three_keep_right\": {\n      \"title\": \"三區域模式翻頁始終為右側下一頁\"\n    },\n    \"time_zone\": {\n      \"title\": \"時區\"\n    },\n    \"timeout_lock\": {\n      \"title\": \"自動鎖定\",\n      \"notice\": \"注意：自動鎖定在桌面端僅支持最小化後超時，手機端支持後台以及鎖屏後超時。如果沒有設置密碼，自動鎖定無效。安卓以及桌面端只會鎖定桌面，不會鎖定下載，iOS未測試，需要手動開啟後台活動。\",\n      \"1_hour\": \"一小時\",\n      \"10_minutes\": \"十分鐘\",\n      \"3_minutes\": \"三分鐘\",\n      \"1_minute\": \"一分鐘\",\n      \"10_seconds\": \"十秒\",\n      \"1_second\": \"一秒\",\n      \"no_lock\": \"不鎖定\"\n    },\n    \"using_right_click_pop\": {\n      \"title\": \"鼠標右鍵返回上一頁\"\n    },\n    \"volume_controller\": {\n      \"title\": \"閱讀器音量鍵翻頁\"\n    },\n    \"volume_next_chapter\": {\n      \"title\": \"雙擊 音量/鍵盤/控制器 下一章節\"\n    },\n    \"webdav\": {\n      \"title\": \"WebDav\",\n      \"not_set\": \"未設置\",\n      \"path\": \"WebDav 路徑\",\n      \"path_hint\": \"請輸入WebDav 路徑\",\n      \"username\": \"WebDav 用戶名\",\n      \"username_hint\": \"請輸入WebDav 用戶名\",\n      \"password\": \"WebDav 密碼\",\n      \"password_hint\": \"請輸入WebDav 密碼\",\n      \"auto_sync_history_to_webdav\": \"開啟時自動同步瀏覽記錄到WebDav\",\n      \"sync_history_to_webdav\": \"立即同步瀏覽記錄到WebDAV\",\n      \"upload_history_to_webdav\": \"覆蓋WebDAV中的瀏覽記錄\",\n      \"upload_history_to_webdav_desc\": \"如有多台設備，請注意自動同步功能\",\n      \"sync_success\": \"同步成功\",\n      \"sync_failed\": \"同步失敗\"\n    }\n  },\n  \"local_favorite\": {\n    \"title\": \"本地收藏夾\",\n    \"all_folders\": \"全部\",\n    \"new_folder\": \"新建資料夾\",\n    \"select_mode\": \"多選\",\n    \"cancel_select_mode\": \"退出多選\",\n    \"select_all\": \"全選\",\n    \"delete_folder\": \"刪除資料夾\",\n    \"move_to_folder\": \"移動到資料夾\",\n    \"remove_selected\": \"移除選中收藏\",\n    \"remove_selected_confirm\": \"確認從本地收藏夾中移除選中的漫畫？\",\n    \"remove_selected_success\": \"已移除\",\n    \"remove_selected_failed\": \"移除失敗\",\n    \"select_comics\": \"請先選擇漫畫\",\n    \"folder_limit_reached\": \"免費版最多建立3個資料夾，升級Pro解鎖無限制\",\n    \"batch_download\": \"批次下載\",\n    \"select_folder\": \"選擇資料夾\",\n    \"folder_name\": \"資料夾名稱\",\n    \"delete_confirm\": \"確認刪除資料夾？\",\n    \"empty_folder\": \"暫無收藏\",\n    \"no_folders\": \"還沒有資料夾，請先建立\",\n    \"remove_confirm_title\": \"取消收藏\",\n    \"remove_confirm_content\": \"確定要從本地收藏夾中移除這部漫畫嗎？\",\n    \"remove_failed\": \"取消收藏失敗\",\n    \"load_failed\": \"載入失敗\",\n    \"add_success\": \"已新增到本地收藏夾\",\n    \"add_failed\": \"新增失敗\",\n    \"create_folder_failed\": \"建立資料夾失敗\",\n    \"create_success\": \"建立成功\",\n    \"delete_success\": \"刪除成功\",\n    \"delete_failed\": \"刪除失敗\",\n    \"move_success\": \"移動成功\",\n    \"move_failed\": \"移動失敗\",\n    \"select_comics_to_download\": \"請選擇要下載的漫畫\",\n    \"download_started\": \"開始下載\",\n    \"download_failed\": \"下載失敗\"\n  },\n  \"screen\": {\n    \"about\": {\n      \"title\": \"關於\",\n      \"version\": \"軟件版本\",\n      \"check_update\": \"檢查更新\",\n      \"tips\": \"提示 : \\n1. 詳情頁的作者/上傳者/分類/標籤都可以點擊\\n2. 詳情頁的作者/上傳者/標題長按可以複製\\n3. 使用分頁而不是瀑布流點擊頁碼可以快速翻頁\\n4. 下載指的是緩存到本地, 需要導出才可以分享\\n5. 下載長按可以刪除\",\n      \"download_new_version\": \"請從獲取渠道下載新版\",\n      \"no_new_version\": \"未檢測到新版本\",\n      \"download_release_version\": \"下載RELEASE版\",\n      \"update_content\": \"更新內容\",\n      \"go_to_release_repository\": \"去RELEASE倉庫\"\n    },\n    \"account\": {\n      \"title\": \"賬戶\",\n      \"username\": \"賬號\",\n      \"username_hint\": \"請輸入賬號\",\n      \"password\": \"密碼\",\n      \"password_hint\": \"請輸入密碼\",\n      \"no_account_register\": \"沒有賬號,我要註冊\",\n      \"password_reset\": \"密碼找回\",\n      \"check_username_password_or_network\": \"請檢查賬號密碼或網絡環境\",\n      \"check_device_time\": \"請檢查設備時間\",\n      \"username_or_password_error\": \"賬號或密碼錯誤\",\n      \"login_failed\": \"登錄失敗\",\n      \"not_set\": \"未設置\"\n    },\n    \"app\": {\n      \"will_pop_notice\": \"再次返回將會退出應用程序\"\n    },\n    \"categories\": {\n      \"search_hint\": \"搜索\"\n    },\n    \"clean\": {\n      \"title\": \"清理\",\n      \"cleaning\": \"清理中\",\n      \"clean_network_cache\": \"清理网络缓存\",\n      \"clean_image_cache\": \"清理图片缓存\",\n      \"clean_all_cache\": \"清理全部缓存\",\n      \"clean_success\": \"清理成功\",\n      \"clean_failed\": \"清理失败\"\n    },\n    \"close_app\": {\n      \"title\": \"提示\",\n      \"close_app\": \"請關閉應用重新打開\"\n    },\n    \"comic_collections\": {\n      \"no_resource\": \"這裡沒有資源呀\"\n    },\n    \"comic_info\": {\n      \"chapter\": \"章节\",\n      \"comment\": \"评论\",\n      \"recommend\": \"推荐\"\n    },\n    \"comics\": {\n      \"search_hint\": \"搜索分类\",\n      \"choose_category\": \"请选择分类\"\n    },\n    \"comic_subscribes\": {\n      \"update_reminder\": \"更新提醒\",\n      \"check_update\": \"检查更新\",\n      \"cancel_all_update_reminder\": \"取消所有更新提醒\"\n    },\n    \"comment\": {\n      \"title\": \"評論\",\n      \"hint_text\": \"請輸入評論內容\",\n      \"success\": \"評論成功\",\n      \"i_have_something_to_say\": \"我有話要講\",\n      \"please_enter_comment\": \"請輸入評論內容\"\n    },\n    \"desktop_authentication\": {\n      \"current_password\": \"當前密碼\",\n      \"password_error\": \"密碼錯誤\",\n      \"password_initialization\": \"密碼初始化\",\n      \"password\": \"密碼\",\n      \"re_enter_password\": \"再次輸入密碼\",\n      \"password_mismatch\": \"兩次輸入的密碼不一致\",\n      \"set_password\": \"設置密碼\"\n    },\n    \"download_confirm\": {\n      \"please_select_ep\": \"請選擇下載的EP\",\n      \"already_added_to_download_list\": \"已經加入下載列表\"\n    },\n    \"download_export_group\": {\n      \"title\": \"批量導出\",\n      \"please_select_content\": \"請選擇導出的內容\",\n      \"exporting\": \"正在導出\",\n      \"export_failed\": \"導出失敗\",\n      \"export_success\": \"導出成功\",\n      \"export_to_pkz\": \"導出成一個PKZ\\n(加密模式,防止網盤檢測,能用pikapika打開觀看)\",\n      \"export_to_pki\": \"每部漫畫都打包一個PKI\\n(加密模式,防止網盤檢測,能用pikapika導入)\",\n      \"export_to_zip\": \"每部漫畫都打包一個ZIP\\n(不加密模式,能用pikapika導入或網頁瀏覽器觀看)\",\n      \"export_to_jpeg_zip\": \"每部漫畫都打包一個ZIP+JPEG\\n(能直接使用其他閱讀器看,不可再導入)\",\n      \"export_to_jpeg_folder\": \"每部漫畫都導出成文件夾+JPEG\",\n      \"export_to_pdf\": \"每部漫畫都導出成PDF\",\n      \"export_to_epub\": \"每部漫畫都導出成EPUB\",\n      \"export_to_pdf_folder\": \"每部漫畫都導出到文件夾, 每個章節一個PDF\",\n      \"export_to_cbz\": \"每部漫畫都導出成cbz\",\n      \"after_power_use\": \"發電後使用\",\n      \"input_save_name\": \"請輸入保存後的名称\",\n      \"export_confirm\": \"導出確認\",\n      \"export_to_pkz_title\": \"將導出您所選的漫畫為一個PKZ\",\n      \"export_to_pki_title\": \"將您所選的漫畫分別導出成單獨的PKI\",\n      \"please_power_up\": \"請先發電鴨\",\n      \"export_to_zip_title\": \"將您所選的漫畫分別導出成ZIP\",\n      \"export_to_jpeg_zip_title\": \"將您所選的漫畫分別導出成ZIP+JPEG\",\n      \"export_to_jpeg_zip_title_not_down_over\": \"將您所選的漫畫分別導出成ZIP+JPEG\\n(即便沒有下載完成也可以使用)\",\n      \"export_to_jpeg_folder_title\": \"將您所選的漫畫分別導出成文件夾+JPEG\",\n      \"export_to_pdf_title\": \"將您所選的漫畫分別導出成PDF\",\n      \"export_to_epub_title\": \"將您所選的漫畫分別導出成EPUB\",\n      \"export_to_pdf_folder_title\": \"將您所選的漫畫分別導出到文件夾, 每個章節一個PDF\",\n      \"export_to_cbz_title\": \"將您所選的漫畫分別導出成cbz\",\n      \"exporting_please_wait\": \"導出中, 請稍後\"\n    },\n    \"download_export_to_file\": {\n      \"title\": \"導出\",\n      \"transfer_to_other_device\": \"傳輸到其他設備\",\n      \"input_save_name\": \"請輸入保存後的名称\",\n      \"export_confirm\": \"導出確認\",\n      \"export_to_pkz_title\": \"將您所選的漫畫導出PKZ\",\n      \"export_to_pkz_desc\": \"導出到xxx.pkz\\n(可直接打開觀看,不支持導入)\\n(可以躲避網盤或者聊天軟件的掃描)\",\n      \"export_to_pki_title\": \"將您所選的漫畫導出PKI\",\n      \"export_to_pki_desc\": \"導出到xxx.pki\\n(只支持導入, 不支持直接閱讀)\\n(可以躲避網盤或者聊天軟件的掃描)\\n(後期版本可能支持直接閱讀)\",\n      \"export_to_zip_title\": \"將您所選的漫畫導出HTML+ZIP\",\n      \"export_to_zip_desc\": \"導出到xxx.zip\\n(可從其他設備導入 / 解壓後可閱讀)\",\n      \"export_to_jpeg_zip_title\": \"將您所選的漫畫導出HTML+JPEG\",\n      \"export_to_jpeg_zip_desc\": \"導出到xxx.jpeg\\n(可直接在相冊中打開觀看)\",\n      \"export_to_pdf_title\": \"將您所選的漫畫導出PDF\",\n      \"export_to_pdf_desc\": \"導出到xxx.pdf\\n(即使沒有下載成功也可以使用、未成功下載的圖片將會跳過)\\n(可直接在相冊中打開觀看)\",\n      \"export_to_pdf_folder_title\": \"將您所選的漫畫導出到文件夾, 每個章節一個PDF\",\n      \"export_to_pdf_folder_desc\": \"導出到xxx.pdf\\n(即使沒有下載成功也可以使用、未成功下載的圖片將會跳過)\\n(可直接在相冊中打開觀看)\",\n      \"export_to_epub_title\": \"將您所選的漫畫導出EPUB\",\n      \"export_to_epub_desc\": \"導出到xxx.epub, 閱讀器可以直接使用(不可再導入)\",\n      \"export_to_jpeg_folder_title\": \"將您所選的漫畫導出JPEGS.zip\",\n      \"export_to_jpeg_folder_desc\": \"導出閱讀器用JPGS.zip\\n(不可再導入)\",\n      \"export_to_cbz_title\": \"將您所選的漫畫導出cbk.zip\",\n      \"export_to_cbz_desc\": \"導出到xxx.cbz, 閱讀器可以直接使用(不可再導入)\"\n    },\n    \"download_export_to_socket\": {\n      \"title\": \"網絡導出\",\n      \"loading\": \"加載中\",\n      \"tips\": \"傳輸成功之前請不要退出頁面, 一次只能導出到一個設備, 兩台設備需要在同一網段或無限局域網中, 請另外一台設備輸入 IP:端口 , 有一個IP時請選擇無限局域網的IP, 通常是192.168開頭\",\n      \"get_ip_failed\": \"獲取IP失敗\",\n      \"getting_ip\": \"正在獲取IP\",\n      \"port\": \"端口號\"\n    },\n    \"download_import\": {\n      \"title\": \"導入\",\n      \"open_file\": \"打開文件\",\n      \"select_file\": \"選擇要導入的文件\",\n      \"import_success\": \"導入成功\",\n      \"import_failed\": \"導入失敗\",\n      \"select_file_desc\": \"選擇zip文件進行導入\\n選擇pki文件進行導入\\n選擇pkz文件進行閱讀\",\n      \"input_address\": \"請輸入導出設備提供的地址\\n例如 \\\"192.168.1.2:50000\\\"\",\n      \"import_from_other_device\": \"從其他設備導入\",\n      \"select_folder_desc\": \"選擇文件夾\\n(導入裡面所有的zip/pki)\\n(發電後使用)\"\n    },\n    \"download_info\": {\n      \"loading\": \"加載中\",\n      \"chapter\": \"章節\",\n      \"comment\": \"評論\",\n      \"recommend\": \"推薦\"\n    },\n    \"download_list\": {\n      \"search_download\": \"搜索下載\",\n      \"multi_select_operation\": \"多選操作\",\n      \"download_list\": \"下載列表\",\n      \"search\": \"搜索\",\n      \"select_folder\": \"選擇文件夾\",\n      \"download_already_in_delete_queue\": \"該下載已經在刪除隊列中\",\n      \"import\": \"導入\",\n      \"export\": \"導出\",\n      \"file\": \"文件\",\n      \"download_task\": \"下載任務\",\n      \"pause_download\": \"暫停下載嗎?\",\n      \"start_download\": \"啟動下載嗎?\",\n      \"resume_failed\": \"恢復失敗任務\",\n      \"resume_failed_desc\": \"所有失敗的下載已經恢復\",\n      \"downloading\": \"下載中\",\n      \"paused\": \"暫停中\",\n      \"move_download\": \"移動下載\",\n      \"select_download_to_move\": \"請選擇要移動的下載\",\n      \"select_download_to_delete\": \"請選擇要刪除的下載\",\n      \"input_name\": \"==> 輸入名稱 <==\",\n      \"empty_folder_will_be_deleted\": \"（空文件夾將會自動刪除，下次需要手動輸入）\",\n      \"folder_name\": \"文件夾名稱\",\n      \"please_input_folder_name\": \"請輸入文件夾名稱\",\n      \"delete_download\": \"刪除下載\",\n      \"delete_selected_download\": \"刪除選中的下載嗎?\",\n      \"multi_select\": \"多選\"\n    },\n    \"download_only_import\": {\n      \"importing\": \"正在導入\",\n      \"import_success\": \"導入成功\",\n      \"import_failed\": \"導入失敗\",\n      \"click_import_file\": \"點擊導入文件\",\n      \"importing_please_wait\": \"導入中, 請稍後\"\n    },\n    \"favourite_paper\": {\n      \"favourite\": \"收藏\"\n    },\n    \"forgot_password\": {\n      \"title\": \"找回密碼\",\n      \"username\": \"帳號\",\n      \"not_set\": \"未設置\",\n      \"confirm\": \"確認\",\n      \"please_enter_username\": \"請輸入帳號\",\n      \"question_1\": \"問題1\",\n      \"question_2\": \"問題2\",\n      \"question_3\": \"問題3\",\n      \"answer_1\": \"回答1\",\n      \"answer_2\": \"回答2\",\n      \"answer_3\": \"回答3\",\n      \"please_enter_answer_1\": \"請輸入回答1\",\n      \"please_enter_answer_2\": \"請輸入回答2\",\n      \"please_enter_answer_3\": \"請輸入回答3\",\n      \"use_answer_1_recover\": \"使用回答1找回密碼\",\n      \"use_answer_2_recover\": \"使用回答2找回密碼\",\n      \"use_answer_3_recover\": \"使用回答3找回密碼\",\n      \"please_enter_answer\": \"請輸入答案\",\n      \"new_password_copied\": \"新密碼正在複製到剪切板\",\n      \"answer_incorrect\": \"答案不正確\",\n      \"password\": \"密碼\"\n    },\n    \"game_download\": {\n      \"title\": \"下載\",\n      \"download_links_obtained\": \"獲取到下載鏈接, 您只需要選擇其中一個\"\n    },\n    \"game_info\": {\n      \"download\": \"下載\",\n      \"details\": \"詳情\",\n      \"comments\": \"評論\"\n    },\n    \"games\": {\n      \"title\": \"遊戲\"\n    },\n    \"import_from_off\": {\n      \"title\": \"導入\",\n      \"import_success\": \"導入成功\",\n      \"import_failed\": \"導入失敗\"\n    },\n    \"modify_password\": {\n      \"title\": \"修改密碼\",\n      \"please_wait\": \"請稍後\",\n      \"old_password\": \"舊密碼\",\n      \"new_password\": \"新密碼\",\n      \"repeat_new_password\": \"重複輸入新密碼\",\n      \"not_filled\": \"未填寫\",\n      \"please_enter_old_password\": \"請輸入舊密碼\",\n      \"please_enter_new_password\": \"請輸入新密碼\",\n      \"please_repeat_new_password\": \"請重複輸入新密碼\",\n      \"new_password_mismatch\": \"新密碼不匹配\",\n      \"modify_success\": \"修改成功\",\n      \"failed\": \"失敗\",\n      \"confirm\": \"確認\"\n    },\n    \"network_settings\": {\n      \"title\": \"網路設置\"\n    },\n    \"pkz_reader\": {\n      \"reading_downloaded_comic\": \"您閱讀的是下載漫畫\"\n    },\n    \"pro\": {\n      \"title\": \"發電中心\",\n      \"power_center\": \"發電中心\",\n      \"power_status\": \"發電狀態\",\n      \"powered\": \"發電中\",\n      \"not_powered\": \"未發電\",\n      \"pat_membership\": \"PAT入會\",\n      \"pat_status\": \"PAT狀態\",\n      \"pat_normal\": \"PAT正常\",\n      \"pat_bind_hint\": \"請點擊這裡綁定到當前賬號發電\",\n      \"pat_rebind_hint\": \"請點換綁到當前賬號發電\",\n      \"pat_not_detected\": \"未檢測到入會, 請到下載頁入會\",\n      \"i_have_powered\": \"我曾經發過電\",\n      \"i_just_powered\": \"我剛才發了電\",\n      \"enter_code\": \"輸入代碼\",\n      \"power_method\": \"發電方式\",\n      \"wind_power\": \"風力發電\",\n      \"hydro_power\": \"水力發電\",\n      \"solar_power\": \"光伏發電\",\n      \"nuclear_power\": \"核能發電\",\n      \"choose_power_method\": \"選擇發電方式\",\n      \"sign_in_exchange\": \"簽到/兌換\",\n      \"click_pat_to_change\": \"點擊下面的PAT會籍進行變更\",\n      \"update_pat_status\": \"更新PAT發電狀態\",\n      \"bind_to_account\": \"綁定到此賬號\",\n      \"change_pat_key\": \"更換PAT密鑰\",\n      \"clear_pat_info\": \"清除PAT信息\",\n      \"click_to_bind\": \"點擊綁定\",\n      \"enter_auth_code\": \"請輸入授權代碼\",\n      \"please_wait\": \"請稍後\",\n      \"key_recorded\": \"密鑰 : 已錄入\",\n      \"pat_account\": \"PAT賬號\",\n      \"bind_pika_account\": \"綁定PIKA賬號\",\n      \"bind_account_time\": \"綁定賬號時間\",\n      \"rebind_time\": \"可以換綁時間\",\n      \"power_features\": \"發電小功能: 多線程下載 / 批量導入導出下載\",\n      \"power_guide\": \"去\\\"關於\\\"界面找到維護地址可獲得發電指引\\n\\n  \\\"我曾經發過電\\\"可同步相應發電狀態\\n  \\\"我剛才發了電\\\"兌換神秘代碼\\n  \\\"發電方式\\\"可以在網絡不通時嘗試更換\\n  \\\"PAT入會\\\"是獨立的發電方式\"\n    },\n    \"rankings\": {\n      \"title\": \"排行榜\",\n      \"day\": \"天\",\n      \"week\": \"周\",\n      \"month\": \"月\",\n      \"knight\": \"騎\",\n      \"refresh\": \"刷新\",\n      \"comics_count\": \"本\"\n    },\n    \"random_comics\": {\n      \"title\": \"隨機本子\"\n    },\n    \"register\": {\n      \"title\": \"註冊\",\n      \"registering\": \"註冊中\",\n      \"register_success\": \"註冊成功\",\n      \"register_failed\": \"註冊失敗\",\n      \"account_exists\": \"賬號已存在\",\n      \"name_exists\": \"暱稱已存在\",\n      \"check_form\": \"請檢查表單, 不允許留空\",\n      \"account\": \"賬號\",\n      \"password\": \"密碼\",\n      \"nickname\": \"暱稱\",\n      \"gender\": \"性別\",\n      \"birthday\": \"生日\",\n      \"question_1\": \"問題1\",\n      \"answer_1\": \"回答1\",\n      \"question_2\": \"問題2\",\n      \"answer_2\": \"回答2\",\n      \"question_3\": \"問題3\",\n      \"answer_3\": \"回答3\",\n      \"not_set\": \"未設置\",\n      \"please_enter_account\": \"請輸入賬號\",\n      \"please_enter_password\": \"請輸入密碼\",\n      \"please_enter_nickname\": \"請輸入暱稱\",\n      \"please_enter_question_1\": \"請輸入問題1\",\n      \"please_enter_answer_1\": \"請輸入回答1\",\n      \"please_enter_question_2\": \"請輸入問題2\",\n      \"please_enter_answer_2\": \"請輸入回答2\",\n      \"please_enter_question_3\": \"請輸入問題3\",\n      \"please_enter_answer_3\": \"請輸入回答3\",\n      \"account_desc\": \"(小寫字母+數字/登錄使用)\",\n      \"password_desc\": \"(大小寫字母+數字/8位或以上)\",\n      \"nickname_desc\": \"(可使用中文/2-50字)\",\n      \"choose_gender\": \"選擇您的性別\",\n      \"futa\": \"扶她\",\n      \"male\": \"公\",\n      \"female\": \"母\",\n      \"register_success_desc\": \"您已經註冊成功, 請返回登錄\",\n      \"account_label\": \"賬號\",\n      \"nickname_label\": \"暱稱\"\n    },\n    \"search\": {\n      \"title\": \"搜索\",\n      \"search_hint\": \"搜索\",\n      \"choose_category\": \"請選擇分類\"\n    },\n    \"search_author\": {\n      \"title\": \"按作者搜索\",\n      \"search_hint\": \"搜索 按作者 + \",\n      \"by_author\": \"按作者: \"\n    },\n    \"space\": {\n      \"title\": \"我的\",\n      \"logout\": \"退出登錄\",\n      \"logout_confirm\": \"您確認要退出當前賬號嗎?\",\n      \"my_favourites\": \"我的收藏\",\n      \"view_history\": \"瀏覽記錄\",\n      \"my_downloads\": \"我的下載\"\n    },\n    \"theme\": {\n      \"title\": \"主題設置\",\n      \"theme\": \"主題\",\n      \"dark_mode_different_theme\": \"深色模式下使用不同的主題\",\n      \"dark_mode_theme\": \"主題 (深色模式)\"\n    },\n    \"view_logs\": {\n      \"title\": \"瀏覽記錄\",\n      \"clear_all\": \"您要清除所有瀏覽記錄嗎? \",\n      \"clear_all_desc\": \"將會同時刪除瀏覽進度!\",\n      \"clear_one\": \"您要清除這條瀏覽記錄嗎? \",\n      \"clear_one_desc\": \"將會同時刪除瀏覽進度!\",\n      \"clear_selected\": \"您要清除選中的瀏覽記錄嗎? \",\n      \"clear_selected_desc\": \"將會同時刪除瀏覽進度!\",\n      \"categories\": \"分類\"\n    },\n    \"web_server\": {\n      \"title\": \"下載 - Web服務器\",\n      \"loading\": \"加載中\",\n      \"get_ip_failed\": \"獲取IP失敗\",\n      \"getting_ip\": \"正在獲取IP\",\n      \"port\": \"端口號:8080\",\n      \"usage_instruction\": \"在瀏覽器中輸入\\\"http://本設備ip:8080/\\\"訪問下載的漫畫\",\n      \"leave_notice\": \"離開頁面後服務器將關閉\"\n    }\n  },\n  \"components\": {\n    \"comic_info_card\": {\n      \"categories\": \"分類\",\n      \"finished\": \"完結\",\n      \"viewed\": \"看過\"\n    },\n    \"comic_list\": {\n      \"shadow\": \"被封印的本子\"\n    },\n    \"common\": {\n      \"display_mode\": \"顯示模式\",\n      \"shadow_mode\": \"封印模式\",\n      \"shadow_list\": \"封印列表\",\n      \"batch_download\": \"批量下載\"\n    },\n    \"image_reader\": {\n      \"already_at_the_end\": \"已經到頭了\",\n      \"click_to_next_chapter\": \"再次點擊跳轉到下一章\",\n      \"reload_page\": \"重載頁面\",\n      \"next_chapter\": \"下一章\",\n      \"end_reading\": \"結束閱讀\",\n      \"reload_image\": \"重新加載圖片\",\n      \"save_image_in_this_page\": \"保存本頁的圖片\",\n      \"image_load_failed\": \"圖片加載失敗\"\n    }\n  }\n}\n"
  },
  {
    "path": "lib/basic/Channels.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\n\nimport 'package:flutter/services.dart';\n\n// EventChannel\n// 由于Flutter的EventChannel只能订阅一次, 且为了和golang的的通信, 这里实现了多次订阅的分发和平铺\n// 根据eventName订阅和取消订阅\n\nvar _eventChannel = const EventChannel(\"flatEvent\");\nStreamSubscription? _eventChannelListen;\n\nMap<void Function(String args), String> _eventMap = {};\n\nvoid registerEvent(void Function(String args) eventHandler, String eventName) {\n  if (_eventMap.containsKey(eventHandler)) {\n    throw 'once register';\n  }\n  _eventMap[eventHandler] = eventName;\n  if (_eventMap.length == 1) {\n    _eventChannelListen =\n        _eventChannel.receiveBroadcastStream().listen(_onFlatEvent);\n  }\n}\n\nvoid unregisterEvent(void Function(String args) eventHandler) {\n  if (!_eventMap.containsKey(eventHandler)) {\n    throw 'no register';\n  }\n  _eventMap.remove(eventHandler);\n  if (_eventMap.isEmpty) {\n    _eventChannelListen?.cancel();\n  }\n}\n\nvoid _onFlatEvent(dynamic t) {\n  _FlatEvent e = _FlatEvent.fromJson(jsonDecode(t));\n  _eventMap.forEach((key, value) {\n    if (value == e.function) {\n      key(e.content);\n    }\n  });\n}\n\nclass _FlatEvent {\n  late String function;\n  late String content;\n\n  _FlatEvent.fromJson(Map<String, dynamic> json) {\n    this.function = json[\"function\"];\n    this.content = json[\"content\"];\n  }\n}\n"
  },
  {
    "path": "lib/basic/Common.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_styled_toast/flutter_styled_toast.dart';\nimport 'package:pikapika/screens/AccessKeyReplaceScreen.dart';\nimport 'package:uni_links/uni_links.dart';\nimport 'package:uri_to_file/uri_to_file.dart';\n\nimport '../screens/ComicInfoScreen.dart';\nimport '../screens/DownloadOnlyImportScreen.dart';\nimport '../screens/PkzArchiveScreen.dart';\nimport 'config/IconLoading.dart';\nimport 'config/TimeOffsetHour.dart';\n\n/// 默认的图片尺寸\ndouble coverWidth = 210;\ndouble coverHeight = 315;\n\nString categoryTitle(String? categoryTitle) {\n  return categoryTitle ?? tr('categories.all');\n}\n\n/// 显示一个toast\nvoid defaultToast(BuildContext context, String title) {\n  showToast(\n    title,\n    context: context,\n    position: StyledToastPosition.center,\n    animation: StyledToastAnimation.scale,\n    reverseAnimation: StyledToastAnimation.fade,\n    duration: const Duration(seconds: 4),\n    animDuration: const Duration(seconds: 1),\n    curve: Curves.elasticOut,\n    reverseCurve: Curves.linear,\n  );\n}\n\n/// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true\nFuture<bool> confirmDialog(\n    BuildContext context, String title, String content) async {\n  return await showDialog(\n          context: context,\n          builder: (context) => AlertDialog(\n                title: Text(title),\n                content: SingleChildScrollView(\n                  child: ListBody(\n                    children: <Widget>[Text(content)],\n                  ),\n                ),\n                actions: <Widget>[\n                  MaterialButton(\n                    child: Text(tr('app.cancel')),\n                    onPressed: () {\n                      Navigator.of(context).pop(false);\n                    },\n                  ),\n                  MaterialButton(\n                    child: Text(tr('app.confirm')),\n                    onPressed: () {\n                      Navigator.of(context).pop(true);\n                    },\n                  ),\n                ],\n              )) ??\n      false;\n}\n\n/// 显示一个消息提示框\nFuture alertDialog(BuildContext context, String title, String content) {\n  return showDialog(\n    context: context,\n    builder: (context) => AlertDialog(\n      title: Text(title),\n      content: SingleChildScrollView(\n        child: ListBody(\n          children: <Widget>[\n            Text(content),\n          ],\n        ),\n      ),\n      actions: <Widget>[\n        MaterialButton(\n          child: Text(tr('app.confirm')),\n          onPressed: () {\n            Navigator.of(context).pop();\n          },\n        ),\n      ],\n    ),\n  );\n}\n\n/// stream-filter的替代方法\nList<T> filteredList<T>(List<T> list, bool Function(T) filter) {\n  List<T> result = [];\n  for (var element in list) {\n    if (filter(element)) {\n      result.add(element);\n    }\n  }\n  return result;\n}\n\n/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容\nFuture<T?> chooseListDialog<T>(\n    BuildContext context, String title, List<T> items,\n    {String? tips}) async {\n  return showDialog<T>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(title),\n        children: [\n          ...items.map((e) => SimpleDialogOption(\n                onPressed: () {\n                  Navigator.of(context).pop(e);\n                },\n                child: Text('$e'),\n              )),\n          ...tips != null\n              ? [\n                  Container(\n                    padding: const EdgeInsets.fromLTRB(15, 5, 15, 15),\n                    child: Text(tips),\n                  ),\n                ]\n              : [],\n        ],\n      );\n    },\n  );\n}\n\n/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value)\nFuture<T?> chooseMapDialog<T>(\n    BuildContext buildContext, Map<String, T> values, String title) async {\n  return await showDialog<T>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(title),\n        children: values.entries\n            .map((e) => SimpleDialogOption(\n                  child: Text(e.key),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.value);\n                  },\n                ))\n            .toList(),\n      );\n    },\n  );\n}\n\n/// 输入对话框1\n\nvar _controller =\n    TextEditingController.fromValue(const TextEditingValue(text: ''));\n\nFuture<String?> displayTextInputDialog(BuildContext context,\n    {String? title,\n    String src = \"\",\n    String? hint,\n    String? desc,\n    bool isPasswd = false}) {\n  _controller.text = src;\n  return showDialog(\n    context: context,\n    builder: (context) {\n      return AlertDialog(\n        title: title == null ? null : Text(title),\n        content: SingleChildScrollView(\n          child: ListBody(\n            children: [\n              TextField(\n                controller: _controller,\n                decoration: InputDecoration(hintText: hint),\n                obscureText: isPasswd,\n                obscuringCharacter: '\\u2022',\n              ),\n              ...(desc == null\n                  ? []\n                  : [\n                      Container(\n                        padding: const EdgeInsets.only(top: 20, bottom: 10),\n                        child: Text(\n                          desc,\n                          style: TextStyle(\n                              fontSize: 12,\n                              color: Theme.of(context)\n                                  .textTheme\n                                  .bodyText1\n                                  ?.color\n                                  ?.withOpacity(.5)),\n                        ),\n                      )\n                    ]),\n            ],\n          ),\n        ),\n        actions: <Widget>[\n          MaterialButton(\n            child: Text(tr('app.cancel')),\n            onPressed: () {\n              Navigator.of(context).pop();\n            },\n          ),\n          MaterialButton(\n            child: Text(tr('app.confirm')),\n            onPressed: () {\n              Navigator.of(context).pop(_controller.text);\n            },\n          ),\n        ],\n      );\n    },\n  );\n}\n\n/// 将字符串前面加0直至满足len位\nString add0(int num, int len) {\n  var rsp = \"$num\";\n  while (rsp.length < len) {\n    rsp = \"0$rsp\";\n  }\n  return rsp;\n}\n\n/// 格式化时间 2012-34-56\nString formatTimeToDate(String str) {\n  try {\n    var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour()));\n    return \"${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)}\";\n  } catch (e) {\n    return \"-\";\n  }\n}\n\n/// 格式化时间 2012-34-56 12:34:56\nString formatTimeToDateTime(String str) {\n  try {\n    var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour()));\n    return \"${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)} ${add0(c.hour, 2)}:${add0(c.minute, 2)}\";\n  } catch (e) {\n    return \"-\";\n  }\n}\n\n/// 输入对话框2\n\nfinal TextEditingController _textEditController =\n    TextEditingController(text: '');\n\nFuture<String?> inputString(BuildContext context, String title,\n    {String hint = \"\", String? defaultValue}) async {\n  if (defaultValue != null) {\n    _textEditController.text = defaultValue;\n  } else {\n    _textEditController.clear();\n  }\n  return showDialog(\n    context: context,\n    builder: (context) {\n      return AlertDialog(\n        content: Card(\n          child: SingleChildScrollView(\n            child: ListBody(\n              children: [\n                Text(title),\n                TextField(\n                  controller: _textEditController,\n                  decoration: InputDecoration(\n                    labelText: hint,\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n        actions: <Widget>[\n          MaterialButton(\n            onPressed: () {\n              Navigator.pop(context);\n            },\n            child: Text(tr('app.cancel')),\n          ),\n          MaterialButton(\n            onPressed: () {\n              Navigator.pop(context, _textEditController.text);\n            },\n            child: Text(tr('app.confirm')),\n          ),\n        ],\n      );\n    },\n  );\n}\n\nStreamSubscription<String?> linkSubscript(BuildContext context) {\n  return linkStream.listen((uri) async {\n    if (uri == null) return;\n    var parsed = Uri.parse(uri);\n    if (RegExp(r\"^pika://access_key/([0-9A-z:\\-]+)/$\").allMatches(uri).isNotEmpty) {\n      String accessKey = RegExp(r\"^pika://access_key/([0-9A-z:\\-]+)/$\")\n          .allMatches(uri)\n          .first\n          .group(1)!;\n      Navigator.of(context).push(\n        mixRoute(\n          builder: (BuildContext context) => AccessKeyReplaceScreen(accessKey: accessKey),\n        ),\n      );\n    } else if (RegExp(r\"^pika://comic/([0-9A-z]+)/$\").allMatches(uri).isNotEmpty) {\n      String comicId = RegExp(r\"^pika://comic/([0-9A-z]+)/$\")\n          .allMatches(uri)\n          .first\n          .group(1)!;\n      Navigator.of(context).push(\n        mixRoute(\n          builder: (BuildContext context) => ComicInfoScreen(comicId: comicId),\n        ),\n      );\n    } else if (RegExp(r\"^https?://pika/comic/([0-9A-z]+)/$\").allMatches(uri).isNotEmpty) {\n      String comicId = RegExp(r\"^https?://pika/comic/([0-9A-z]+)/$\")\n          .allMatches(uri)\n          .first\n          .group(1)!;\n      Navigator.of(context).push(\n        mixRoute(\n          builder: (BuildContext context) => ComicInfoScreen(comicId: comicId),\n        ),\n      );\n    } else if (RegExp(r\"^.*\\.pkz$\").allMatches(parsed.path).isNotEmpty) {\n      File file = await toFile(uri);\n      Navigator.of(context).push(\n        mixRoute(\n          builder: (BuildContext context) =>\n              PkzArchiveScreen(pkzPath: file.path),\n        ),\n      );\n    } else if (RegExp(r\"^.*\\.((pki)|(zip))$\").allMatches(parsed.path).isNotEmpty) {\n      File file = await toFile(uri);\n      Navigator.of(context).push(\n        mixRoute(\n          builder: (BuildContext context) =>\n              DownloadOnlyImportScreen(path: file.path),\n        ),\n      );\n    }\n  });\n}\n"
  },
  {
    "path": "lib/basic/Cross.dart",
    "content": "/// 与平台交互的操作\nimport 'dart:io';\n\nimport 'package:clipboard/clipboard.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/CopySkipConfirm.dart';\nimport 'package:pikapika/basic/config/Platform.dart';\nimport 'package:url_launcher/url_launcher.dart';\n\nimport 'Method.dart';\nimport 'config/ChooserRoot.dart';\n\n/// 复制内容到剪切板\nvoid copyToClipBoard(BuildContext context, String string) {\n  FlutterClipboard.copy(string);\n  defaultToast(\n    context,\n    tr('app.copied_to_clipboard'),\n  );\n}\n\nvoid copyToClipBoardTips(BuildContext context, String string) {\n  FlutterClipboard.copy(string);\n  defaultToast(context, tr('app.copied_to_clipboard') + \" :\\n$string\");\n}\n\n/// 打开web页面\nFuture<dynamic> openUrl(String url) async {\n  if (await canLaunch(url)) {\n    await launch(\n      url,\n      forceSafariVC: false,\n    );\n  }\n}\n\n/// 保存图片\nFuture<dynamic> saveImage(String path, BuildContext context) async {\n  Future? future;\n  if (Platform.isIOS) {\n    future = method.iosSaveFileToImage(path);\n  } else if (Platform.isAndroid) {\n    future = _saveImageAndroid(path, context);\n  } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {\n    String? folder = await chooseFolder(context);\n    if (folder != null) {\n      future = method.convertImageToJPEG100(path, folder);\n    }\n  } else {\n    defaultToast(context, tr('app.not_supported_platform'));\n    return;\n  }\n  if (future == null) {\n    defaultToast(context, tr('app.save_cancel'));\n    return;\n  }\n  try {\n    await future;\n    defaultToast(context, tr('app.save_success'));\n  } catch (e, s) {\n    print(\"$e\\n$s\");\n    defaultToast(context, tr('app.save_failed'));\n  }\n}\n\n/// 保存图片且保持静默, 用于批量导出到相册\nFuture<dynamic> saveImageQuiet(String path, BuildContext context) async {\n  if (Platform.isIOS) {\n    return method.iosSaveFileToImage(path);\n  } else if (Platform.isAndroid) {\n    return _saveImageAndroid(path, context);\n  } else {\n    throw Exception(\"only mobile\");\n  }\n}\n\nFuture<dynamic> _saveImageAndroid(String path, BuildContext context) async {\n  late bool g;\n  if (androidVersion < 30) {\n    g = await Permission.storage.request().isGranted;\n  } else {\n    g = await Permission.manageExternalStorage.request().isGranted;\n  }\n  if (!g) {\n    return;\n  }\n  return method.androidSaveFileToImage(path);\n}\n\n/// 选择一个文件夹用于保存文件\nFuture<String?> chooseFolder(BuildContext context) async {\n  return FilePicker.platform.getDirectoryPath(\n    dialogTitle: tr('app.choose_folder'),\n    initialDirectory:\n        Directory.fromUri(Uri.file(await currentChooserRoot())).absolute.path,\n  );\n}\n\n/// 复制对话框\nvoid confirmCopy(BuildContext context, String content) async {\n  if (copySkipConfirm()) {\n    copyToClipBoardTips(context, content);\n  } else {\n    if (await confirmDialog(context, tr('app.copy'), content)) {\n      copyToClipBoard(context, content);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/basic/Entities.dart",
    "content": "import 'dart:convert';\n\n/// 图片\nclass RemoteImageInfo {\n  late String originalName;\n  late String path;\n  late String fileServer;\n\n  RemoteImageInfo.fromJson(Map<String, dynamic> json) {\n    this.originalName = json[\"originalName\"];\n    this.path = json[\"path\"];\n    this.fileServer = json[\"fileServer\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['originalName'] = originalName;\n    _data['path'] = path;\n    _data['fileServer'] = fileServer;\n    return _data;\n  }\n}\n\n/// 用户基本信息\nclass BasicUser {\n  late String id;\n  late String gender;\n  late String name;\n  late String title;\n  late bool verified;\n  late int exp;\n  late int level;\n  late List<String> characters;\n  late RemoteImageInfo avatar;\n  late String? slogan;\n\n  BasicUser.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.gender = json[\"gender\"];\n    this.name = json[\"name\"];\n    this.title = json[\"title\"];\n    this.verified = json[\"verified\"];\n    this.exp = json[\"exp\"];\n    this.level = json[\"level\"];\n    this.characters = json[\"characters\"] == null\n        ? []\n        : List.of(json[\"characters\"]).map((e) => \"$e\").toList();\n    this.avatar =\n        RemoteImageInfo.fromJson(Map<String, dynamic>.of(json[\"avatar\"]));\n    this.slogan = json[\"slogan\"];\n  }\n}\n\n/// 用户自己的信息\nclass UserProfile extends BasicUser {\n  late String birthday;\n  late String email;\n  late String createdAt;\n  late bool isPunched;\n\n  UserProfile.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.birthday = json[\"birthday\"];\n    this.email = json[\"email\"];\n    this.createdAt = json[\"created_at\"];\n    this.isPunched = json[\"isPunched\"];\n  }\n}\n\n/// 分页\nclass Page {\n  late int total;\n  late int limit;\n  late int page;\n  late int pages;\n\n  Page.fromJson(Map<String, dynamic> json) {\n    this.total = json[\"total\"];\n    this.limit = json[\"limit\"];\n    this.page = json[\"page\"];\n    this.pages = json[\"pages\"];\n  }\n\n  Page.of(this.total, this.limit, this.page, this.pages);\n}\n\n/// 分类\nclass Category {\n  late String id;\n  late String title;\n  late String description;\n  late RemoteImageInfo thumb;\n  late bool isWeb;\n  late bool active;\n  late String link;\n\n  Category.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.title = json[\"title\"];\n    this.description = json[\"description\"];\n    this.thumb = RemoteImageInfo.fromJson(json[\"thumb\"]);\n    this.isWeb = json[\"isWeb\"];\n    this.active = json[\"active\"];\n    this.link = json[\"link\"];\n  }\n}\n\n/// 漫画分页\nclass ComicsPage extends Page {\n  late List<ComicSimple> docs;\n\n  ComicsPage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.from(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => ComicSimple.fromJson(e))\n        .toList();\n  }\n}\n\n/// 漫画基本信息\nclass ComicSimple {\n  late String id;\n  late String title;\n  late String author;\n  late int pagesCount;\n  late int epsCount;\n  late bool finished;\n  late List<String> categories;\n  late RemoteImageInfo thumb;\n  late int likesCount;\n\n  ComicSimple.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.title = json[\"title\"];\n    this.author = json[\"author\"];\n    this.pagesCount = json[\"pagesCount\"];\n    this.epsCount = json[\"epsCount\"];\n    this.finished = json[\"finished\"];\n    this.categories = List<String>.from(json[\"categories\"]);\n    this.thumb = RemoteImageInfo.fromJson(json[\"thumb\"]);\n    this.likesCount = json[\"likesCount\"];\n  }\n}\n\n/// 漫画详情\nclass ComicInfo extends ComicSimple {\n  late String description;\n  late String chineseTeam;\n  late List<String> tags;\n  late String updatedAt;\n  late String createdAt;\n  late bool allowDownload;\n  late int viewsCount;\n  late bool isFavourite;\n  late bool isLiked;\n  late int commentsCount;\n  late Creator creator;\n\n  ComicInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.description = json[\"description\"];\n    this.chineseTeam = json[\"chineseTeam\"];\n    this.tags = List<String>.from(json[\"tags\"]);\n    this.updatedAt = (json[\"updated_at\"]);\n    this.createdAt = (json[\"created_at\"]);\n    this.allowDownload = json[\"allowDownload\"];\n    this.viewsCount = json[\"viewsCount\"];\n    this.isFavourite = json[\"isFavourite\"];\n    this.isLiked = json[\"isLiked\"];\n    this.commentsCount = json[\"commentsCount\"];\n    this.creator = Creator.fromJson(Map<String, dynamic>.of(json[\"_creator\"]));\n  }\n}\n\n/// 漫画创建人信息\nclass Creator extends BasicUser {\n  late String role;\n  late String character;\n\n  Creator.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.role = json[\"role\"];\n    this.character = json[\"character\"];\n  }\n}\n\n/// 漫画章节\nclass Ep {\n  late String id;\n  late String title;\n  late int order;\n  late String updatedAt;\n\n  Ep.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.title = json[\"title\"];\n    this.order = json[\"order\"];\n    this.updatedAt = (json[\"updated_at\"]);\n  }\n}\n\n/// 漫画章节分页\nclass EpPage extends Page {\n  late List<Ep> docs;\n\n  EpPage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.from(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => Ep.fromJson(e))\n        .toList();\n  }\n}\n\n/// 漫画图片分页\nclass PicturePage extends Page {\n  late List<Picture> docs;\n\n  PicturePage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.from(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => Picture.fromJson(e))\n        .toList();\n  }\n}\n\n/// 漫画图片信息\nclass Picture {\n  late String id;\n  late RemoteImageInfo media;\n\n  Picture.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.media = RemoteImageInfo.fromJson(json[\"media\"]);\n  }\n}\n\n/// 显示图片数据\nclass RemoteImageData {\n  late int fileSize;\n  late String format;\n  late int width;\n  late int height;\n  late String finalPath;\n\n  RemoteImageData.forData(\n    this.fileSize,\n    this.format,\n    this.width,\n    this.height,\n    this.finalPath,\n  );\n\n  RemoteImageData.fromJson(Map<String, dynamic> json) {\n    this.fileSize = json[\"fileSize\"];\n    this.format = json[\"format\"];\n    this.width = json[\"width\"];\n    this.height = json[\"height\"];\n    this.finalPath = json[\"finalPath\"];\n  }\n}\n\n/// 漫画评论分页\nclass CommentPage extends Page {\n  late List<Comment> docs;\n\n  CommentPage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.from(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => Comment.fromJson(e))\n        .toList();\n  }\n}\n\nclass CommentBase {\n  late String id;\n  late String content;\n  late CommentUser user;\n  late bool isTop;\n  late bool hide;\n  late String createdAt;\n  late int likesCount;\n  late int commentsCount;\n  late bool isLiked;\n\n  CommentBase.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.content = json[\"content\"];\n    this.user = CommentUser.fromJson(Map<String, dynamic>.of(json[\"_user\"]));\n    this.isTop = json[\"isTop\"];\n    this.hide = json[\"hide\"];\n    this.createdAt = json[\"created_at\"];\n    this.likesCount = json[\"likesCount\"];\n    this.commentsCount = json[\"commentsCount\"];\n    this.isLiked = json[\"isLiked\"];\n  }\n}\n\n/// 子评论\nclass ChildOfComment extends CommentBase {\n  late String parent;\n\n  ChildOfComment.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.parent = json[\"_parent\"];\n  }\n}\n\n/// 漫画评论详情\nclass Comment extends CommentBase {\n  late String comic;\n\n  Comment.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.comic = json[\"_comic\"];\n  }\n}\n\n/// 评论的用户信息\nclass CommentUser extends BasicUser {\n  late String role;\n\n  CommentUser.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.role = json[\"role\"];\n  }\n}\n\n/// 已下载图片的信息\nclass DownloadPicture {\n  late int rankInEp;\n  late String fileServer;\n  late String path;\n  late String localPath;\n  late int width;\n  late int height;\n  late String format;\n  late int fileSize;\n\n  DownloadPicture.fromJson(Map<String, dynamic> json) {\n    this.rankInEp = json[\"rankInEp\"];\n    this.fileServer = json[\"fileServer\"];\n    this.path = json[\"path\"];\n    this.localPath = json[\"localPath\"];\n    this.width = json[\"width\"];\n    this.height = json[\"height\"];\n    this.format = json[\"format\"];\n    this.fileSize = json[\"fileSize\"];\n  }\n}\n\n/// 浏览历史记录\nclass ViewLog {\n  late String id;\n  late String title;\n  late String author;\n  late int pagesCount;\n  late int epsCount;\n  late bool finished;\n  late String categories;\n  late String thumbOriginalName;\n  late String thumbFileServer;\n  late String thumbPath;\n  late String description;\n  late String chineseTeam;\n  late String tags;\n  late String lastViewTime;\n  late int lastViewEpOrder;\n  late String lastViewEpTitle;\n  late int lastViewPictureRank;\n\n  ViewLog.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"id\"];\n    this.title = json[\"title\"];\n    this.author = json[\"author\"];\n    this.pagesCount = json[\"pagesCount\"];\n    this.epsCount = json[\"epsCount\"];\n    this.finished = json[\"finished\"];\n    this.categories = json[\"categories\"];\n    this.thumbOriginalName = json[\"thumbOriginalName\"];\n    this.thumbFileServer = json[\"thumbFileServer\"];\n    this.thumbPath = json[\"thumbPath\"];\n    this.description = json[\"description\"];\n    this.chineseTeam = json[\"chineseTeam\"];\n    this.tags = json[\"tags\"];\n    this.lastViewTime = json[\"lastViewTime\"];\n    this.lastViewEpOrder = json[\"lastViewEpOrder\"];\n    this.lastViewEpTitle = json[\"lastViewEpTitle\"];\n    this.lastViewPictureRank = json[\"lastViewPictureRank\"];\n  }\n}\n\n/// 已下载漫画的信息\nclass DownloadComic {\n  late String id;\n  late String createdAt;\n  late String updatedAt;\n  late String title;\n  late String author;\n  late int pagesCount;\n  late int epsCount;\n  late bool finished;\n\n  late String categories;\n  late String thumbOriginalName;\n  late String thumbFileServer;\n  late String thumbPath;\n  late String thumbLocalPath;\n\n  late String description;\n  late String chineseTeam;\n  late String tags;\n  late int selectedEpCount;\n  late int selectedPictureCount;\n  late int downloadEpCount;\n  late int downloadPictureCount;\n  late bool downloadFinished;\n  late String downloadFinishedTime;\n  late bool downloadFailed;\n  late bool deleting;\n\n  void copy(DownloadComic other) {\n    this.id = other.id;\n    this.createdAt = other.createdAt;\n    this.updatedAt = other.updatedAt;\n    this.title = other.title;\n    this.author = other.author;\n    this.pagesCount = other.pagesCount;\n    this.epsCount = other.epsCount;\n    this.finished = other.finished;\n    this.categories = other.categories;\n    this.thumbOriginalName = other.thumbOriginalName;\n    this.thumbFileServer = other.thumbFileServer;\n    this.thumbPath = other.thumbPath;\n    this.description = other.description;\n    this.chineseTeam = other.chineseTeam;\n    this.tags = other.tags;\n    this.selectedEpCount = other.selectedEpCount;\n    this.selectedPictureCount = other.selectedPictureCount;\n    this.downloadEpCount = other.downloadEpCount;\n    this.downloadPictureCount = other.downloadPictureCount;\n    this.downloadFinished = other.downloadFinished;\n    this.downloadFinishedTime = other.downloadFinishedTime;\n    this.downloadFailed = other.downloadFailed;\n    this.thumbLocalPath = other.thumbLocalPath;\n    // this.deleting = other.deleting;\n  }\n\n  DownloadComic.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"id\"];\n    this.createdAt = (json[\"createdAt\"]);\n    this.updatedAt = (json[\"updatedAt\"]);\n    this.title = json[\"title\"];\n    this.author = json[\"author\"];\n    this.pagesCount = json[\"pagesCount\"];\n    this.epsCount = json[\"epsCount\"];\n    this.finished = json[\"finished\"];\n    this.categories = json[\"categories\"];\n    this.thumbOriginalName = json[\"thumbOriginalName\"];\n    this.thumbFileServer = json[\"thumbFileServer\"];\n    this.thumbPath = json[\"thumbPath\"];\n    this.description = json[\"description\"];\n    this.chineseTeam = json[\"chineseTeam\"];\n    this.tags = json[\"tags\"];\n    this.selectedEpCount = json[\"selectedEpCount\"];\n    this.selectedPictureCount = json[\"selectedPictureCount\"];\n    this.downloadEpCount = json[\"downloadEpCount\"];\n    this.downloadPictureCount = json[\"downloadPictureCount\"];\n    this.downloadFinished = json[\"downloadFinished\"];\n    this.downloadFinishedTime = json[\"downloadFinishedTime\"];\n    this.downloadFailed = json[\"downloadFailed\"];\n    this.deleting = json[\"deleting\"];\n    this.thumbLocalPath = json[\"thumbLocalPath\"];\n  }\n}\n\n/// 已下载的章节信息\nclass DownloadEp {\n  late String comicId;\n  late String id;\n  late String updatedAt;\n\n  late int epOrder;\n  late String title;\n\n  late bool fetchedPictures;\n  late int selectedPictureCount;\n  late int downloadPictureCount;\n  late bool downloadFinish;\n  late String downloadFinishTime;\n  late bool downloadFailed;\n\n  DownloadEp.fromJson(Map<String, dynamic> json) {\n    this.comicId = json[\"comicId\"];\n    this.id = json[\"id\"];\n    this.epOrder = json[\"epOrder\"];\n    this.title = json[\"title\"];\n\n    this.fetchedPictures = json[\"fetchedPictures\"];\n    this.selectedPictureCount = json[\"selectedPictureCount\"];\n    this.downloadPictureCount = json[\"downloadPictureCount\"];\n    this.downloadFinish = json[\"downloadFinish\"];\n    this.downloadFinishTime = json[\"downloadFinishTime\"];\n    this.downloadFailed = json[\"downloadFailed\"];\n  }\n}\n\n/// 游戏的分页\nclass GamePage extends Page {\n  late List<GameSimple> docs;\n\n  GamePage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.of(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.of(e))\n        .map((e) => GameSimple.fromJson(e))\n        .toList();\n  }\n}\n\n/// 游戏的简要信息\nclass GameSimple {\n  late String id;\n  late String title;\n  late String version;\n  late RemoteImageInfo icon;\n  late String publisher;\n  late bool adult;\n  late bool suggest;\n  late int likesCount;\n  late bool android;\n  late bool ios;\n\n  GameSimple.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.title = json[\"title\"];\n    this.version = json[\"version\"];\n    this.icon = RemoteImageInfo.fromJson(json[\"icon\"]);\n    this.publisher = json[\"publisher\"];\n    this.adult = json[\"adult\"];\n    this.suggest = json[\"suggest\"];\n    this.likesCount = json[\"likesCount\"];\n    this.android = json[\"android\"];\n    this.ios = json[\"ios\"];\n  }\n}\n\n/// 游戏详情\nclass GameInfo extends GameSimple {\n  late String description;\n  late String updateContent;\n  late String videoLink;\n  late List<RemoteImageInfo> screenshots;\n  late int commentsCount;\n  late int downloadsCount;\n  late bool isLiked;\n  late List<String> androidLinks;\n  late double androidSize;\n  late List<String> iosLinks;\n  late double iosSize;\n  late String updatedAt;\n  late String createdAt;\n\n  GameInfo.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.description = json[\"description\"];\n    this.updateContent = json[\"updateContent\"];\n    this.videoLink = json[\"videoLink\"];\n    this.screenshots = List.of(json[\"screenshots\"])\n        .map((e) => Map<String, dynamic>.of(e))\n        .map((e) => RemoteImageInfo.fromJson(e))\n        .toList();\n    this.commentsCount = json[\"commentsCount\"];\n    this.downloadsCount = json[\"downloadsCount\"];\n    this.isLiked = json[\"isLiked\"];\n    this.androidLinks = List.of(json[\"androidLinks\"]).map((e) => \"$e\").toList();\n    this.androidSize = double.parse(json[\"androidSize\"].toString());\n    this.iosLinks = List.of(json[\"iosLinks\"]).map((e) => \"$e\").toList();\n    this.iosSize = double.parse(json[\"iosSize\"].toString());\n    this.updatedAt = json[\"updated_at\"];\n    this.createdAt = json[\"created_at\"];\n  }\n}\n\n/// 我的评论页面分页\nclass MyCommentsPage extends Page {\n  late List<MyComment> docs;\n\n  MyCommentsPage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs =\n        List.of(json[\"docs\"]).map((e) => MyComment.fromJson(e)).toList();\n  }\n}\n\n/// 我的评论\nclass MyComment {\n  late String id;\n  late String content;\n  late bool hide;\n  late String createdAt;\n  late int likesCount;\n  late int commentsCount;\n  late bool isLiked;\n  late MyCommentComic comic;\n\n  MyComment.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.content = json[\"content\"];\n    this.hide = json[\"hide\"];\n    this.createdAt = json[\"created_at\"];\n    this.likesCount = json[\"likesCount\"];\n    this.commentsCount = json[\"commentsCount\"];\n    this.isLiked = json[\"isLiked\"];\n    this.comic = MyCommentComic.fromJson(json[\"_comic\"]);\n  }\n}\n\n/// 我的评论漫画简要信息\nclass MyCommentComic {\n  late String id;\n  late String title;\n\n  MyCommentComic.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"_id\"];\n    this.title = json[\"title\"];\n  }\n}\n\n/// 子评论分页\nclass CommentChildrenPage extends Page {\n  late List<CommentChild> docs;\n\n  CommentChildrenPage.fromJson(Map<String, dynamic> json)\n      : super.fromJson(json) {\n    this.docs = [];\n    if (json[\"docs\"] != null) {\n      docs.addAll(\n          List.of(json[\"docs\"]).map((e) => CommentChild.fromJson(e)).toList());\n    }\n  }\n}\n\n/// 子评论\nclass CommentChild extends ChildOfComment {\n  late String comic;\n\n  CommentChild.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.comic = json[\"_comic\"];\n  }\n}\n\n/// 漫画评论分页\nclass GameCommentPage extends Page {\n  late List<GameComment> docs;\n\n  GameCommentPage.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.docs = List.from(json[\"docs\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => GameComment.fromJson(e))\n        .toList();\n  }\n}\n\n/// 游戏评论\nclass GameComment extends CommentBase {\n  late String game;\n\n  GameComment.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.game = json[\"_game\"];\n  }\n}\n\n/// 子评论分页\nclass GameCommentChildrenPage extends Page {\n  late List<GameCommentChild> docs;\n\n  GameCommentChildrenPage.fromJson(Map<String, dynamic> json)\n      : super.fromJson(json) {\n    this.docs = [];\n    if (json[\"docs\"] != null) {\n      docs.addAll(List.of(json[\"docs\"])\n          .map((e) => GameCommentChild.fromJson(e))\n          .toList());\n    }\n  }\n}\n\n/// 子评论\nclass GameCommentChild extends ChildOfComment {\n  late String game;\n\n  GameCommentChild.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    this.game = json[\"_game\"];\n  }\n}\n\nclass Collection {\n  late String title;\n  late List<ComicSimple> comics;\n\n  Collection.fromJson(Map<String, dynamic> json) {\n    this.title = json[\"title\"];\n    this.comics = List.from(json[\"comics\"])\n        .map((e) => Map<String, dynamic>.from(e))\n        .map((e) => ComicSimple.fromJson(e))\n        .toList();\n  }\n}\n\nclass PkzArchive {\n  PkzArchive({\n    required this.coverPath,\n    required this.authorAvatarPath,\n    required this.comics,\n    required this.comicCount,\n    required this.volumesCount,\n    required this.chapterCount,\n    required this.pictureCount,\n  });\n\n  late final String coverPath;\n  late final String authorAvatarPath;\n  late final List<PkzComic> comics;\n  late final int comicCount;\n  late final int volumesCount;\n  late final int chapterCount;\n  late final int pictureCount;\n\n  PkzArchive.fromJson(Map<String, dynamic> json) {\n    coverPath = json['cover_path'];\n    authorAvatarPath = json['author_avatar_path'];\n    comics =\n        List.from(json['comics']).map((e) => PkzComic.fromJson(e)).toList();\n    comicCount = json['comic_count'];\n    volumesCount = json['volumes_count'];\n    chapterCount = json['chapter_count'];\n    pictureCount = json['picture_count'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['cover_path'] = coverPath;\n    _data['author_avatar_path'] = authorAvatarPath;\n    _data['comics'] = comics.map((e) => e.toJson()).toList();\n    _data['comic_count'] = comicCount;\n    _data['volumes_count'] = volumesCount;\n    _data['chapter_count'] = chapterCount;\n    _data['picture_count'] = pictureCount;\n    return _data;\n  }\n}\n\nclass PkzComic {\n  PkzComic({\n    required this.id,\n    required this.title,\n    required this.categories,\n    required this.tags,\n    required this.updatedAt,\n    required this.createdAt,\n    required this.description,\n    required this.chineseTeam,\n    required this.finished,\n    required this.coverPath,\n    required this.authorAvatarPath,\n    required this.volumes,\n    required this.volumesCount,\n    required this.chapterCount,\n    required this.pictureCount,\n    required this.idx,\n  });\n\n  late final String id;\n  late final String title;\n  late final List<String> categories;\n  late final List<String> tags;\n  late final int updatedAt;\n  late final int createdAt;\n  late final String description;\n  late final String chineseTeam;\n  late final bool finished;\n  late final String coverPath;\n  late final String authorAvatarPath;\n  late final List<PkzVolume> volumes;\n  late final int volumesCount;\n  late final int chapterCount;\n  late final int pictureCount;\n  late final int idx;\n  late final String author;\n  late final String authorId;\n\n  PkzComic.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    categories = List.castFrom<dynamic, String>(json['categories']);\n    tags = List.castFrom<dynamic, String>(json['tags']);\n    updatedAt = json['updated_at'];\n    createdAt = json['created_at'];\n    description = json['description'];\n    chineseTeam = json['chinese_team'];\n    finished = json['finished'];\n    coverPath = json['cover_path'];\n    authorAvatarPath = json['author_avatar_path'];\n    volumes =\n        List.from(json['volumes']).map((e) => PkzVolume.fromJson(e)).toList();\n    volumesCount = json['volumes_count'];\n    chapterCount = json['chapter_count'];\n    pictureCount = json['picture_count'];\n    idx = json['idx'];\n    author = json['author'];\n    authorId = json['author_id'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['title'] = title;\n    _data['categories'] = categories;\n    _data['tags'] = tags;\n    _data['updated_at'] = updatedAt;\n    _data['created_at'] = createdAt;\n    _data['description'] = description;\n    _data['chinese_team'] = chineseTeam;\n    _data['finished'] = finished;\n    _data['cover_path'] = coverPath;\n    _data['author_avatar_path'] = authorAvatarPath;\n    _data['volumes'] = volumes.map((e) => e.toJson()).toList();\n    _data['volumes_count'] = volumesCount;\n    _data['chapter_count'] = chapterCount;\n    _data['picture_count'] = pictureCount;\n    _data['idx'] = idx;\n    _data['author'] = author;\n    _data['author_id'] = authorId;\n    return _data;\n  }\n}\n\nclass PkzVolume {\n  PkzVolume({\n    required this.id,\n    required this.title,\n    required this.updatedAt,\n    required this.createdAt,\n    required this.coverPath,\n    required this.chapters,\n    required this.chapterCount,\n    required this.pictureCount,\n    required this.idx,\n  });\n\n  late final String id;\n  late final String title;\n  late final int updatedAt;\n  late final int createdAt;\n  late final String coverPath;\n  late final List<PkzChapter> chapters;\n  late final int chapterCount;\n  late final int pictureCount;\n  late final int idx;\n\n  PkzVolume.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    updatedAt = json['updated_at'];\n    createdAt = json['created_at'];\n    coverPath = json['cover_path'];\n    chapters =\n        List.from(json['chapters']).map((e) => PkzChapter.fromJson(e)).toList();\n    chapterCount = json['chapter_count'];\n    pictureCount = json['picture_count'];\n    idx = json['idx'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['title'] = title;\n    _data['updated_at'] = updatedAt;\n    _data['created_at'] = createdAt;\n    _data['cover_path'] = coverPath;\n    _data['chapters'] = chapters.map((e) => e.toJson()).toList();\n    _data['chapter_count'] = chapterCount;\n    _data['picture_count'] = pictureCount;\n    _data['idx'] = idx;\n    return _data;\n  }\n}\n\nclass PkzChapter {\n  PkzChapter({\n    required this.id,\n    required this.title,\n    required this.updatedAt,\n    required this.createdAt,\n    required this.coverPath,\n    required this.pictures,\n    required this.pictureCount,\n    required this.idx,\n  });\n\n  late final String id;\n  late final String title;\n  late final int updatedAt;\n  late final int createdAt;\n  late final String coverPath;\n  late final List<PkzPicture> pictures;\n  late final int pictureCount;\n  late final int idx;\n\n  PkzChapter.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    updatedAt = json['updated_at'];\n    createdAt = json['created_at'];\n    coverPath = json['cover_path'];\n    pictures =\n        List.from(json['pictures']).map((e) => PkzPicture.fromJson(e)).toList();\n    pictureCount = json['picture_count'];\n    idx = json['idx'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['title'] = title;\n    _data['updated_at'] = updatedAt;\n    _data['created_at'] = createdAt;\n    _data['cover_path'] = coverPath;\n    _data['pictures'] = pictures.map((e) => e.toJson()).toList();\n    _data['picture_count'] = pictureCount;\n    _data['idx'] = idx;\n    return _data;\n  }\n}\n\nclass PkzPicture {\n  PkzPicture({\n    required this.id,\n    required this.title,\n    required this.width,\n    required this.height,\n    required this.format,\n    required this.picturePath,\n    required this.idx,\n  });\n\n  late final String id;\n  late final String title;\n  late final int width;\n  late final int height;\n  late final String format;\n  late final String picturePath;\n  late final int idx;\n\n  PkzPicture.fromJson(Map<String, dynamic> json) {\n    id = json['id'];\n    title = json['title'];\n    width = json['width'];\n    height = json['height'];\n    format = json['format'];\n    picturePath = json['picture_path'];\n    idx = json['idx'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['title'] = title;\n    _data['width'] = width;\n    _data['height'] = height;\n    _data['format'] = format;\n    _data['picture_path'] = picturePath;\n    _data['idx'] = idx;\n    return _data;\n  }\n}\n\nclass Knight extends BasicUser {\n  late final String role;\n  late final String character;\n  late final int comicsUploaded;\n\n  Knight.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    role = json['role'];\n    character = json['character'];\n    comicsUploaded = json['comicsUploaded'];\n  }\n}\n\nclass PkzComicViewLog {\n  PkzComicViewLog({\n    required this.fileName,\n    required this.lastViewComicId,\n    required this.filePath,\n    required this.lastViewComicTitle,\n    required this.lastViewEpId,\n    required this.lastViewEpName,\n    required this.lastViewPictureRank,\n    required this.lastViewTime,\n  });\n\n  late final String fileName;\n  late final String lastViewComicId;\n  late final String filePath;\n  late final String lastViewComicTitle;\n  late final String lastViewEpId;\n  late final String lastViewEpName;\n  late final int lastViewPictureRank;\n  late final String lastViewTime;\n\n  PkzComicViewLog.fromJson(Map<String, dynamic> json) {\n    fileName = json['fileName'];\n    lastViewComicId = json['lastViewComicId'];\n    filePath = json['filePath'];\n    lastViewComicTitle = json['lastViewComicTitle'];\n    lastViewEpId = json['lastViewEpId'];\n    lastViewEpName = json['lastViewEpName'];\n    lastViewPictureRank = json['lastViewPictureRank'];\n    lastViewTime = json['lastViewTime'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['fileName'] = fileName;\n    _data['lastViewComicId'] = lastViewComicId;\n    _data['filePath'] = filePath;\n    _data['lastViewComicTitle'] = lastViewComicTitle;\n    _data['lastViewEpId'] = lastViewEpId;\n    _data['lastViewEpName'] = lastViewEpName;\n    _data['lastViewPictureRank'] = lastViewPictureRank;\n    _data['lastViewTime'] = lastViewTime;\n    return _data;\n  }\n}\n\nclass ProInfoAll {\n  ProInfoAll({\n    required this.proInfoAf,\n    required this.proInfoPat,\n  });\n\n  late final ProInfoAf proInfoAf;\n  late final ProInfoPat proInfoPat;\n\n  ProInfoAll.fromJson(Map<String, dynamic> json) {\n    proInfoAf = ProInfoAf.fromJson(json['pro_info_af']);\n    proInfoPat = ProInfoPat.fromJson(json['pro_info_pat']);\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['pro_info_normal'] = proInfoAf.toJson();\n    _data['pro_info_pat'] = proInfoPat.toJson();\n    return _data;\n  }\n}\n\nclass ProInfoAf {\n  ProInfoAf({\n    required this.isPro,\n    required this.expire,\n  });\n\n  late final bool isPro;\n  late final int expire;\n\n  ProInfoAf.fromJson(Map<String, dynamic> json) {\n    isPro = json['is_pro'];\n    expire = json['expire'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['is_pro'] = isPro;\n    _data['expire'] = expire;\n    return _data;\n  }\n}\n\nclass ProInfoPat {\n  ProInfoPat({\n    required this.isPro,\n    required this.patId,\n    required this.bindUid,\n    required this.requestDelete,\n    required this.reBind,\n    required this.errorType,\n    required this.errorMsg,\n    required this.accessKey,\n  });\n\n  late final bool isPro;\n  late final String patId;\n  late final String bindUid;\n  late final int requestDelete;\n  late final int reBind;\n  late final int errorType;\n  late final String errorMsg;\n  late final String accessKey;\n\n  ProInfoPat.fromJson(Map<String, dynamic> json) {\n    isPro = json['is_pro'];\n    patId = json['pat_id'];\n    bindUid = json['bind_uid'];\n    requestDelete = json['request_delete'];\n    reBind = json['re_bind'];\n    errorType = json['error_type'];\n    errorMsg = json['error_msg'];\n    accessKey = json['access_key'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['is_pro'] = isPro;\n    _data['pat_id'] = patId;\n    _data['bind_uid'] = bindUid;\n    _data['request_delete'] = requestDelete;\n    _data['re_bind'] = reBind;\n    _data['error_type'] = errorType;\n    _data['error_msg'] = errorMsg;\n    _data['access_key'] = accessKey;\n    return _data;\n  }\n}\n\nclass ForgotPasswordResult {\n  ForgotPasswordResult({\n    required this.question1,\n    required this.question2,\n    required this.question3,\n  });\n\n  late final String question1;\n  late final String question2;\n  late final String question3;\n\n  ForgotPasswordResult.fromJson(Map<String, dynamic> json) {\n    question1 = json['question1'];\n    question2 = json['question2'];\n    question3 = json['question3'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['question1'] = question1;\n    _data['question2'] = question2;\n    _data['question3'] = question3;\n    return _data;\n  }\n}\n\nclass ResetPasswordResult {\n  ResetPasswordResult({\n    required this.password,\n  });\n\n  late final String password;\n\n  ResetPasswordResult.fromJson(Map<String, dynamic> json) {\n    password = json['password'];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['password'] = password;\n    return _data;\n  }\n}\n\n/// 订阅\nclass ComicSubscribe {\n  late String id;\n  late String title;\n  late String author;\n  late int pagesCount;\n  late int epsCount;\n  late bool finished;\n  late String categories;\n  late String thumbOriginalName;\n  late String thumbFileServer;\n  late String thumbPath;\n  late String description;\n  late String chineseTeam;\n  late String tags;\n  late int likesCount;\n  late String subscribeTime;\n  late String updateSubscribeTime;\n  late int newEpCount;\n\n  ComicSubscribe.fromJson(Map<String, dynamic> json) {\n    print(json);\n    this.id = json[\"id\"];\n    this.title = json[\"title\"];\n    this.author = json[\"author\"];\n    this.pagesCount = json[\"pagesCount\"];\n    this.epsCount = json[\"epsCount\"];\n    this.finished = json[\"finished\"];\n    this.categories = json[\"categories\"];\n    this.thumbOriginalName = json[\"thumbOriginalName\"];\n    this.thumbFileServer = json[\"thumbFileServer\"];\n    this.thumbPath = json[\"thumbPath\"];\n    this.description = json[\"description\"];\n    this.chineseTeam = json[\"chineseTeam\"];\n    this.tags = json[\"tags\"];\n    this.likesCount = json[\"likesCount\"];\n    this.subscribeTime = json[\"subscribeTime\"];\n    this.updateSubscribeTime = json[\"updateSubscribeTime\"];\n    this.newEpCount = json[\"newEpCount\"];\n  }\n\n  Map<String, dynamic> toSimpleJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['_id'] = id;\n    _data['title'] = title;\n    _data['author'] = author;\n    _data['pagesCount'] = pagesCount;\n    _data['epsCount'] = epsCount;\n    _data['finished'] = finished;\n    _data['categories'] = jsonDecode(categories);\n    _data['thumbOriginalName'] = thumbOriginalName;\n    _data['thumbFileServer'] = thumbFileServer;\n    _data['thumbPath'] = thumbPath;\n    _data['description'] = description;\n    _data['chineseTeam'] = chineseTeam;\n    _data['tags'] = tags;\n    _data['likesCount'] = likesCount;\n    _data['thumb'] = jsonDecode(jsonEncode(RemoteImageInfo.fromJson({\n      \"originalName\": thumbOriginalName,\n      \"fileServer\": thumbFileServer,\n      \"path\": thumbPath\n    })));\n    _data['subscribeTime'] = subscribeTime;\n    _data['updateSubscribeTime'] = updateSubscribeTime;\n    _data['newEpCount'] = newEpCount;\n    return _data;\n  }\n}\n\n/// 本地收藏夹文件夹\nclass LocalFavoriteFolder {\n  late String id;\n  late String name;\n  late int createdAt;\n  late int updatedAt;\n  late int deletedAt;\n\n  LocalFavoriteFolder.fromJson(Map<String, dynamic> json) {\n    this.id = json[\"id\"];\n    this.name = json[\"name\"];\n    this.createdAt = json[\"createdAt\"];\n    this.updatedAt = json[\"updatedAt\"];\n    this.deletedAt = json[\"deletedAt\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['id'] = id;\n    _data['name'] = name;\n    _data['createdAt'] = createdAt;\n    _data['updatedAt'] = updatedAt;\n    _data['deletedAt'] = deletedAt;\n    return _data;\n  }\n}\n\n/// 本地收藏夹漫画\nclass LocalFavoriteComic {\n  late String comicId;\n  late String folderId;\n  String? info;\n  late int createdAt;\n  late int updatedAt;\n  late int deletedAt;\n\n  LocalFavoriteComic.fromJson(Map<String, dynamic> json) {\n    this.comicId = json[\"comicId\"];\n    this.folderId = json[\"folderId\"];\n    this.info = json[\"info\"];\n    this.createdAt = json[\"createdAt\"];\n    this.updatedAt = json[\"updatedAt\"];\n    this.deletedAt = json[\"deletedAt\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    final _data = <String, dynamic>{};\n    _data['comicId'] = comicId;\n    _data['folderId'] = folderId;\n    _data['info'] = info;\n    _data['createdAt'] = createdAt;\n    _data['updatedAt'] = updatedAt;\n    _data['deletedAt'] = deletedAt;\n    return _data;\n  }\n}\n"
  },
  {
    "path": "lib/basic/Method.dart",
    "content": "import 'dart:convert';\nimport 'dart:typed_data';\n\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Entities.dart';\n\n/// 使用MethodChannel与平台通信\n\nfinal method = Method._();\n\nclass Method {\n  /// 禁止其他页面构造此类\n  Method._();\n\n  /// channel\n  final MethodChannel _channel = const MethodChannel(\"method\");\n\n  /// 平铺调用, 为了直接与golang进行通信\n  Future<dynamic> _flatInvoke(String method, dynamic params) {\n    return _channel.invokeMethod(\"flatInvoke\", {\n      \"method\": method,\n      \"params\": params is String ? params : jsonEncode(params),\n    });\n  }\n\n  /// 读取配置文件\n  Future<String> loadProperty(String propertyName, String defaultValue) async {\n    return await _flatInvoke(\"loadProperty\", {\n      \"name\": propertyName,\n      \"defaultValue\": defaultValue,\n    });\n  }\n\n  /// 远程推荐链接（来自 pikapika-config）\n  Future<Map<String, String>> configLinks() async {\n    final rsp = await _flatInvoke(\"configLinks\", \"\");\n    final decoded = jsonDecode(rsp);\n    if (decoded is! Map) {\n      return {};\n    }\n    return decoded.map((key, value) => MapEntry(\"$key\", \"$value\"));\n  }\n\n  /// 应用配置（版本信息、下载地址等）\n  Future<Map<String, dynamic>> appConfig() async {\n    final rsp = await _flatInvoke(\"appConfig\", \"\");\n    final decoded = jsonDecode(rsp);\n    if (decoded is! Map) {\n      return {};\n    }\n    return Map<String, dynamic>.from(decoded);\n  }\n\n  /// 保存配置文件\n  Future<dynamic> saveProperty(String propertyName, String value) {\n    return _flatInvoke(\"saveProperty\", {\n      \"name\": propertyName,\n      \"value\": value,\n    });\n  }\n\n  /// 获取当前的分流\n  Future<String> getSwitchAddress() async {\n    return await _flatInvoke(\"getSwitchAddress\", \"\");\n  }\n\n  /// 更换分流\n  Future<dynamic> setSwitchAddress(String switchAddress) async {\n    return await _flatInvoke(\"setSwitchAddress\", switchAddress);\n  }\n\n  /// 获取当前的图片分流\n  Future<String> getImageSwitchAddress() async {\n    return await _flatInvoke(\"getImageSwitchAddress\", \"\");\n  }\n\n  /// 更换图片分流\n  Future<dynamic> setImageSwitchAddress(String switchAddress) async {\n    return await _flatInvoke(\"setImageSwitchAddress\", switchAddress);\n  }\n\n  Future<String> getUseApiClientLoadImage() async {\n    return await _flatInvoke(\"getUseApiClientLoadImage\", \"\");\n  }\n\n  Future<dynamic> setUseApiClientLoadImage(String switchAddress) async {\n    return await _flatInvoke(\"setUseApiClientLoadImage\", switchAddress);\n  }\n\n  /// 获取代理\n  Future<String> getProxy() async {\n    return await _flatInvoke(\"getProxy\", \"\");\n  }\n\n  /// 更换当前的代理\n  Future<dynamic> setProxy(String proxy) async {\n    return await _flatInvoke(\"setProxy\", proxy);\n  }\n\n  /// 获取用户名\n  Future<String> getUsername() async {\n    return await _flatInvoke(\"getUsername\", \"\");\n  }\n\n  /// 设置用户名\n  Future<dynamic> setUsername(String username) async {\n    return await _flatInvoke(\"setUsername\", username);\n  }\n\n  /// 获取密码\n  Future<String> getPassword() async {\n    return await _flatInvoke(\"getPassword\", \"\");\n  }\n\n  /// 设置密码\n  Future<dynamic> setPassword(String password) async {\n    return await _flatInvoke(\"setPassword\", password);\n  }\n\n  /// 预登录, 程序启用时会调用\n  /// 如果又账号密码或token, 且登录成功, 将返回true\n  Future<bool> preLogin() async {\n    String rsp = await _flatInvoke(\"preLogin\", \"\");\n    return rsp == \"true\";\n  }\n\n  /// 登录\n  Future<dynamic> login() async {\n    return _flatInvoke(\"login\", \"\");\n  }\n\n  /// 注册\n  Future<dynamic> register(\n      String email,\n      String name,\n      String password,\n      String gender,\n      String birthday,\n      String question1,\n      String answer1,\n      String question2,\n      String answer2,\n      String question3,\n      String answer3) {\n    return _flatInvoke(\"register\", {\n      \"email\": email,\n      \"name\": name,\n      \"password\": password,\n      \"gender\": gender,\n      \"birthday\": birthday,\n      \"question1\": question1,\n      \"answer1\": answer1,\n      \"question2\": question2,\n      \"answer2\": answer2,\n      \"question3\": question3,\n      \"answer3\": answer3,\n    });\n  }\n\n  /// 退出登录\n  Future<dynamic> clearToken() {\n    return _flatInvoke(\"clearToken\", \"\");\n  }\n\n  /// 获取用户自身基础信息\n  Future<UserProfile> userProfile() async {\n    String rsp = await _flatInvoke(\"userProfile\", \"\");\n    return UserProfile.fromJson(json.decode(rsp));\n  }\n\n  /// 打卡\n  Future<dynamic> punchIn() {\n    return _flatInvoke(\"punchIn\", \"\");\n  }\n\n  /// 使用服务器地址以及路径获取图片用户显示\n  /// 如果本地有缓存会返回路径, 如果本地没有缓存会下载再返回路径, 没有下载成功则会抛出异常\n  Future<RemoteImageData> remoteImageData(\n      String fileServer, String path) async {\n    var data = await _flatInvoke(\"remoteImageData\", {\n      \"fileServer\": fileServer,\n      \"path\": path,\n    });\n    return RemoteImageData.fromJson(json.decode(data));\n  }\n\n  /// 功能同上, 用于预加载\n  Future<dynamic> remoteImagePreload(String fileServer, String path) async {\n    return _flatInvoke(\"remoteImagePreload\", {\n      \"fileServer\": fileServer,\n      \"path\": path,\n    });\n  }\n\n  /// 获取已经下载好图片的保存位置\n  Future<String> downloadImagePath(String path) async {\n    return await _flatInvoke(\"downloadImagePath\", path);\n  }\n\n  /// 获取分类\n  Future<List<Category>> categories() async {\n    String rsp = await _flatInvoke(\"categories\", \"\");\n    List list = json.decode(rsp);\n    return list.map((e) => Category.fromJson(e)).toList();\n  }\n\n  /// 列出漫画\n  /// [sort] 排序方式\n  /// [page] 页数\n  /// [category] 分类\n  /// [tag] 标签\n  /// [creatorId] 创建人ID\n  /// [chineseTeam] 汉化组名称\n  /// * 几种条件使用且的关系\n  Future<ComicsPage> comics(\n    String sort,\n    int page, {\n    String category = \"\",\n    String tag = \"\",\n    String author = \"\",\n    String creatorId = \"\",\n    String chineseTeam = \"\",\n  }) async {\n    String rsp = await _flatInvoke(\"comics\", {\n      \"category\": category,\n      \"tag\": tag,\n      \"author\": author,\n      \"creatorId\": creatorId,\n      \"chineseTeam\": chineseTeam,\n      \"sort\": sort,\n      \"page\": page,\n    });\n    return ComicsPage.fromJson(json.decode(rsp));\n  }\n\n  /// 搜索漫画\n  Future<ComicsPage> searchComics(String keyword, String sort, int page) {\n    return searchComicsInCategories(keyword, sort, page, []);\n  }\n\n  /// 搜索漫画, 在多个分类中\n  Future<ComicsPage> searchComicsInCategories(\n      String keyword, String sort, int page, List<String> categories) async {\n    String rsp = await _flatInvoke(\"searchComics\", {\n      \"keyword\": keyword,\n      \"sort\": sort,\n      \"page\": page,\n      \"categories\": categories,\n    });\n    return ComicsPage.fromJson(json.decode(rsp));\n  }\n\n  /// 随机漫画\n  Future<List<ComicSimple>> randomComics() async {\n    String data = await _flatInvoke(\"randomComics\", \"\");\n    return List.of(jsonDecode(data))\n        .map((e) => Map<String, dynamic>.of(e))\n        .map((e) => ComicSimple.fromJson(e))\n        .toList();\n  }\n\n  /// 漫画榜单\n  /// [type] 榜单类型 H24 D7 D30\n  Future<List<ComicSimple>> leaderboard(String type) async {\n    String data = await _flatInvoke(\"leaderboard\", type);\n    return List.of(jsonDecode(data))\n        .map((e) => Map<String, dynamic>.of(e))\n        .map((e) => ComicSimple.fromJson(e))\n        .toList();\n  }\n\n  /// 获取漫画详情\n  Future<ComicInfo> comicInfo(String comicId, bool ignoreHistory) async {\n    String rsp =\n        await _flatInvoke(ignoreHistory ? \"comicInfoA\" : \"comicInfo\", comicId);\n    return ComicInfo.fromJson(json.decode(rsp));\n  }\n\n  /// 分页获取漫画的章节\n  Future<EpPage> comicEpPage(String comicId, int page) async {\n    String rsp = await _flatInvoke(\"comicEpPage\", {\n      \"comicId\": comicId,\n      \"page\": page,\n    });\n    return EpPage.fromJson(json.decode(rsp));\n  }\n\n  /// 分页获取一个章节的图片, 并且需要图片的质量参数\n  Future<PicturePage> comicPicturePageWithQuality(\n      String comicId, int epOrder, int page, String quality) async {\n    String data = await _flatInvoke(\"comicPicturePageWithQuality\", {\n      \"comicId\": comicId,\n      \"epOrder\": epOrder,\n      \"page\": page,\n      \"quality\": quality,\n    });\n    return PicturePage.fromJson(json.decode(data));\n  }\n\n  /// 对漫画进行点赞/取消点赞操作\n  Future<String> switchLike(String comicId) async {\n    return await _flatInvoke(\"switchLike\", comicId);\n  }\n\n  /// 对漫画进行收藏/取消收藏操作\n  Future<String> switchFavourite(String comicId) async {\n    return await _flatInvoke(\"switchFavourite\", comicId);\n  }\n\n  /// 收藏漫画列表\n  Future<ComicsPage> favouriteComics(String sort, int page) async {\n    var rsp = await _flatInvoke(\"favouriteComics\", {\n      \"sort\": sort,\n      \"page\": page,\n    });\n    return ComicsPage.fromJson(json.decode(rsp));\n  }\n\n  /// 看了此漫画的人还看了...\n  Future<List<ComicSimple>> recommendation(String comicId) async {\n    String rsp = await _flatInvoke(\"recommendation\", comicId);\n    List list = json.decode(rsp);\n    return list.map((e) => ComicSimple.fromJson(e)).toList();\n  }\n\n  /// 对漫画发送评论\n  Future<dynamic> postComment(String comicId, String content) {\n    return _flatInvoke(\"postComment\", {\n      \"comicId\": comicId,\n      \"content\": content,\n    });\n  }\n\n  /// 发送子评论\n  Future<dynamic> postChildComment(String commentId, String content) {\n    return _flatInvoke(\"postChildComment\", {\n      \"commentId\": commentId,\n      \"content\": content,\n    });\n  }\n\n  /// 漫画的评论列表\n  Future<CommentPage> comments(String comicId, int page) async {\n    var rsp = await _flatInvoke(\"comments\", {\n      \"comicId\": comicId,\n      \"page\": page,\n    });\n    return CommentPage.fromJson(json.decode(rsp));\n  }\n\n  /// 拉取子评论\n  Future<CommentChildrenPage> commentChildren(\n    String comicId,\n    String commentId,\n    int page,\n  ) async {\n    var rsp = await _flatInvoke(\"commentChildren\", {\n      \"comicId\": comicId,\n      \"commentId\": commentId,\n      \"page\": page,\n    });\n    return CommentChildrenPage.fromJson(json.decode(rsp));\n  }\n\n  /// 喜欢/取消喜欢 一条评论\n  Future switchLikeComment(String commentId, String comicId) {\n    return _flatInvoke(\"switchLikeComment\", {\n      \"commentId\": commentId,\n      \"comicId\": comicId,\n    });\n  }\n\n  /// 我的评论列表\n  Future<MyCommentsPage> myComments(int page) async {\n    String response = await _flatInvoke(\"myComments\", \"$page\");\n    return MyCommentsPage.fromJson(jsonDecode(response));\n  }\n\n  /// 浏览记录\n  Future<List<ViewLog>> viewLogPage(int offset, int limit) async {\n    var data = await _flatInvoke(\"viewLogPage\", {\n      \"offset\": offset,\n      \"limit\": limit,\n    });\n    List list = json.decode(data);\n    return list.map((e) => ViewLog.fromJson(e)).toList();\n  }\n\n  /// 清除所有的浏览记录\n  Future<dynamic> clearAllViewLog() {\n    return _flatInvoke(\"clearAllViewLog\", \"\");\n  }\n\n  /// 删除一个漫画的浏览记录\n  Future<dynamic> deleteViewLog(String id) {\n    return _flatInvoke(\"deleteViewLog\", id);\n  }\n\n  /// 删除一个漫画的浏览记录\n  Future<dynamic> viewComic(String id) {\n    return _flatInvoke(\"viewComic\", id);\n  }\n\n  /// 游戏列表\n  Future<GamePage> games(int page) async {\n    var data = await _flatInvoke(\"games\", \"$page\");\n    return GamePage.fromJson(json.decode(data));\n  }\n\n  /// 游戏详情\n  Future<GameInfo> game(String gameId) async {\n    var data = await _flatInvoke(\"game\", gameId);\n    return GameInfo.fromJson(json.decode(data));\n  }\n\n  /// 游戏的评论列表\n  Future<GameCommentPage> gameComments(String gameId, int page) async {\n    var rsp = await _flatInvoke(\"gameComments\", {\n      \"gameId\": gameId,\n      \"page\": page,\n    });\n    return GameCommentPage.fromJson(json.decode(rsp));\n  }\n\n  /// 对游戏发送评论\n  Future<dynamic> postGameComment(String gameId, String content) {\n    return _flatInvoke(\"postGameComment\", {\n      \"gameId\": gameId,\n      \"content\": content,\n    });\n  }\n\n  /// 拉取游戏子评论\n  Future<GameCommentChildrenPage> gameCommentChildren(\n    String gameId,\n    String commentId,\n    int page,\n  ) async {\n    var rsp = await _flatInvoke(\"gameCommentChildren\", {\n      \"gameId\": gameId,\n      \"commentId\": commentId,\n      \"page\": page,\n    });\n    return GameCommentChildrenPage.fromJson(json.decode(rsp));\n  }\n\n  /// 喜欢/取消喜欢 一条游戏评论\n  Future switchLikeGameComment(String commentId, String gameId) {\n    return _flatInvoke(\"switchLikeGameComment\", {\n      \"commentId\": commentId,\n      \"gameId\": gameId,\n    });\n  }\n\n  /// 发送游戏子评论\n  Future<dynamic> postGameChildComment(String commentId, String content) {\n    return _flatInvoke(\"postGameChildComment\", {\n      \"commentId\": commentId,\n      \"content\": content,\n    });\n  }\n\n  /// 清理网络缓存\n  Future cleanNetworkCache() {\n    return _flatInvoke(\"cleanNetworkCache\", \"\");\n  }\n\n  /// 清理图片缓存\n  Future cleanImageCache() {\n    return _flatInvoke(\"cleanImageCache\", \"\");\n  }\n\n  /// 清理缓存\n  Future clean() {\n    return _flatInvoke(\"clean\", \"\");\n  }\n\n  /// 清理[expireSec]秒以前的缓存\n  Future autoClean(String expireSec) {\n    return _flatInvoke(\"autoClean\", expireSec);\n  }\n\n  /// 保存当前浏览器的进度\n  Future storeViewEp(\n      String comicId, int epOrder, String epTitle, int pictureRank) {\n    return _flatInvoke(\"storeViewEp\", {\n      \"comicId\": comicId,\n      \"epOrder\": epOrder,\n      \"epTitle\": epTitle,\n      \"pictureRank\": pictureRank,\n    });\n  }\n\n  /// 加载浏览进度\n  Future<ViewLog?> loadView(String comicId) async {\n    String data = await _flatInvoke(\"loadView\", comicId);\n    if (data == \"\") {\n      return null;\n    }\n    return ViewLog.fromJson(jsonDecode(data));\n  }\n\n  /// 下载是否在后台运行\n  Future<bool> downloadRunning() async {\n    String rsp = await _flatInvoke(\"downloadRunning\", \"\");\n    return rsp == \"true\";\n  }\n\n  /// 暂停/继续 下载\n  Future<dynamic> setDownloadRunning(bool status) async {\n    return _flatInvoke(\"setDownloadRunning\", \"$status\");\n  }\n\n  /// 下载漫画\n  Future<dynamic> createDownload(\n      Map<String, dynamic> comic, List<Map<String, dynamic>> epList) async {\n    return _flatInvoke(\"createDownload\", {\n      \"comic\": comic,\n      \"epList\": epList,\n    });\n  }\n\n  /// 追加下载的章节\n  Future<dynamic> addDownload(\n      Map<String, dynamic> comic, List<Map<String, dynamic>> epList) async {\n    await _flatInvoke(\"addDownload\", {\n      \"comic\": comic,\n      \"epList\": epList,\n    });\n  }\n\n  /// 下载详情\n  Future<DownloadComic?> loadDownloadComic(String comicId) async {\n    var data = await _flatInvoke(\"loadDownloadComic\", comicId);\n    // 未找到 且 未异常\n    if (data == \"\") {\n      return null;\n    }\n    return DownloadComic.fromJson(json.decode(data));\n  }\n\n  /// 所有下载\n  Future<List<DownloadComic>> allDownloads(String search,\n      {String customFolder = \"\"}) async {\n    var data = await _flatInvoke(\"allDownloads\", {\n      \"search\": search,\n      \"customFolder\": customFolder,\n    });\n    data = jsonDecode(data);\n    if (data == null) {\n      return [];\n    }\n    List list = data;\n    return list.map((e) => DownloadComic.fromJson(e)).toList();\n  }\n\n  Future<List<String>> allCustomFolders() async {\n    var data = await _flatInvoke(\"allCustomFolders\", \"\");\n    return List.of(jsonDecode(data)).map((e) => e.toString()).toList();\n  }\n\n  /// 删除一个下载\n  Future<dynamic> deleteDownloadComic(String comicId) async {\n    return _flatInvoke(\"deleteDownloadComic\", comicId);\n  }\n\n  Future<dynamic> moveDownloadComic(\n      List<String> comicIdList, String customFolder) {\n    return _flatInvoke(\"moveDownloadComic\", {\n      \"comicIdList\": comicIdList,\n      \"customFolder\": customFolder,\n    });\n  }\n\n  /// 所有下载的EP\n  Future<List<DownloadEp>> downloadEpList(String comicId) async {\n    var data = await _flatInvoke(\"downloadEpList\", comicId);\n    List list = json.decode(data);\n    return list.map((e) => DownloadEp.fromJson(e)).toList();\n  }\n\n  /// 下载漫画这个EP下的图片\n  Future<List<DownloadPicture>> downloadPicturesByEpId(String epId) async {\n    var data = await _flatInvoke(\"downloadPicturesByEpId\", epId);\n    List list = json.decode(data);\n    return list.map((e) => DownloadPicture.fromJson(e)).toList();\n  }\n\n  /// 重置所有下载失败的漫画\n  Future<dynamic> resetFailed() async {\n    return _flatInvoke(\"resetAllDownloads\", \"\");\n  }\n\n  /// 导出下载的漫画到zip\n  Future<dynamic> exportComicDownload(String comicId, String dir, String name) {\n    return _flatInvoke(\"exportComicDownload\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的漫画到pki\n  Future<dynamic> exportComicDownloadToPki(\n      String comicId, String dir, String name) {\n    return _flatInvoke(\"exportComicDownloadToPki\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到HTML+JPG (即使没有下载完成)\n  Future<dynamic> exportComicJpegsEvenNotFinish(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicJpegsEvenNotFinish\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到HTML+JPG\n  Future<dynamic> exportComicDownloadToJPG(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadToJPG\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到HTML+JPG\n  Future<dynamic> exportComicDownloadToPDF(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadToPDF\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到HTML+JPG\n  Future<dynamic> exportComicDownloadToEpub(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadToEpub\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到PDF文件夹\n  Future<dynamic> exportComicDownloadToPDFFolder(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadToPDFFolder\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到HTML+JPG\n  Future<dynamic> exportComicDownloadJpegZip(\n    String comicId,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadJpegZip\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出下载的图片到PKZ\n  Future<dynamic> exportComicDownloadToPkz(\n    List<String> comicIds,\n    String dir,\n    String name,\n  ) {\n    return _flatInvoke(\"exportComicDownloadToPkz\", {\n      \"comicIds\": comicIds,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导出zip\n  Future<dynamic> exportAnyComicDownloadsToZip(\n    List<String> comicIds,\n    String dir,\n  ) {\n    return _flatInvoke(\"exportAnyComicDownloadsToZip\", {\n      \"comicIds\": comicIds,\n      \"dir\": dir,\n    });\n  }\n\n  /// 导出pki\n  Future<dynamic> exportAnyComicDownloadsToPki(\n    List<String> comicIds,\n    String dir,\n  ) {\n    return _flatInvoke(\"exportAnyComicDownloadsToPki\", {\n      \"comicIds\": comicIds,\n      \"dir\": dir,\n    });\n  }\n\n  /// 导出下载的漫画到cbzs.zip\n  Future<dynamic> exportComicDownloadToCbzsZip(\n      String comicId, String dir, String name) {\n    return _flatInvoke(\"exportComicDownloadToCbzsZip\", {\n      \"comicId\": comicId,\n      \"dir\": dir,\n      \"name\": name,\n    });\n  }\n\n  /// 导入文件夹所有的文件\n  Future<dynamic> importComicDownloadDir(\n    String dir,\n  ) {\n    return _flatInvoke(\"importComicDownloadDir\", dir);\n  }\n\n  /// 使用网络将下载传输到其他设备\n  Future<int> exportComicUsingSocket(String comicId) async {\n    return int.parse(await _flatInvoke(\"exportComicUsingSocket\", comicId));\n  }\n\n  /// 传输窗口关闭时调用, 令socket关闭(如果传输没有结束)\n  Future<dynamic> exportComicUsingSocketExit() {\n    return _flatInvoke(\"exportComicUsingSocketExit\", \"\");\n  }\n\n  /// 从zip导入漫画\n  Future<dynamic> importComicDownload(String zipPath) {\n    return _flatInvoke(\"importComicDownload\", zipPath);\n  }\n\n  /// 从pki导入漫画\n  Future<dynamic> importComicDownloadPki(String zipPath) {\n    return _flatInvoke(\"importComicDownloadPki\", zipPath);\n  }\n\n  /// 从网络接收漫画\n  Future<dynamic> importComicDownloadUsingSocket(String addr) {\n    return _flatInvoke(\"importComicDownloadUsingSocket\", addr);\n  }\n\n  /// 获取本机的所有ip地址\n  Future<String> clientIpSet() async {\n    return await _flatInvoke(\"clientIpSet\", \"\");\n  }\n\n  /// 获取一个游戏的下载地址\n  Future<List<String>> downloadGame(String url) async {\n    if (url.startsWith(\"https://game.eroge.xyz/hhh.php\")) {\n      var data = await _flatInvoke(\"downloadGame\", url);\n      return [\n        url,\n        ...List.of(jsonDecode(data)).map((e) => e.toString()),\n      ];\n    }\n    return [url];\n  }\n\n  /// 保存图片(ios)\n  Future<dynamic> iosSaveFileToImage(String path) async {\n    return _channel.invokeMethod(\"iosSaveFileToImage\", {\n      \"path\": path,\n    });\n  }\n\n  /// 保存图片(android)\n  Future androidSaveFileToImage(String path) async {\n    return _channel.invokeMethod(\"androidSaveFileToImage\", {\n      \"path\": path,\n    });\n  }\n\n  /// 保存图片(PC)\n  Future convertImageToJPEG100(String path, String dir) async {\n    return _flatInvoke(\"convertImageToJPEG100\", {\n      \"path\": path,\n      \"dir\": dir,\n    });\n  }\n\n  /// 获取安卓的屏幕刷新率\n  Future<List<String>> loadAndroidModes() async {\n    return List.of(await _channel.invokeMethod(\"androidGetModes\"))\n        .map((e) => \"$e\")\n        .toList();\n  }\n\n  /// 设置安卓的屏幕刷新率\n  Future setAndroidMode(String androidDisplayMode) {\n    return _channel\n        .invokeMethod(\"androidSetMode\", {\"mode\": androidDisplayMode});\n  }\n\n  /// 获取安卓的版本\n  Future<int> androidGetVersion() async {\n    return await _channel.invokeMethod(\"androidGetVersion\", {});\n  }\n\n  /// 数据文件保存位置\n  Future<String> dataLocal() async {\n    return await _channel.invokeMethod(\"dataLocal\", {});\n  }\n\n  /// 获取安卓支持的文件保存路径\n  Future<List<String>> androidGetExtendDirs() async {\n    String? tmp = await _channel.invokeMethod(\"androidGetExtendDirs\", {});\n    if (tmp != null && tmp.isNotEmpty) {\n      return tmp.split(\"|\");\n    }\n    return [];\n  }\n\n  /// 安卓文件迁移\n  Future migrate(String path) async {\n    return _channel.invokeMethod(\"migrate\", {\"path\": path});\n  }\n\n  /// 下载的同时导出-配置获取\n  Future loadDownloadAndExportPath() {\n    return _flatInvoke(\"loadDownloadAndExportPath\", \"\");\n  }\n\n  /// 下载的同时导出-设置\n  Future saveDownloadAndExportPath(String folder) {\n    return _flatInvoke(\"saveDownloadAndExportPath\", folder);\n  }\n\n  /// 使用下载缓存-配置获取\n  Future loadDownloadCachePath() {\n    return _flatInvoke(\"loadDownloadCachePath\", \"\");\n  }\n\n  /// 使用下载缓存-设置\n  Future saveDownloadCachePath(String folder) {\n    return _flatInvoke(\"saveDownloadCachePath\", folder);\n  }\n\n  /// 切换安全策略 (安卓禁止截图, 录屏, 不显示在任务视图)\n  Future androidSecureFlag(bool flag) {\n    return _channel.invokeMethod(\"androidSecureFlag\", {\n      \"flag\": flag,\n    });\n  }\n\n  /// 获取下载线程数量\n  Future<int> loadDownloadThreadCount() async {\n    var strValue = await _flatInvoke(\"loadDownloadThreadCount\", {});\n    return int.parse(strValue);\n  }\n\n  /// 设置下载线程数\n  Future saveDownloadThreadCount(int value) {\n    return _flatInvoke(\"saveDownloadThreadCount\", \"$value\");\n  }\n\n  /// HTTP-GET-STRING\n  Future<String> defaultHttpClientGet(String url) async {\n    return await _flatInvoke(\"defaultHttpClientGet\", url);\n  }\n\n  /// 更新签名\n  Future updateSlogan(String input) async {\n    return await _flatInvoke(\"updateSlogan\", input);\n  }\n\n  /// 更新签名\n  Future updateAvatar(String data) async {\n    return await _flatInvoke(\"updateAvatar\", data);\n  }\n\n  /// 修改密码\n  Future updatePassword(String oldPassword, String newPassword) {\n    return _flatInvoke(\"updatePassword\", {\n      \"oldPassword\": oldPassword,\n      \"newPassword\": newPassword,\n    });\n  }\n\n  Future<List<String>> loadViewedList(List<String> list) async {\n    return List.of(jsonDecode(await _flatInvoke(\"loadViewedList\", list)))\n        .cast();\n  }\n\n  Future<List<Collection>> collections() async {\n    String rsp = await _flatInvoke(\"collections\", \"\");\n    List list = json.decode(rsp);\n    return list.map((e) => Collection.fromJson(e)).toList();\n  }\n\n  Future<bool> verifyAuthentication() async {\n    return await _channel.invokeMethod(\"verifyAuthentication\");\n  }\n\n  Future<PkzArchive> pkzInfo(String pkzPath) async {\n    return PkzArchive.fromJson(\n        jsonDecode(await _flatInvoke(\"pkzInfo\", pkzPath)));\n  }\n\n  Future<Uint8List> loadPkzFile(String pkzPath, String path) async {\n    return base64Decode(await _flatInvoke(\"loadPkzFile\", {\n      \"pkzPath\": pkzPath,\n      \"path\": path,\n    }));\n  }\n\n  Future<List<PkzComicViewLog>> pkzComicViewLogs(\n    String fileName,\n    String comicId,\n  ) async {\n    return List.of(jsonDecode(await _flatInvoke(\"pkzComicViewLogs\", fileName)))\n        .map((e) => PkzComicViewLog.fromJson(e))\n        .toList();\n  }\n\n  Future<PkzComicViewLog?> pkzComicViewLogByPkzNameAndId(\n    String fileName,\n    String comicId,\n  ) async {\n    String data = await _flatInvoke(\"pkzComicViewLogByPkzNameAndId\", {\n      \"fileName\": fileName,\n      \"comicId\": comicId,\n    });\n    if (data == \"\" || data == \"nil\" || data == \"null\") {\n      return null;\n    }\n    return PkzComicViewLog.fromJson(jsonDecode(data));\n  }\n\n  Future viewPkz(\n    String fileName,\n    String filePath,\n  ) async {\n    return _flatInvoke(\"viewPkz\", {\n      \"fileName\": fileName,\n      \"filePath\": filePath,\n    });\n  }\n\n  Future viewPkzComic(\n    String fileName,\n    String filePath,\n    String comicId,\n    String comicTitle,\n  ) async {\n    return _flatInvoke(\"viewPkzComic\", {\n      \"fileName\": fileName,\n      \"filePath\": filePath,\n      \"comicId\": comicId,\n      \"comicTitle\": comicTitle,\n    });\n  }\n\n  Future viewPkzEpAndPicture(\n    String fileName,\n    String filePath,\n    String comicId,\n    String comicTitle,\n    String epId,\n    String epTitle,\n    int pictureRank,\n  ) async {\n    return _flatInvoke(\"viewPkzEpAndPicture\", {\n      \"fileName\": fileName,\n      \"filePath\": filePath,\n      \"comicId\": comicId,\n      \"comicTitle\": comicTitle,\n      \"epId\": epId,\n      \"epTitle\": epTitle,\n      \"pictureRank\": pictureRank,\n    });\n  }\n\n  Future<List<Knight>> leaderboardOfKnight() async {\n    return List.of(jsonDecode(await _flatInvoke(\"leaderboardOfKnight\", \"\")))\n        .map((e) => Knight.fromJson(e))\n        .toList();\n  }\n\n  Future<ProInfoAll> proInfoAll() async {\n    return ProInfoAll.fromJson(jsonDecode(await _flatInvoke(\"proInfoAll\", \"\")));\n  }\n\n  Future reloadPro() {\n    return _flatInvoke(\"reloadPro\", \"\");\n  }\n\n  Future inputCdKey(String cdKey) {\n    return _flatInvoke(\"inputCdKey\", cdKey);\n  }\n\n  Future reloadSwitchAddress() {\n    return _flatInvoke(\"reloadSwitchAddress\", \"\");\n  }\n\n  Future resetSwitchAddress() {\n    return _flatInvoke(\"resetSwitchAddress\", \"\");\n  }\n\n  Future<String> iosGetDocumentDir() async {\n    return await _channel.invokeMethod('iosGetDocumentDir', '');\n  }\n\n  /// 找回密码1\n  Future<ForgotPasswordResult> forgotPassword(email) async {\n    String data = await _flatInvoke(\"forgotPassword\", email);\n    return ForgotPasswordResult.fromJson(jsonDecode(data));\n  }\n\n  /// 找回密码2\n  Future<ResetPasswordResult> resetPassword(\n    String email,\n    int questionNo,\n    String answer,\n  ) async {\n    String data = await _flatInvoke(\"resetPassword\", {\n      \"email\": email,\n      \"questionNo\": questionNo,\n      \"answer\": answer,\n    });\n    return ResetPasswordResult.fromJson(jsonDecode(data));\n  }\n\n  Future mergeHistoriesFromWebDav(\n    String root,\n    String username,\n    String password,\n    String file,\n    String direction,\n  ) {\n    return _flatInvoke(\"mergeHistoriesFromWebDav\", {\n      \"root\": root,\n      \"username\": username,\n      \"password\": password,\n      \"file\": file,\n      \"direction\": direction,\n    });\n  }\n\n  Future mergeHistoriesFromLocal(String localPath) {\n    return _flatInvoke(\"mergeHistoriesFromLocal\", localPath);\n  }\n\n  Future<int> ping(String idx) async {\n    String ms = await _flatInvoke(\"ping\", idx);\n    return int.parse(ms);\n  }\n\n  Future<int> pingImg(String idx) async {\n    String ms = await _flatInvoke(\"pingImg\", idx);\n    return int.parse(ms);\n  }\n\n  Future<String> androidStorageRoot() async {\n    return await _channel.invokeMethod(\"androidStorageRoot\");\n  }\n\n  Future importComicViewFormOff(String dbPath) {\n    return _flatInvoke(\"importComicViewFormOff\", dbPath);\n  }\n\n  Future startWebServer() {\n    return _flatInvoke(\"startWebServer\", \"\");\n  }\n\n  Future stopWebServer() {\n    return _flatInvoke(\"stopWebServer\", \"\");\n  }\n\n  Future<String> androidDefaultExportsDir() async {\n    return await _channel.invokeMethod(\"androidDefaultExportsDir\");\n  }\n\n  Future getHomeDir() {\n    return _flatInvoke(\"getHomeDir\", \"\");\n  }\n\n  Future mkdirs(String path) {\n    return _flatInvoke(\"mkdirs\", path);\n  }\n\n  Future androidMkdirs(String path) async {\n    return await _channel.invokeMethod(\"androidMkdirs\", path);\n  }\n\n  Future downloadAll(List<String> comicIds) {\n    return _flatInvoke(\"downloadAll\", comicIds);\n  }\n\n  Future setPatAccessKey(String accessKey) {\n    return _flatInvoke(\"setPatAccessKey\", accessKey);\n  }\n\n  Future reloadPatAccount() {\n    return _flatInvoke(\"reloadPatAccount\", \"\");\n  }\n\n  Future bindThisAccount() {\n    return _flatInvoke(\"bindThisAccount\", \"\");\n  }\n\n  Future clearPat() {\n    return _flatInvoke(\"clearPat\", \"\");\n  }\n\n  Future<String> getProServerName() async {\n    return await _flatInvoke(\"getProServerName\", \"\");\n  }\n\n  Future<dynamic> setProServerName(String serverName) {\n    return _flatInvoke(\"setProServerName\", serverName);\n  }\n\n  /// 加载已订阅\n  Future<ComicSubscribe?> loadSubscribed(String comicId) async {\n    String data = await _flatInvoke(\"loadSubscribed\", comicId);\n    if (data == \"\") {\n      return null;\n    }\n    return ComicSubscribe.fromJson(jsonDecode(data));\n  }\n\n  Future addSubscribed(String comicId) async {\n    return _flatInvoke(\"addSubscribed\", comicId);\n  }\n\n  Future removeAllSubscribed() async {\n    return _flatInvoke(\"removeAllSubscribed\", \"\");\n  }\n\n  Future removeSubscribed(String comicId) async {\n    return _flatInvoke(\"removeSubscribed\", comicId);\n  }\n\n  Future<List<ComicSubscribe>> allSubscribed() async {\n    var data = await _flatInvoke(\"allSubscribed\", \"\");\n    List list = json.decode(data);\n    return list.map((e) => ComicSubscribe.fromJson(e)).toList();\n  }\n\n  Future updateSubscribed() async {\n    return _flatInvoke(\"updateSubscribed\", \"\");\n  }\n\n  Future updateSubscribedForce() async {\n    return _flatInvoke(\"updateSubscribedForce\", \"\");\n  }\n\n  Future<List<String>> fontList() async {\n    return await _channel.invokeMethod(\n        \"fontList\", {}).then((value) => List.of(value).cast<String>());\n  }\n\n  // 本地收藏夹方法\n  Future<LocalFavoriteFolder> createLocalFavoriteFolder(String name) async {\n    String data =\n        await _flatInvoke(\"createLocalFavoriteFolder\", {\"name\": name});\n    return LocalFavoriteFolder.fromJson(jsonDecode(data));\n  }\n\n  Future updateLocalFavoriteFolder(LocalFavoriteFolder folder) async {\n    return _flatInvoke(\"updateLocalFavoriteFolder\", folder.toJson());\n  }\n\n  Future deleteLocalFavoriteFolder(String folderId) async {\n    return _flatInvoke(\"deleteLocalFavoriteFolder\", folderId);\n  }\n\n  Future<LocalFavoriteFolder> getLocalFavoriteFolder(String folderId) async {\n    String data = await _flatInvoke(\"getLocalFavoriteFolder\", folderId);\n    return LocalFavoriteFolder.fromJson(jsonDecode(data));\n  }\n\n  Future<List<LocalFavoriteFolder>> listLocalFavoriteFolders() async {\n    String data = await _flatInvoke(\"listLocalFavoriteFolders\", \"\");\n    List list = jsonDecode(data);\n    return list.map((e) => LocalFavoriteFolder.fromJson(e)).toList();\n  }\n\n  Future<int> countLocalFavoriteFolders() async {\n    String data = await _flatInvoke(\"countLocalFavoriteFolders\", \"\");\n    return int.parse(data);\n  }\n\n  Future addLocalFavoriteComic(String comicId, String folderId,\n      {String info = \"\"}) async {\n    return _flatInvoke(\"addLocalFavoriteComic\", {\n      \"comicId\": comicId,\n      \"folderId\": folderId,\n      \"info\": info,\n    });\n  }\n\n  Future removeLocalFavoriteComic(String comicId) async {\n    return _flatInvoke(\"removeLocalFavoriteComic\", comicId);\n  }\n\n  Future moveLocalFavoriteComics(List<String> comicIds, String folderId) async {\n    return _flatInvoke(\"moveLocalFavoriteComics\", {\n      \"comicIds\": comicIds,\n      \"folderId\": folderId,\n    });\n  }\n\n  Future<LocalFavoriteComic?> getLocalFavoriteComic(String comicId) async {\n    try {\n      String data = await _flatInvoke(\"getLocalFavoriteComic\", comicId);\n      if (data == \"\") {\n        return null;\n      }\n      return LocalFavoriteComic.fromJson(jsonDecode(data));\n    } catch (e) {\n      return null;\n    }\n  }\n\n  Future<List<LocalFavoriteComic>> listLocalFavoriteComics(\n      String folderId) async {\n    String data =\n        await _flatInvoke(\"listLocalFavoriteComics\", {\"folderId\": folderId});\n    List list = jsonDecode(data);\n    return list.map((e) => LocalFavoriteComic.fromJson(e)).toList();\n  }\n\n  Future<List<LocalFavoriteComic>> listAllLocalFavoriteComics() async {\n    String data = await _flatInvoke(\"listAllLocalFavoriteComics\", \"\");\n    List list = jsonDecode(data);\n    return list.map((e) => LocalFavoriteComic.fromJson(e)).toList();\n  }\n\n  Future mergeLocalFavoritesFromWebDav(\n    String webdavRoot,\n    String webdavUsername,\n    String webdavPassword,\n  ) async {\n    return _flatInvoke(\"mergeLocalFavoritesFromWebDav\", {\n      \"webdavRoot\": webdavRoot,\n      \"webdavUsername\": webdavUsername,\n      \"webdavPassword\": webdavPassword,\n    });\n  }\n}\n"
  },
  {
    "path": "lib/basic/Navigator.dart",
    "content": "/// 导航相关\n\nimport 'dart:async';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/IconLoading.dart';\n\n// 用于监听返回到当前页面的事件\n// (await Navigator.push 会在子页面pushReplacement时结束阻塞)\nfinal RouteObserver<ModalRoute<void>> routeObserver =\n    RouteObserver<ModalRoute<void>>();\n\n// 路径深度计数\n\nconst _depthMax = 15;\nvar _depth = 0;\n\nvar navigatorObserver = _NavigatorObserver();\n\nclass _NavigatorObserver extends NavigatorObserver {\n  @override\n  void didPop(Route route, Route? previousRoute) {\n    _depth--;\n    print(\"DEPTH : $_depth\");\n    super.didPop(route, previousRoute);\n  }\n\n  @override\n  void didPush(Route route, Route? previousRoute) {\n    _depth++;\n    print(\"DEPTH : $_depth\");\n    super.didPush(route, previousRoute);\n  }\n}\n\n// 路径达到一定深度的时候使用 pushReplacement\nFuture<dynamic> navPushOrReplace(\n    BuildContext context, WidgetBuilder builder) async {\n  if (_depth < _depthMax) {\n    return Navigator.push(\n      context,\n      mixRoute(builder: builder),\n    );\n  } else {\n    return Navigator.pushReplacement(\n      context,\n      mixRoute(builder: builder),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/Address.dart",
    "content": "/// 分流地址\n\n// addr = \"172.67.7.24:443\"\n// addr = \"104.20.180.50:443\"\n// addr = \"172.67.208.169:443\"\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/ImageAddress.dart';\nimport 'package:pikapika/basic/config/UseApiLoadImage.dart';\n\nimport '../Method.dart';\n\nvar _addresses = [\n  \"0\",\n  \"1\",\n  \"2\",\n  \"3\",\n  \"4\",\n  \"5\",\n  \"6\",\n  \"7\",\n  \"8\",\n  \"9\",\n  \"10\",\n];\n\nlate String _currentAddress;\n\nFuture<void> initAddress() async {\n  _currentAddress = await method.getSwitchAddress();\n}\n\nString currentAddress() => _currentAddress;\n\nString currentAddressName() => _currentAddress == \"0\" ? tr('net.no_address') : tr('net.address') + _currentAddress;\n\nWidget switchAddressSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('net.address')),\n        subtitle: Text(currentAddressName()),\n        onTap: () async {\n          await chooseAddressAndSwitch(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nWidget reloadSwitchAddressSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('net.address_sync')),\n        onTap: () async {\n          String? choose = await chooseListDialog(context, tr('net.address_sync'), [\n            tr('net.address_sync_from_server'),\n            tr('net.address_sync_reset'),\n          ]);\n          if (choose != null) {\n            if (choose == tr('net.address_sync_from_server')) {\n              try {\n                await method.reloadSwitchAddress();\n                defaultToast(context, tr('net.address_sync_success'));\n              } catch (e, s) {\n                print(\"$e\\n$s\");\n                defaultToast(context, tr('net.address_sync_failed'));\n              }\n            } else if (choose == tr('net.address_sync_reset')) {\n              try {\n                await method.resetSwitchAddress();\n                defaultToast(context, tr('net.address_sync_reset_success'));\n              } catch (e, s) {\n                print(\"$e\\n$s\");\n                defaultToast(context, tr('net.address_sync_reset_failed'));\n              }\n            }\n          }\n        },\n      );\n    },\n  );\n}\n\nFuture chooseAddressAndSwitch(BuildContext context) async {\n  String? choose = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr('net.choose_address')),\n        children: <Widget>[\n          ..._addresses.map(\n            (e) => SimpleDialogOption(\n              child: ApiOptionRow(\n                e == \"0\" ? tr('net.no_address') : tr('net.address') + e,\n                e,\n                key: Key(\"API:$e\"),\n              ),\n              onPressed: () {\n                Navigator.of(context).pop(e);\n              },\n            ),\n          ),\n          SimpleDialogOption(\n            child: Text(tr('net.address_sync')),\n            onPressed: () {\n              Navigator.of(context).pop(tr('net.address_sync'));\n            },\n          )\n        ],\n      );\n    },\n  );\n  if (choose != null) {\n    if (tr('net.address_sync') == choose) {\n      try {\n        await method.reloadSwitchAddress();\n        defaultToast(context, tr('net.address_sync_success'));\n      } catch (e, s) {\n        print(\"$e\\n$s\");\n        defaultToast(context, tr('net.address_sync_failed'));\n      }\n      return;\n    }\n    await method.setSwitchAddress(choose);\n    _currentAddress = choose;\n  }\n}\n\nWidget addressPopMenu(BuildContext context) {\n  return PopupMenuButton<int>(\n    icon: const Icon(Icons.webhook),\n    itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n      PopupMenuItem<int>(\n        value: 0,\n        child: ListTile(\n          leading: const Icon(Icons.share),\n          title: Text(\"${tr('net.address')} (${currentAddressName()})\"),\n        ),\n      ),\n      PopupMenuItem<int>(\n        value: 1,\n        child: ListTile(\n          leading: const Icon(Icons.image_search),\n          title: Text(\"${tr('net.image_address')} (${currentImageAddressName()})\"),\n        ),\n      ),\n      PopupMenuItem<int>(\n        value: 2,\n        child: ListTile(\n          leading: const Icon(Icons.network_ping),\n          title: Text(\"${tr('net.use_api_load_image')} (${currentUseApiLoadImageName()})\"),\n        ),\n      ),\n    ],\n    onSelected: (int value) {\n      switch (value) {\n        case 0:\n          chooseAddressAndSwitch(context);\n          break;\n        case 1:\n          chooseImageAddress(context);\n          break;\n        case 2:\n          chooseUseApiLoadImage(context);\n          break;\n      }\n    },\n  );\n}\n\nclass ApiOptionRow extends StatefulWidget {\n  final String title;\n  final String value;\n\n  const ApiOptionRow(this.title, this.value, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ApiOptionRowState();\n}\n\nclass _ApiOptionRowState extends State<ApiOptionRow> {\n  late Future<int> _feature;\n\n  @override\n  void initState() {\n    super.initState();\n    _feature = method.ping(widget.value);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Text(widget.title),\n        Expanded(child: Container()),\n        FutureBuilder(\n          future: _feature,\n          builder: (\n            BuildContext context,\n            AsyncSnapshot<int> snapshot,\n          ) {\n            if (snapshot.connectionState != ConnectionState.done) {\n              return PingStatus(\n                tr('net.ping_testing'),\n                Colors.blue,\n              );\n            }\n            if (snapshot.hasError) {\n              return PingStatus(\n                tr('net.ping_failed'),\n                Colors.red,\n              );\n            }\n            int ping = snapshot.requireData;\n            if (ping <= 200) {\n              return PingStatus(\n                \"${ping}ms\",\n                Colors.green,\n              );\n            }\n            if (ping <= 500) {\n              return PingStatus(\n                \"${ping}ms\",\n                Colors.yellow,\n              );\n            }\n            return PingStatus(\n              \"${ping}ms\",\n              Colors.orange,\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n\nclass PingStatus extends StatelessWidget {\n  final String title;\n  final Color color;\n\n  const PingStatus(this.title, this.color, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Text(\n          '\\u2022',\n          style: TextStyle(\n            color: color,\n          ),\n        ),\n        Text(\" $title\"),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/AndroidDisplayMode.dart",
    "content": "/// 显示模式, 仅安卓有效\n\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../Common.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"androidDisplayMode\";\nList<String> _modes = [];\nString _androidDisplayMode = \"\";\n\nFuture initAndroidDisplayMode() async {\n  if (Platform.isAndroid) {\n    _androidDisplayMode = await method.loadProperty(_propertyName, \"\");\n    _modes = await method.loadAndroidModes();\n    await _changeMode();\n  }\n}\n\nFuture _changeMode() async {\n  await method.setAndroidMode(_androidDisplayMode);\n}\n\nFuture<void> _chooseAndroidDisplayMode(BuildContext context) async {\n  if (Platform.isAndroid) {\n    List<String> list = [\"\"];\n    list.addAll(_modes);\n    String? result = await chooseListDialog<String>(\n      context,\n      tr('settings.android_display_mode.dialog_title'),\n      list,\n    );\n    if (result != null) {\n      await method.saveProperty(_propertyName, result);\n      _androidDisplayMode = result;\n      await _changeMode();\n    }\n  }\n}\n\nWidget androidDisplayModeSetting() {\n  if (Platform.isAndroid) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(\n            tr('settings.android_display_mode.title') + (!isPro ? \"(${tr('app.pro')})\" : \"\"),\n            style: TextStyle(\n              color: !isPro ? Colors.grey : null,\n            ),\n          ),\n          subtitle: Text(_androidDisplayMode),\n          onTap: () async {\n            if (!isPro) {\n              defaultToast(context, tr('app.pro_required'));\n              return;\n            }\n            await _chooseAndroidDisplayMode(context);\n            setState(() {});\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/AndroidSecureFlag.dart",
    "content": "/// 音量键翻页\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"androidSecureFlag\";\n\nlate bool _androidSecureFlag;\n\nFuture<void> initAndroidSecureFlag() async {\n  if (Platform.isAndroid) {\n    _androidSecureFlag =\n        (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n    if (_androidSecureFlag) {\n      await method.androidSecureFlag(true);\n    }\n  }\n}\n\nWidget androidSecureFlagSetting() {\n  if (Platform.isAndroid) {\n    return StatefulBuilder(builder:\n        (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n          value: _androidSecureFlag,\n          title: Text(\n            tr(\"settings.android_secure_flag\") + (!isPro ? \"(${tr('settings.app.pro')})\" : \"\"),\n            style: TextStyle(\n              color: !isPro ? Colors.grey : null,\n            ),\n          ),\n          onChanged: (target) async {\n            if (!isPro) {\n              defaultToast(context, tr('app.pro_required'));\n              return;\n            }\n            await method.saveProperty(_propertyName, \"$target\");\n            _androidSecureFlag = target;\n            await method.androidSecureFlag(_androidSecureFlag);\n            setState(() {});\n          });\n    });\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/AppOrientation.dart",
    "content": "// AppOrientation.dart\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nconst _propertyName = \"appOrientation\";\nlate AppOrientation _appOrientation;\n\nenum AppOrientation {\n  normal,\n  landscape,\n  portrait,\n}\n\nString appOrientationName(AppOrientation type) {\n  switch (type) {\n    case AppOrientation.normal:\n      return tr('settings.app_orientation.normal');\n    case AppOrientation.landscape:\n      return tr('settings.app_orientation.landscape');\n    case AppOrientation.portrait:\n      return tr('settings.app_orientation.portrait');\n  }\n}\n\nFuture initAppOrientation() async {\n  _appOrientation = _fromString(await method.loadProperty(\n      _propertyName, AppOrientation.normal.toString()));\n  _set();\n}\n\nAppOrientation _fromString(String valueForm) {\n  for (var value in AppOrientation.values) {\n    if (value.toString() == valueForm) {\n      return value;\n    }\n  }\n  return AppOrientation.values.first;\n}\n\nAppOrientation get currentAppOrientation => _appOrientation;\n\nFuture chooseAppOrientation(BuildContext context) async {\n  final Map<String, AppOrientation> map = {};\n  for (var element in AppOrientation.values) {\n    map[appOrientationName(element)] = element;\n  }\n  final newAppOrientation = await chooseMapDialog(\n    context,\n    map,\n    tr('settings.app_orientation.choose'),\n  );\n  if (newAppOrientation != null) {\n    await method.saveProperty(_propertyName, \"$newAppOrientation\");\n    _appOrientation = newAppOrientation;\n    _set();\n  }\n}\n\nWidget appOrientationWidget() {\n  if (!Platform.isAndroid && !Platform.isIOS) {\n    return const SizedBox.shrink();\n  }\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('settings.app_orientation.title')),\n        subtitle: Text(appOrientationName(_appOrientation)),\n        onTap: () async {\n          await chooseAppOrientation(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nvoid _set() {\n  if (Platform.isAndroid || Platform.isIOS) {\n    switch (_appOrientation) {\n      case AppOrientation.normal:\n        SystemChrome.setPreferredOrientations([]);\n        break;\n      case AppOrientation.landscape:\n        SystemChrome.setPreferredOrientations([\n          DeviceOrientation.landscapeLeft,\n          DeviceOrientation.landscapeRight,\n        ]);\n        break;\n      case AppOrientation.portrait:\n        SystemChrome.setPreferredOrientations([\n          DeviceOrientation.portraitUp,\n          DeviceOrientation.portraitDown,\n        ]);\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/Authentication.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/Platform.dart';\nimport 'package:pikapika/screens/DesktopAuthenticationScreen.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"authentication\";\nlate bool _authentication;\n\nFuture<void> initAuthentication() async {\n  if (Platform.isIOS || androidVersion >= 29) {\n    _authentication =\n        (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n  } else if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {\n    _authentication = await needDesktopAuthentication();\n  } else {\n    _authentication = false;\n  }\n}\n\nbool currentAuthentication() {\n  return _authentication;\n}\n\nFuture<bool> verifyAuthentication(BuildContext context) async {\n  if (Platform.isIOS || androidVersion >= 29) {\n    return await method.verifyAuthentication();\n  }\n  if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {\n    return await Navigator.of(context).push(\n            MaterialPageRoute(builder: (context) => const VerifyPassword())) ==\n        true;\n  }\n  return false;\n}\n\nWidget authenticationSetting() {\n  if (Platform.isIOS || androidVersion >= 29) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return SwitchListTile(\n          value: _authentication,\n          title: Text(tr(\"settings.authentication\")),\n          onChanged: (target) async {\n            await method.saveProperty(_propertyName, \"$target\");\n            _authentication = target;\n            setState(() {});\n          },\n        );\n      },\n    );\n  }\n  if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {\n    return StatefulBuilder(builder: (\n      BuildContext context,\n      void Function(void Function()) setState,\n    ) {\n      return ListTile(\n        title: Text(tr(\"settings.set_password\")),\n        onTap: () async {\n          await Navigator.of(context).push(\n              MaterialPageRoute(builder: (context) => const SetPassword()));\n          await initAuthentication();\n        },\n      );\n    });\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/AutoClean.dart",
    "content": "/// 自动清理\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\n// const _autoCleanMap = {\n//   \"一个月前\": \"${3600 * 24 * 30}\",\n//   \"一周前\": \"${3600 * 24 * 7}\",\n//   \"一天前\": \"${3600 * 24 * 1}\",\n//   \"不自动清理\": \"${0}\",\n// };\n\nfinal _autoCleanMap = {};\n\nlate String _autoCleanSec;\n\nFuture<dynamic> initAutoClean() async {\n  _autoCleanMap.putIfAbsent(tr(\"settings.auto_clean.one_month_ago\"), () => \"${3600 * 24 * 30}\");\n  _autoCleanMap.putIfAbsent(tr(\"settings.auto_clean.one_week_ago\"), () => \"${3600 * 24 * 7}\");\n  _autoCleanMap.putIfAbsent(tr(\"settings.auto_clean.one_day_ago\"), () => \"${3600 * 24 * 1}\");\n  _autoCleanMap.putIfAbsent(tr(\"settings.auto_clean.no_auto_clean\"), () => \"${0}\");\n\n  _autoCleanSec =\n      await method.loadProperty(\"autoCleanSec\", \"${3600 * 24 * 30}\");\n  if (\"0\" != _autoCleanSec) {\n    await method.autoClean(_autoCleanSec);\n  }\n}\n\nString _currentAutoCleanSec() {\n  for (var value in _autoCleanMap.entries) {\n    if (value.value == _autoCleanSec) {\n      return value.key;\n    }\n  }\n  return \"\";\n}\n\nFuture<void> _chooseAutoCleanSec(BuildContext context) async {\n  String? choose = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.auto_clean.title\")),\n        children: <Widget>[\n          ..._autoCleanMap.entries.map(\n            (e) => SimpleDialogOption(\n              child: Text(e.key),\n              onPressed: () {\n                Navigator.of(context).pop(e.value);\n              },\n            ),\n          ),\n        ],\n      );\n    },\n  );\n  if (choose != null) {\n    await method.saveProperty(\"autoCleanSec\", choose);\n    _autoCleanSec = choose;\n  }\n}\n\nWidget autoCleanSecSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.auto_clean.title\")),\n        subtitle: Text(_currentAutoCleanSec()),\n        onTap: () async {\n          await _chooseAutoCleanSec(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/AutoDeleteDownloadOnUnfavorite.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"autoDeleteDownloadOnUnfavorite\";\n\nlate bool _autoDeleteDownloadOnUnfavorite;\n\nFuture initAutoDeleteDownloadOnUnfavorite() async {\n  _autoDeleteDownloadOnUnfavorite =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n  if (_autoDeleteDownloadOnUnfavorite && !isPro) {\n    _autoDeleteDownloadOnUnfavorite = false;\n    await method.saveProperty(_propertyName, \"false\");\n  }\n}\n\nbool autoDeleteDownloadOnUnfavorite() {\n  return _autoDeleteDownloadOnUnfavorite;\n}\n\nWidget autoDeleteDownloadOnUnfavoriteSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _autoDeleteDownloadOnUnfavorite,\n        title: Text(\n          tr(\"settings.auto_delete_download_on_unfavorite\") +\n              (!isPro ? \"(${tr(\"app.pro\")})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        subtitle: !isPro ? Text(tr(\"app.pro_required\")) : null,\n        onChanged: (value) async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          await method.saveProperty(_propertyName, \"$value\");\n          _autoDeleteDownloadOnUnfavorite = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/AutoDownloadOnFavorite.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"autoDownloadOnFavorite\";\n\nlate bool _autoDownloadOnFavorite;\n\nFuture initAutoDownloadOnFavorite() async {\n  _autoDownloadOnFavorite =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n  if (_autoDownloadOnFavorite && !isPro) {\n    _autoDownloadOnFavorite = false;\n    await method.saveProperty(_propertyName, \"false\");\n  }\n}\n\nbool autoDownloadOnFavorite() {\n  return _autoDownloadOnFavorite;\n}\n\nWidget autoDownloadOnFavoriteSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _autoDownloadOnFavorite,\n        title: Text(\n          tr(\"settings.auto_download_on_favorite\") +\n              (!isPro ? \"(${tr(\"app.pro\")})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        subtitle: !isPro ? Text(tr(\"app.pro_required\")) : null,\n        onChanged: (value) async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          await method.saveProperty(_propertyName, \"$value\");\n          _autoDownloadOnFavorite = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/AutoFullScreen.dart",
    "content": "/// 自动全屏\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"autoFullScreen\";\nlate bool _autoFullScreen;\n\nFuture<void> initAutoFullScreen() async {\n  _autoFullScreen =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentAutoFullScreen() {\n  return _autoFullScreen;\n}\n\nWidget autoFullScreenSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _autoFullScreen,\n        title: Text(tr(\"settings.auto_full_screen.title\")),\n        onChanged: (a) async {\n          await method.saveProperty(_propertyName, \"$a\");\n          _autoFullScreen = a;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/AutoFullScreenOnForward.dart",
    "content": "/// 前进时自动全屏\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"autoFullScreenOnForward\";\nlate bool _autoFullScreenOnForward;\n\nFuture<void> initAutoFullScreenOnForward() async {\n  _autoFullScreenOnForward =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentAutoFullScreenOnForward() {\n  return _autoFullScreenOnForward;\n}\n\nFuture<void> setAutoFullScreenOnForward(bool value) async {\n  _autoFullScreenOnForward = value;\n  await method.saveProperty(_propertyName, \"$value\");\n}\n\nWidget autoFullScreenOnForwardSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _autoFullScreenOnForward,\n        title: Text(tr(\"settings.auto_full_screen_on_forward.title\")),\n        onChanged: (a) async {\n          await method.saveProperty(_propertyName, \"$a\");\n          _autoFullScreenOnForward = a;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/CategoriesColumnCount.dart",
    "content": "/// 多线程下载并发数\n\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nString _propertyName = \"categoriesColumnCount\";\nlate int categoriesColumnCount;\n\nEvent categoriesColumnCountEvent = Event();\n\nFuture initCategoriesColumnCount() async {\n  categoriesColumnCount =\n      int.parse(await method.loadProperty(_propertyName, \"0\"));\n}\n\nWidget categoriesColumnCountSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          tr('settings.categories_column_count.title'),\n        ),\n        subtitle:\n            Text(categoriesColumnCount == 0 ? tr('settings.categories_column_count.auto') : \"$categoriesColumnCount\"),\n        onTap: () async {\n          int? value = await chooseMapDialog(\n              context,\n              {\n                tr('settings.categories_column_count.auto'): 0,\n                \"2\": 2,\n                \"3\": 3,\n                \"4\": 4,\n                \"5\": 5,\n              },\n              tr('settings.categories_column_count.choose'));\n          if (value != null) {\n            await method.saveProperty(_propertyName, \"$value\");\n            categoriesColumnCount = value;\n            setState(() {});\n            categoriesColumnCountEvent.broadcast();\n          }\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/CategoriesSort.dart",
    "content": "import 'dart:convert';\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/screens/CategoriesSortScreen.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"categoriesSort\";\nList<String> _categoriesSort = [];\n\nFuture initCategoriesSort() async {\n  var json = await method.loadProperty(_propertyName, \"[]\");\n  _categoriesSort = List<String>.from(jsonDecode(json));\n}\n\nFuture saveCategoriesSort(List<String> categoriesSort) async {\n  _categoriesSort = categoriesSort;\n  await method.saveProperty(_propertyName, jsonEncode(categoriesSort));\n  categoriesSortEvent.broadcast();\n}\n\nList<String> getCategoriesSort() {\n  return _categoriesSort;\n}\n\nvar categoriesSortEvent = Event();\n\nWidget categoriesSortSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        onTap: () {\n          Navigator.of(context).push(MaterialPageRoute(\n            builder: (BuildContext context) {\n              return const CategoriesSortScreen();\n            },\n          ));\n        },\n        title: Text(\n          tr('settings.categories_sort.title'),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ChooserRoot.dart",
    "content": "/// 文件夹选择器的根路径\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:permission_handler/permission_handler.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'Platform.dart';\n\nconst _propertyName = \"chooserRoot\";\nlate String _chooserRoot;\n\nFuture<dynamic> initChooserRoot() async {\n  _chooserRoot = await method.loadProperty(_propertyName, \"\");\n  if (_chooserRoot.isEmpty) {\n    if (Platform.isAndroid) {\n      try {\n        _chooserRoot = await method.androidStorageRoot();\n      } catch (e) {\n        _chooserRoot = \"/sdcard\";\n      }\n    } else if (Platform.isMacOS || Platform.isLinux) {\n      _chooserRoot = await method.getHomeDir();\n    } else if (Platform.isWindows) {\n      _chooserRoot = \"/\";\n    }\n  }\n}\n\nFuture<String> currentChooserRoot() async {\n  if (Platform.isAndroid) {\n    late bool g;\n    if (androidVersion < 30) {\n      g = await Permission.storage.request().isGranted;\n    } else {\n      g = await Permission.manageExternalStorage.request().isGranted;\n    }\n    if (!g) {\n      throw Exception(tr('app.permission_denied'));\n    }\n  }\n  return _chooserRoot;\n}\n\nFuture<dynamic> _inputChooserRoot(BuildContext context) async {\n  String? input = await displayTextInputDialog(\n    context,\n    src: _chooserRoot,\n    title: tr('settings.chooser_root.title'),\n    hint: tr('settings.chooser_root.hint'),\n    desc: tr('settings.chooser_root.desc'),\n  );\n  if (input != null) {\n    await method.saveProperty(_propertyName, input);\n    _chooserRoot = input;\n  }\n}\n\nWidget chooserRootSetting() {\n  if (Platform.isIOS) {\n    return Container();\n  }\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('settings.chooser_root.title')),\n        subtitle: Text(_chooserRoot),\n        onTap: () async {\n          await _inputChooserRoot(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ContentFailedReloadAction.dart",
    "content": "/// 全屏操作\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nenum ContentFailedReloadAction {\n  PULL_DOWN,\n  TOUCH_LOADER,\n}\n\nconst _propertyName = \"contentFailedReloadAction\";\nlate ContentFailedReloadAction contentFailedReloadAction;\nMap<String, ContentFailedReloadAction> _contentFailedReloadActionMap = {};\nFuture<void> initContentFailedReloadAction() async {\n  _contentFailedReloadActionMap = {\n    tr(\"settings.content_failed_reload_action.pull_down\"):\n        ContentFailedReloadAction.PULL_DOWN,\n    tr(\"settings.content_failed_reload_action.touch_loader\"):\n        ContentFailedReloadAction.TOUCH_LOADER,\n  };\n  contentFailedReloadAction =\n      _contentFailedReloadActionFromString(await method.loadProperty(\n    _propertyName,\n    ContentFailedReloadAction.PULL_DOWN.toString(),\n  ));\n}\n\nContentFailedReloadAction _contentFailedReloadActionFromString(String string) {\n  for (var value in ContentFailedReloadAction.values) {\n    if (string == value.toString()) {\n      return value;\n    }\n  }\n  return ContentFailedReloadAction.PULL_DOWN;\n}\n\nString _currentContentFailedReloadActionName() {\n  for (var e in _contentFailedReloadActionMap.entries) {\n    if (e.value == contentFailedReloadAction) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> _chooseContentFailedReloadAction(BuildContext context) async {\n  ContentFailedReloadAction? result =\n      await chooseMapDialog<ContentFailedReloadAction>(\n          context, _contentFailedReloadActionMap, tr(\"settings.content_failed_reload_action.choose\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    contentFailedReloadAction = result;\n  }\n}\n\nWidget contentFailedReloadActionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.content_failed_reload_action.title\")),\n        subtitle: Text(_currentContentFailedReloadActionName()),\n        onTap: () async {\n          await _chooseContentFailedReloadAction(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/CopyFullName.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"copyFullName\";\n\nlate bool _copyFullName;\n\nFuture initCopyFullName() async {\n  _copyFullName = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool copyFullName() {\n  return _copyFullName;\n}\n\nWidget copyFullNameSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.copy_full_name.title\")),\n        value: _copyFullName,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _copyFullName = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/CopyFullNameTemplate.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"copyFullNameTemplate\";\n\nlate String _copyFullNameTemplate;\n\nFuture initCopyFullNameTemplate() async {\n  _copyFullNameTemplate =\n      await method.loadProperty(_propertyName, \"[{author}] {title}\");\n}\n\nString copyFullNameTemplate() {\n  return _copyFullNameTemplate;\n}\n\nWidget copyFullNameTemplateSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.copy_full_name_template.title\")),\n        subtitle: Text(_copyFullNameTemplate),\n        onTap: () async {\n          var result = await displayTextInputDialog(\n            context,\n            title: tr(\"settings.copy_full_name_template.title\"),\n            hint: tr(\"settings.copy_full_name_template.hint\"),\n            src: _copyFullNameTemplate,\n          );\n          if (result == null) {\n            return;\n          }\n          await method.saveProperty(_propertyName, result);\n          _copyFullNameTemplate = result;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/CopySkipConfirm.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"copySkipConfirm\";\n\nlate bool _copySkipConfirm;\n\nFuture initCopySkipConfirm() async {\n  _copySkipConfirm = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool copySkipConfirm() {\n  return _copySkipConfirm;\n}\n\nWidget copySkipConfirmSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title:  Text(tr(\"settings.copy_skip_confirm.title\")),\n        value: _copySkipConfirm,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _copySkipConfirm = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/DisableAutoDownloadOnMobile.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"disableAutoDownloadOnMobile\";\n\nlate bool _disableAutoDownloadOnMobile;\n\nFuture initDisableAutoDownloadOnMobile() async {\n  _disableAutoDownloadOnMobile =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n  if (_disableAutoDownloadOnMobile && !isPro) {\n    _disableAutoDownloadOnMobile = false;\n    await method.saveProperty(_propertyName, \"false\");\n  }\n}\n\nbool disableAutoDownloadOnMobile() {\n  return _disableAutoDownloadOnMobile;\n}\n\nWidget disableAutoDownloadOnMobileSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _disableAutoDownloadOnMobile,\n        title: Text(\n          tr(\"settings.disable_auto_download_on_mobile\") +\n              (!isPro ? \"(${tr(\"app.pro\")})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        subtitle: !isPro ? Text(tr(\"app.pro_required\")) : null,\n        onChanged: (value) async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          await method.saveProperty(_propertyName, \"$value\");\n          _disableAutoDownloadOnMobile = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/DownloadAndExportPath.dart",
    "content": "/// 下载的同时导出到文件系统\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\n\nimport '../Method.dart';\n\nlate String _downloadAndExportPath;\n\nFuture initDownloadAndExportPath() async {\n  if (Platform.isWindows ||\n      Platform.isMacOS ||\n      Platform.isAndroid ||\n      Platform.isLinux) {\n    _downloadAndExportPath = await method.loadDownloadAndExportPath();\n  }\n}\n\nWidget downloadAndExportPathSetting() {\n  if (Platform.isWindows ||\n      Platform.isMacOS ||\n      Platform.isAndroid ||\n      Platform.isLinux) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(tr(\"settings.download_and_export_path.title\")),\n          subtitle: Text(_downloadAndExportPath),\n          onTap: () async {\n            if (_downloadAndExportPath == \"\") {\n              bool b = await confirmDialog(\n                context,\n                tr(\"settings.download_and_export_path.confirm\"),\n                tr(\"settings.download_and_export_path.desc\"),\n              );\n              if (b) {\n                late String? folder;\n                try {\n                  folder = await chooseFolder(context);\n                } catch (e) {\n                  defaultToast(context, \"$e\");\n                  return;\n                }\n                if (folder != null) {\n                  await method.saveDownloadAndExportPath(folder);\n                  _downloadAndExportPath = folder;\n                  setState(() {});\n                }\n              }\n            } else {\n              bool b = await confirmDialog(\n                context,\n                tr(\"settings.download_and_export_path.confirm\"),\n                tr(\"settings.download_and_export_path.desc\"),\n              );\n              if (b) {\n                var folder = \"\";\n                await method.saveDownloadAndExportPath(folder);\n                _downloadAndExportPath = folder;\n                setState(() {});\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/DownloadCachePath.dart",
    "content": "/// 下载的同时导出到文件系统\n\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:filesystem_picker/filesystem_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\n\nimport '../../screens/ImportFromOffScreen.dart';\nimport '../Method.dart';\nimport 'ChooserRoot.dart';\nimport 'IconLoading.dart';\n\nlate String _downloadCachePath;\n\nFuture initDownloadCachePath() async {\n  if (Platform.isWindows ||\n      Platform.isMacOS ||\n      Platform.isAndroid ||\n      Platform.isLinux) {\n    _downloadCachePath = await method.loadDownloadCachePath();\n  }\n}\n\nWidget downloadCachePathSetting() {\n  if (Platform.isWindows ||\n      Platform.isMacOS ||\n      Platform.isAndroid ||\n      Platform.isLinux) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(tr(\"settings.download_cache_path.title\")),\n          subtitle: Text(_downloadCachePath),\n          onTap: () async {\n            if (_downloadCachePath == \"\") {\n              bool b = await confirmDialog(\n                context,\n                tr(\"settings.download_cache_path.confirm\"),\n                tr(\"settings.download_cache_path.desc\") + \"\\n\\n${String.fromCharCodes(base64Decode(\"L0FuZHJvaWQvZGF0YS9jb20ucGljYWNvbWljLmZyZWdhdGEvZmlsZXMv\"))}\",\n              );\n              if (b) {\n                late String? folder;\n                try {\n                  folder = await chooseFolder(context);\n                } catch (e) {\n                  defaultToast(context, \"$e\");\n                  return;\n                }\n                if (folder != null) {\n                  await method.saveDownloadCachePath(folder);\n                  _downloadCachePath = folder;\n                  setState(() {});\n                }\n              }\n            } else {\n              bool b = await confirmDialog(\n                context,\n                tr(\"settings.download_cache_path.confirm\"),\n                tr(\"settings.download_cache_path.cancel_desc\"),\n              );\n              if (b) {\n                var folder = \"\";\n                await method.saveDownloadCachePath(folder);\n                _downloadCachePath = folder;\n                setState(() {});\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n\nWidget importViewLogFromOff() {\n  if (Platform.isWindows ||\n      Platform.isMacOS ||\n      Platform.isAndroid ||\n      Platform.isLinux) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(tr(\"settings.download_cache_path.import_view_log_from_off.title\")),\n          subtitle: Text(_downloadCachePath),\n          onTap: () async {\n            bool b = await confirmDialog(\n              context,\n              tr(\"settings.download_cache_path.import_view_log_from_off.title\"),\n              tr('settings.download_cache_path.import_view_log_from_off.desc')+ \"\\n\\n${String.fromCharCodes(base64Decode(\"L2RhdGEvZGF0YS9jb20ucGljYWNvbWljLmdyZWdhdGEvZGF0YWJhc2VzL2NvbV9waWNhY29taWNfZnJlZ2F0YS5kYg==\"))}\",\n            );\n            if (b) {\n              late String chooseRoot;\n              try {\n                chooseRoot = await currentChooserRoot();\n              } catch (e) {\n                defaultToast(context, \"$e\");\n                return;\n              }\n              String? path;\n              if (Platform.isAndroid) {\n                path = await FilesystemPicker.open(\n                  title: 'Open file',\n                  context: context,\n                  rootDirectory: Directory(chooseRoot),\n                  fsType: FilesystemType.file,\n                  folderIconColor: Colors.teal,\n                  allowedExtensions: ['.db'],\n                  fileTileSelectMode: FileTileSelectMode.wholeTile,\n                );\n              } else {\n                var ls = await FilePicker.platform.pickFiles(\n                  dialogTitle: tr(\"settings.download_cache_path.import_view_log_from_off.choose_file_dialog_title\"),\n                  allowMultiple: false,\n                  initialDirectory: chooseRoot,\n                  type: FileType.custom,\n                  allowedExtensions: ['db'],\n                  allowCompression: false,\n                );\n                path = ls != null && ls.count > 0 ? ls.paths[0] : null;\n              }\n              if (path != null) {\n                if (path.endsWith(\".db\")) {\n                  Navigator.of(context).push(\n                    mixRoute(\n                      builder: (BuildContext context) =>\n                          ImportFromOffScreen(dbPath: path!),\n                    ),\n                  );\n                }\n              }\n            }\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/DownloadThreadCount.dart",
    "content": "/// 多线程下载并发数\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport 'IsPro.dart';\n\nlate int _downloadThreadCount;\nconst _values = [1, 2, 3, 4, 5];\n\nFuture initDownloadThreadCount() async {\n  _downloadThreadCount = await method.loadDownloadThreadCount();\n}\n\nWidget downloadThreadCountSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          tr(\"settings.download_thread_count.title\") + (!isPro ? \"(${tr(\"app.pro\")})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        subtitle: Text(\"$_downloadThreadCount\"),\n        onTap: () async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          int? value = await chooseListDialog(context, tr(\"settings.download_thread_count.choose\"), _values);\n          if (value != null) {\n            await method.saveDownloadThreadCount(value);\n            _downloadThreadCount = value;\n            setState(() {});\n          }\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/DragRegionLock.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"dragRegionLock\";\n\nlate bool _dragRegionLock;\n\nFuture initDragRegionLock() async {\n  _dragRegionLock = (await method.loadProperty(_propertyName, \"true\")) == \"true\";\n}\n\nbool dragRegionLock() {\n  return _dragRegionLock;\n}\n\nFuture<void> setDragRegionLock(bool value) async {\n  await method.saveProperty(_propertyName, \"$value\");\n  _dragRegionLock = value;\n}\n\nWidget dragRegionLockSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _dragRegionLock,\n        title: Text(tr(\"settings.drag_region_lock.title\")),\n        onChanged: (target) async {\n          await method.saveProperty(_propertyName, \"$target\");\n          _dragRegionLock = target;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/EBookScrolling.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"eBookScrolling\";\n\nlate bool _eBookScrolling;\n\nbool get eBookScrolling => _eBookScrolling;\n\nFuture initEBookScrolling() async {\n  _eBookScrolling =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget eBookScrollingSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.ebook_scrolling.title\")),\n        value: _eBookScrolling,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _eBookScrolling = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/EBookScrollingRange.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\n\nconst _propertyName = \"eBookScrollingRange\";\n\nlate int _eBookScrollingRange;\n\nFuture initEBookScrollingRange() async {\n  _eBookScrollingRange =\n      int.parse((await method.loadProperty(_propertyName, \"80\")));\n}\n\ndouble get eBookScrollingRange => _eBookScrollingRange / 100;\n\nWidget eBookScrollingRangeSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.ebook_scrolling_range.title\") + \" - \" + tr(\"settings.ebook_scrolling_range.desc\") + \" : $_eBookScrollingRange%${tr(\"settings.ebook_scrolling_range.screen_height\")}\"),\n        subtitle: Slider(\n          min: 30.toDouble(),\n          max: 80.toDouble(),\n          value: _eBookScrollingRange.toDouble(),\n          onChanged: (double value) async {\n            final va = value.toInt();\n            await method.loadProperty(_propertyName, \"$va\");\n            setState(() {\n              _eBookScrollingRange = va;\n            });\n          },\n          divisions: (80 - 30),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/EBookScrollingTrigger.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\n\nconst _propertyName = \"eBookScrollingTrigger\";\n\nlate double _eBookScrollingTrigger;\n\nFuture initEBookScrollingTrigger() async {\n  _eBookScrollingTrigger =\n      double.parse((await method.loadProperty(_propertyName, \"0.3\")));\n}\n\ndouble get eBookScrollingTrigger => _eBookScrollingTrigger;\n\nWidget eBookScrollingTriggerSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.ebook_scrolling_trigger.title\") + \" - \" + tr(\"settings.ebook_scrolling_trigger.desc\") + \" : $_eBookScrollingTrigger ${tr(\"settings.ebook_scrolling_trigger.cm\")}\"),\n        subtitle: Slider(\n          min: 0.1.toDouble(),\n          max: 2.0.toDouble(),\n          value: _eBookScrollingTrigger.toDouble(),\n          onChanged: (double value) async {\n            await method.saveProperty(_propertyName, \"$value\");\n            setState((){\n              _eBookScrollingTrigger = value;\n            });\n          },\n          divisions: (20 - 1),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ExportPath.dart",
    "content": "/// 文件夹选择器的根路径\n\nimport 'dart:io';\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport '../Cross.dart';\nimport '../Method.dart';\nimport 'Platform.dart';\n\nconst _propertyName = \"exportPath\";\nlate String _exportPath;\n\nFuture<dynamic> initExportPath() async {\n  _exportPath = await method.loadProperty(_propertyName, \"\");\n  if (_exportPath.isEmpty) {\n    if (Platform.isAndroid) {\n      try {\n        _exportPath = await method.androidDefaultExportsDir();\n      } catch (e) {\n        _exportPath = \"/sdcard/Download/pikapika/exports\";\n      }\n    } else if (Platform.isMacOS || Platform.isLinux) {\n      _exportPath = await method.getHomeDir();\n      if (Platform.isMacOS) {\n        _exportPath = _exportPath + \"/Downloads\";\n      }\n    } else if (Platform.isWindows) {\n      _exportPath = \"exports\";\n    }\n  }\n}\n\nFuture<String> attachExportPath() async {\n  late String path;\n  if (Platform.isIOS) {\n    path = await method.iosGetDocumentDir();\n  } else {\n    if (Platform.isAndroid) {\n      late bool g;\n      if (androidVersion < 30) {\n        g = await Permission.storage.request().isGranted;\n      }else{\n        g = await Permission.manageExternalStorage.request().isGranted;\n      }\n      if (!g) {\n        throw Exception(\"申请权限被拒绝\");\n      }\n    }\n    path = _exportPath;\n  }\n  if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {\n    await method.mkdirs(path);\n  } else if (Platform.isAndroid) {\n    await method.androidMkdirs(path);\n  }\n  return path;\n}\n\nString showExportPath() {\n  if (Platform.isIOS) {\n    return \"\\n\\n\"+tr(\"settings.export_path.ios_desc\") ;\n  }\n  return \"\\n\\n$_exportPath\";\n}\n\nFuture _setExportPath(String folder) async {\n  await method.saveProperty(_propertyName, folder);\n  _exportPath = folder;\n}\n\nWidget displayExportPathInfo() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      if (Platform.isIOS) {\n        return Container(\n          margin: const EdgeInsets.all(15),\n          padding: const EdgeInsets.all(15),\n          color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n              .withOpacity(.01),\n          child: Text(tr(\"settings.export_path.ios_desc2\")),\n        );\n      }\n      return Column(children: [\n        MaterialButton(\n          onPressed: () async {\n            String? choose = await chooseFolder(context);\n            if (choose != null) {\n              _setExportPath(choose);\n            }\n            setState(() {});\n          },\n          child: LayoutBuilder(\n            builder: (BuildContext context, BoxConstraints constraints) {\n              return Container(\n                width: constraints.maxWidth,\n                padding: const EdgeInsets.only(top: 15, bottom: 15),\n                color: (Theme.of(context).textTheme.bodyText1?.color ??\n                        Colors.black)\n                    .withOpacity(.05),\n                child: Text(\n                  tr(\"settings.export_path.export_path_desc\") + \":\\n\"\n                  \"$_exportPath\",\n                  textAlign: TextAlign.center,\n                ),\n              );\n            },\n          ),\n        ),\n        ...Platform.isAndroid\n            ? [\n                Container(height: 15),\n                Container(\n                  margin: const EdgeInsets.all(15),\n                  padding: const EdgeInsets.all(15),\n                  color: (Theme.of(context).textTheme.bodyText1?.color ??\n                          Colors.black)\n                      .withOpacity(.01),\n                  child: Text(\n                    tr(\"settings.export_path.android_desc\"),\n                  ),\n                ),\n              ]\n            : [],\n      ]);\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ExportRename.dart",
    "content": "/// 自动全屏\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"exportRename\";\nlate bool _exportRename;\n\nFuture<void> initExportRename() async {\n  _exportRename = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentExportRename() {\n  return _exportRename;\n}\n\nWidget exportRenameSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.export_rename.title\")),\n        value: _exportRename,\n        onChanged: (value) async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          } \n          await method.saveProperty(_propertyName, \"$value\");\n          _exportRename = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/FullScreenAction.dart",
    "content": "/// 全屏操作\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nenum FullScreenAction {\n  TOUCH_ONCE,\n  CONTROLLER,\n  TOUCH_DOUBLE,\n  TOUCH_DOUBLE_ONCE_NEXT,\n  THREE_AREA,\n}\n\n// Map<String, FullScreenAction> _fullScreenActionMap = {\n//   \"点击屏幕一次全屏\": FullScreenAction.TOUCH_ONCE,\n//   \"使用控制器全屏\": FullScreenAction.CONTROLLER,\n//   \"双击屏幕全屏\": FullScreenAction.TOUCH_DOUBLE,\n//   \"双击屏幕全屏 + 单击屏幕下一页\": FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT,\n//   \"将屏幕划分成三个区域 (上一页, 下一页, 全屏)\": FullScreenAction.THREE_AREA,\n// };\n\nMap<String, FullScreenAction> _fullScreenActionMap = {};\n\nconst _defaultController = FullScreenAction.TOUCH_ONCE;\nconst _propertyName = \"fullScreenAction\";\nlate FullScreenAction _fullScreenAction;\n\nFuture<void> initFullScreenAction() async {\n  _fullScreenActionMap.addAll({\n    tr(\"settings.full_screen_action.touch_once\"): FullScreenAction.TOUCH_ONCE,\n    tr(\"settings.full_screen_action.controller\"): FullScreenAction.CONTROLLER,\n    tr(\"settings.full_screen_action.touch_double\"): FullScreenAction.TOUCH_DOUBLE,\n    tr(\"settings.full_screen_action.touch_double_once_next\"): FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT,\n    tr(\"settings.full_screen_action.three_area\"): FullScreenAction.THREE_AREA,\n  });\n  _fullScreenAction = _fullScreenActionFromString(await method.loadProperty(\n    _propertyName,\n    FullScreenAction.TOUCH_ONCE.toString(),\n  ));\n}\n\nFullScreenAction currentFullScreenAction() {\n  return _fullScreenAction;\n}\n\nFullScreenAction _fullScreenActionFromString(String string) {\n  for (var value in FullScreenAction.values) {\n    if (string == value.toString()) {\n      return value;\n    }\n  }\n  return _defaultController;\n}\n\nString currentFullScreenActionName() {\n  for (var e in _fullScreenActionMap.entries) {\n    if (e.value == _fullScreenAction) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> chooseFullScreenAction(BuildContext context) async {\n  FullScreenAction? result = await chooseMapDialog<FullScreenAction>(\n      context, _fullScreenActionMap, tr(\"settings.full_screen_action.choose\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    _fullScreenAction = result;\n  }\n}\n\nWidget fullScreenActionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.full_screen_action.title\")),\n        subtitle: Text(currentFullScreenActionName()),\n        onTap: () async {\n          await chooseFullScreenAction(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/FullScreenUI.dart",
    "content": "/// 全屏操作\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nenum FullScreenUI {\n  NO,\n  HIDDEN_BOTTOM,\n  ALL,\n}\n\n// Map<String, FullScreenUI> fullScreenUIMap = {\n//   \"不使用\": FullScreenUI.NO,\n//   \"去除虚拟控制器\": FullScreenUI.HIDDEN_BOTTOM,\n//   \"全屏\": FullScreenUI.ALL,\n// };\n\nfinal Map<String, FullScreenUI> fullScreenUIMap = {};\n\nconst _propertyName = \"fullScreenUI\";\nlate FullScreenUI fullScreenUI;\n\nFuture<void> initFullScreenUI() async {\n  fullScreenUIMap.addAll({\n    tr(\"settings.full_screen_ui.no\"): FullScreenUI.NO,\n    tr(\"settings.full_screen_ui.hidden_bottom\"): FullScreenUI.HIDDEN_BOTTOM,\n    tr(\"settings.full_screen_ui.all\"): FullScreenUI.ALL,\n  });\n  fullScreenUI = _fullScreenUIFromString(await method.loadProperty(\n    _propertyName,\n    FullScreenUI.NO.toString(),\n  ));\n  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(\n    systemStatusBarContrastEnforced: true,\n    systemNavigationBarContrastEnforced: true,\n  ));\n  SystemChrome.setEnabledSystemUIMode(\n    SystemUiMode.edgeToEdge,\n    overlays: SystemUiOverlay.values,\n  );\n  switchFullScreenUI();\n}\n\nFullScreenUI _fullScreenUIFromString(String string) {\n  for (var value in FullScreenUI.values) {\n    if (string == value.toString()) {\n      return value;\n    }\n  }\n  return FullScreenUI.NO;\n}\n\nString currentFullScreenUIName() {\n  for (var e in fullScreenUIMap.entries) {\n    if (e.value == fullScreenUI) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> chooseFullScreenUI(BuildContext context) async {\n  FullScreenUI? result =\n      await chooseMapDialog<FullScreenUI>(context, fullScreenUIMap, tr(\"settings.full_screen_ui.choose\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    fullScreenUI = result;\n    switchFullScreenUI();\n  }\n}\n\nvoid switchFullScreenUI() {\n  List<SystemUiOverlay> list = [...SystemUiOverlay.values];\n  switch (fullScreenUI) {\n    case FullScreenUI.HIDDEN_BOTTOM:\n      SystemChrome.setEnabledSystemUIMode(\n        SystemUiMode.manual,\n        overlays: [SystemUiOverlay.top],\n      );\n      break;\n    case FullScreenUI.ALL:\n      SystemChrome.setEnabledSystemUIMode(\n        SystemUiMode.manual,\n        overlays: [],\n      );\n      break;\n    case FullScreenUI.NO:\n      SystemChrome.setEnabledSystemUIMode(\n        SystemUiMode.edgeToEdge,\n        overlays: SystemUiOverlay.values,\n      );\n      break;\n  }\n  if (Platform.isAndroid || Platform.isIOS) {\n    SystemChrome.setEnabledSystemUIMode(\n      SystemUiMode.edgeToEdge,\n      overlays: list,\n    );\n  }\n}\n\nWidget fullScreenUISetting() {\n  if (Platform.isAndroid || Platform.isIOS) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(tr(\"settings.full_screen_ui.title\")),\n          subtitle: Text(currentFullScreenUIName()),\n          onTap: () async {\n            await chooseFullScreenUI(context);\n            setState(() {});\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/GalleryPreloadCount.dart",
    "content": "/// 相册模式下预加载图片数量\n\nconst galleryPrePreloadCount = 1;\nconst galleryPreloadCount = 2;\n"
  },
  {
    "path": "lib/basic/config/GestureSpeed.dart",
    "content": "import 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nconst _propertyName = \"gestureSpeed\";\n\nlate double _gestureSpeed;\n\nFuture initGestureSpeed() async {\n  _gestureSpeed = double.parse(await method.loadProperty(_propertyName, \"1.0\"));\n}\n\ndouble currentGestureSpeed() {\n  return _gestureSpeed;\n}\n\nFuture<void> setGestureSpeed(double value) async {\n  _gestureSpeed = value;\n  await method.saveProperty(_propertyName, \"$value\");\n}\n\nWidget gestureSpeedSetting() {\n  return StatefulBuilder(builder: (BuildContext context, StateSetter setState) {\n    return ListTile(\n      title: Text(tr(\"settings.gesture_speed.title\")),\n      subtitle: Text(\"${currentGestureSpeed().toStringAsFixed(1)}x\"),\n      onTap: () async {\n        double value = currentGestureSpeed();\n        await showDialog(\n          context: context,\n          builder: (context) {\n            return StatefulBuilder(\n              builder: (context, setState) {\n                return AlertDialog(\n                  title: Text(tr(\"settings.gesture_speed.title\")),\n                  content: SizedBox(\n                    height: 100,\n                    child: Column(\n                      children: [\n                        Text(\"${value.toStringAsFixed(1)}x\"),\n                        Slider(\n                          min: 0.1,\n                          max: 5.0,\n                          divisions: 49,\n                          value: value,\n                          onChanged: (v) {\n                            setState(() {\n                              value = v;\n                            });\n                          },\n                        ),\n                      ],\n                    ),\n                  ),\n                  actions: [\n                    TextButton(\n                      onPressed: () {\n                        Navigator.of(context).pop();\n                      },\n                      child: Text(tr(\"app.cancel\")),\n                    ),\n                    TextButton(\n                      onPressed: () async {\n                        await setGestureSpeed(value);\n                        Navigator.of(context).pop();\n                      },\n                      child: Text(tr(\"app.confirm\")),\n                    ),\n                  ],\n                );\n              },\n            );\n          },\n        );\n        setState(() {});\n      },\n    );\n  });\n}\n"
  },
  {
    "path": "lib/basic/config/HiddenFdIcon.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"hiddenFdIcon\";\n\nlate bool _hiddenFdIcon;\n\n bool get hiddenFdIcon => _hiddenFdIcon;\n\nvar hiddenFdIconEvent = Event<EventArgs>();\n\nFuture initHiddenFdIcon() async {\n  _hiddenFdIcon = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget hiddenFdIconSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title:  Text(tr(\"settings.hidden_fd_icon.title\")),\n        value: _hiddenFdIcon,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _hiddenFdIcon = value;\n          setState(() {});\n          hiddenFdIconEvent.broadcast();\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/HiddenSearchPersion.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"hiddenSearchPersion\";\n\nlate bool _hiddenSearchPersion;\n\nbool get hiddenSearchPersion => _hiddenSearchPersion;\n\nvar hiddenSearchPersionEvent = Event<EventArgs>();\n\nFuture initHiddenSearchPersion() async {\n  _hiddenSearchPersion =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget hiddenSearchPersionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.hidden_search_persion.title\")),\n        value: _hiddenSearchPersion,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _hiddenSearchPersion = value;\n          setState(() {});\n          hiddenSearchPersionEvent.broadcast();\n          await method.removeAllSubscribed();\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/HiddenSubIcon.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"hiddenSubIcon\";\n\nlate bool _hiddenSubIcon;\n\nbool get hiddenSubIcon => _hiddenSubIcon;\n\nvar hiddenSubIconEvent = Event<EventArgs>();\n\nFuture initHiddenSubIcon() async {\n  _hiddenSubIcon =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget hiddenSubIconSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.hidden_sub_icon.title\")),\n        value: _hiddenSubIcon,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _hiddenSubIcon = value;\n          setState(() {});\n          hiddenSubIconEvent.broadcast();\n          await method.removeAllSubscribed();\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/HiddenViewed.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"hiddenViewed\";\n\nlate bool _hiddenViewed;\n\nbool get hiddenViewed => _hiddenViewed;\n\nvar hiddenViewedEvent = Event<EventArgs>();\n\nFuture initHiddenViewed() async {\n  _hiddenViewed =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget hiddenViewedSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.hidden_viewed.title\")),\n        value: _hiddenViewed,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _hiddenViewed = value;\n          setState(() {});\n          hiddenViewedEvent.broadcast();\n          await method.removeAllSubscribed();\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/HiddenWords.dart",
    "content": "import 'dart:convert';\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../../screens/HiddenWordsScreen.dart';\nimport '../Method.dart';\n\nconst _key = \"hiddenWords\";\n\nfinal List<String> _hiddenWords = [];\n\nList<String> get hiddenWords => _hiddenWords;\n\nFuture<String> initHiddenWords() async {\n  final words = await method.loadProperty(_key, \"[]\");\n  _hiddenWords.clear();\n  _hiddenWords.addAll((jsonDecode(words) as List).cast<String>());\n  return words;\n}\n\nFuture<void> saveHiddenWords(List<String> words) async {\n  _hiddenWords.clear();\n  _hiddenWords.addAll(words);\n  await method.saveProperty(_key, jsonEncode(words));\n}\n\nFuture<void> addHiddenWord(String word) async {\n  if (word.trim().isEmpty) return;\n  if (!_hiddenWords.contains(word)) {\n    _hiddenWords.add(word);\n    await method.saveProperty(_key, jsonEncode(_hiddenWords));\n  }\n}\n\nFuture<void> removeHiddenWord(String word) async {\n  _hiddenWords.remove(word);\n  await method.saveProperty(_key, jsonEncode(_hiddenWords));\n}\n\nFuture<void> clearHiddenWords() async {\n  _hiddenWords.clear();\n  await method.saveProperty(_key, \"[]\");\n}\n\nWidget hiddenWordsSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.hidden_words.title\")),\n        subtitle: Text(subString(jsonEncode(_hiddenWords))),\n        onTap: () async {\n          await Navigator.of(context).push(MaterialPageRoute(\n            builder: (context) => const HiddenWordsScreen(),\n          ));\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nString subString(String str) {\n  if (str.length > 20) {\n    return str.substring(0, 20) + \"...\";\n  }\n  return str;\n}\n"
  },
  {
    "path": "lib/basic/config/HideOnlineFavorite.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"hideOnlineFavorite\";\n\nlate bool _hideOnlineFavorite;\n\nbool get hideOnlineFavorite => _hideOnlineFavorite;\n\nvar hideOnlineFavoriteEvent = Event<EventArgs>();\n\nFuture initHideOnlineFavorite() async {\n  _hideOnlineFavorite =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget hideOnlineFavoriteSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.hide_online_favorite.title\")),\n        subtitle: Text(tr(\"settings.hide_online_favorite.desc\")),\n        value: _hideOnlineFavorite,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _hideOnlineFavorite = value;\n          setState(() {});\n          hideOnlineFavoriteEvent.broadcast();\n        },\n      );\n    },\n  );\n}\n\n"
  },
  {
    "path": "lib/basic/config/IconLoading.dart",
    "content": "/// 自动全屏\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"iconLoading\";\n\n// 启动图始终显示\nlate bool _iconLoading = false;\n\nFuture<void> initIconLoading() async {\n  _iconLoading = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentIconLoading() {\n  return _iconLoading;\n}\n\nWidget iconLoadingSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.icon_loading.title\")),\n        value: _iconLoading,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _iconLoading = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nRoute<T> mixRoute<T>({required WidgetBuilder builder}) {\n  if (currentIconLoading()) {\n    return PageRouteBuilder(\n      pageBuilder: (context, animation1, animation2) => builder.call(context),\n      transitionDuration: Duration.zero,\n      reverseTransitionDuration: Duration.zero,\n    );\n  }\n  return MaterialPageRoute(builder: builder);\n}\n"
  },
  {
    "path": "lib/basic/config/IgnoreInfoHistory.dart",
    "content": "/// 自动全屏\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"ignoreInfoHistory\";\nlate bool _ignoreInfoHistory;\n\nFuture<void> initIgnoreInfoHistory() async {\n  _ignoreInfoHistory =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentIgnoreInfoHistory() {\n  return _ignoreInfoHistory;\n}\n\nWidget ignoreInfoHistorySetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _ignoreInfoHistory,\n        title: Text(tr(\"settings.ignore_info_history.title\")),\n        onChanged: (a) async {\n          await method.saveProperty(_propertyName, \"$a\");\n          _ignoreInfoHistory = a;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/IgnoreUpgradeConfirm.dart",
    "content": "/// 音量键翻页\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _propertyName = \"ignoreUpgradeConfirm\";\n\nlate bool _ignoreUpgradeConfirm;\n\nbool get ignoreUpgradeConfirm => _ignoreUpgradeConfirm;\n\nFuture<void> initIgnoreUpgradeConfirm() async {\n  _ignoreUpgradeConfirm =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n  if (_ignoreUpgradeConfirm && !isPro) {\n    _ignoreUpgradeConfirm = false;\n    await method.saveProperty(_propertyName, \"false\");\n  }\n}\n\nWidget ignoreUpgradeConfirmSetting() {\n  return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n    return SwitchListTile(\n        value: _ignoreUpgradeConfirm,\n        title: Text(\n          tr(\"settings.ignore_upgrade_confirm.title\") + (!isPro ? \"(${tr(\"app.pro\")})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        onChanged: (target) async {\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          await method.saveProperty(_propertyName, \"$target\");\n          _ignoreUpgradeConfirm = target;\n          setState(() {});\n        });\n  });\n}\n"
  },
  {
    "path": "lib/basic/config/ImageAddress.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\nimport 'Address.dart';\n\nvar _imageAddresses = [\n  \"0\",\n  \"1\",\n  \"2\",\n  \"3\",\n  \"4\",\n  \"5\",\n  \"6\",\n];\n\nlate String _currentImageAddress;\n\nFuture<void> initImageAddress() async {\n  _currentImageAddress = await method.getImageSwitchAddress();\n}\n\nint currentImageAddress() {\n  return int.parse(_currentImageAddress);\n}\n\nString currentImageAddressName() => _currentImageAddress == \"0\"\n    ? tr('net.no_address')\n    : tr('net.address') + _currentImageAddress;\n\nFuture<void> chooseImageAddress(BuildContext context) async {\n  String? choose = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr('settings.image_address.title')),\n        children: <Widget>[\n          ..._imageAddresses.map(\n            (e) => SimpleDialogOption(\n              child: ApiOptionRowImg(\n                e == \"0\" ? tr('net.no_address') : tr('net.address') + e,\n                e,\n                key: Key(\"API:${e}\"),\n              ),\n              onPressed: () {\n                Navigator.of(context).pop(e);\n              },\n            ),\n          ),\n        ],\n      );\n    },\n  );\n  if (choose != null) {\n    await method.setImageSwitchAddress(choose);\n    _currentImageAddress = choose;\n  }\n}\n\nWidget imageSwitchAddressSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('settings.image_address.title')),\n        subtitle: Text(currentImageAddressName()),\n        onTap: () async {\n          await chooseImageAddress(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nclass ApiOptionRowImg extends StatefulWidget {\n  final String title;\n  final String value;\n\n  const ApiOptionRowImg(this.title, this.value, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ApiOptionRowImgState();\n}\n\nclass _ApiOptionRowImgState extends State<ApiOptionRowImg> {\n  late Future<int> _feature;\n\n  @override\n  void initState() {\n    super.initState();\n    if (\"0\" != widget.value) {\n      _feature = method.pingImg(widget.value);\n    } else {\n      _feature = method.ping(currentAddress());\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Row(\n      children: [\n        Text(widget.title),\n        Expanded(child: Container()),\n        FutureBuilder(\n          future: _feature,\n          builder: (\n            BuildContext context,\n            AsyncSnapshot<int> snapshot,\n          ) {\n            if (snapshot.connectionState != ConnectionState.done) {\n              return PingStatus(\n                tr('settings.image_address.pinging'),\n                Colors.blue,\n              );\n            }\n            if (snapshot.hasError) {\n              return PingStatus(\n                tr('settings.image_address.failed'),\n                Colors.red,\n              );\n            }\n            int ping = snapshot.requireData;\n            if (ping <= 200) {\n              return PingStatus(\n                \"${ping}ms\",\n                Colors.green,\n              );\n            }\n            if (ping <= 500) {\n              return PingStatus(\n                \"${ping}ms\",\n                Colors.yellow,\n              );\n            }\n            return PingStatus(\n              \"${ping}ms\",\n              Colors.orange,\n            );\n          },\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/ImageFilter.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"imageFilter\";\nlate ImageFilter imageFilter;\n\nWidget processImageFilter(Widget child) => imageFilter.process(child);\n\nFuture<void> initImageFilter() async {\n  imageFilter = _imageFilterFromString(await method.loadProperty(\n    _propertyName,\n    _filters[0].name,\n  ));\n}\n\nImageFilter _imageFilterFromString(String string) {\n  for (var value in _filters) {\n    if (string == value.name) {\n      return value;\n    }\n  }\n  return _filters[0];\n}\n\nclass ImageFilter {\n  final String name;\n  final Widget Function(Widget widget) process;\n\n  ImageFilter(this.name, this.process);\n}\n\nfinal List<ImageFilter> _filters = [\n  ImageFilter(\n    tr(\"settings.image_filter.normal\"),\n    (child) {\n      return child;\n    },\n  ),\n  ImageFilter(\n    tr(\"settings.image_filter.gray\"),\n        (child) {\n      return ColorFiltered(\n        colorFilter: const ColorFilter.mode(Colors.grey, BlendMode.color),\n        child: child,\n      );\n    },\n  ),\n  ImageFilter(\n    tr(\"settings.image_filter.brown\"),\n        (child) {\n      return ColorFiltered(\n        colorFilter: const ColorFilter.matrix(<double>[ 0.393, 0.769, 0.189, 0, 0, 0.349, 0.686, 0.168, 0, 0, 0.272, 0.534, 0.131, 0, 0, 0, 0, 0, 1, 0, ]),\n        child: child,\n      );\n    },\n  ),\n  ImageFilter(\n    \"srgbToLinearGamma\",\n        (child) {\n      return ColorFiltered(\n        colorFilter: const ColorFilter.srgbToLinearGamma(),\n        child: child,\n      );\n    },\n  ),\n  ImageFilter(\n    \"linearToSrgbGamma\",\n        (child) {\n      return ColorFiltered(\n        colorFilter: const ColorFilter.linearToSrgbGamma(),\n        child: child,\n      );\n    },\n  ),\n];\n\nFuture<void> chooseImageFilter(BuildContext context) async {\n  Map<String, ImageFilter> map = {};\n  for (var element in _filters) {\n    map[element.name] = element;\n  }\n  ImageFilter? result = await chooseMapDialog<ImageFilter>(\n    context,\n    map,\n    tr(\"settings.image_filter.choose\"),\n  );\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.name);\n    imageFilter = result;\n  }\n}\n\nWidget imageFilterSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.image_filter.title\")),\n        subtitle: Text(imageFilter.name),\n        onTap: () async {\n          await chooseImageFilter(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ImportNotice.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nWidget importNotice(BuildContext context) {\n  if (Platform.isAndroid) {\n    return Container(\n      margin: const EdgeInsets.all(15),\n      padding: const EdgeInsets.all(15),\n      color: (Theme\n          .of(context)\n          .textTheme\n          .bodyText1\n          ?.color ?? Colors.black)\n          .withOpacity(.01),\n      child: Text(\n        tr(\"settings.import_notice.android_desc\"),\n      ),\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/IsPro.dart",
    "content": "import 'package:event/event.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport '../Entities.dart';\n\nbool get isPro {\n  return _proInfoAll.proInfoAf.isPro || _proInfoAll.proInfoPat.isPro;\n}\n\nProInfoAf get proInfoAf => _proInfoAll.proInfoAf;\nProInfoPat get proInfoPat => _proInfoAll.proInfoPat;\n\nfinal proEvent = Event();\nlate ProInfoAll _proInfoAll;\n\nFuture reloadIsPro() async {\n  _proInfoAll = await method.proInfoAll();\n  proEvent.broadcast();\n}\n"
  },
  {
    "path": "lib/basic/config/KeyboardController.dart",
    "content": "/// 上下键翻页\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"keyboardController\";\n\nlate bool keyboardController;\n\nFuture<void> initKeyboardController() async {\n  keyboardController =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget keyboardControllerSetting() {\n  if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {\n    return StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return SwitchListTile(\n          value: keyboardController,\n          title: Text(tr(\"settings.keyboard_controller.title\")),\n          onChanged: (target) async {\n            await method.saveProperty(_propertyName, \"$target\");\n            keyboardController = target;\n            setState(() {});\n          },\n        );\n      },\n    );\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/ListLayout.dart",
    "content": "/// 列表页的布局\n\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport '../Common.dart';\nimport '../Method.dart';\n\nenum ListLayout {\n  INFO_CARD,\n  ONLY_IMAGE,\n  COVER_AND_TITLE,\n}\n\n// const Map<String, ListLayout> _listLayoutMap = {\n//   '详情': ListLayout.INFO_CARD,\n//   '封面': ListLayout.ONLY_IMAGE,\n//   '封面+标题': ListLayout.COVER_AND_TITLE,\n// };\n\nfinal Map<String, ListLayout> _listLayoutMap = {};\n\nconst _propertyName = \"listLayout\";\nlate ListLayout currentLayout;\n\nvar listLayoutEvent = Event<EventArgs>();\n\nFuture<void> initListLayout() async {\n  _listLayoutMap.addAll({\n    tr('settings.list_layout.info_card'): ListLayout.INFO_CARD,\n    tr('settings.list_layout.only_image'): ListLayout.ONLY_IMAGE,\n    tr('settings.list_layout.cover_and_title'): ListLayout.COVER_AND_TITLE,\n  });\n  currentLayout = _listLayoutFromString(await method.loadProperty(\n    _propertyName,\n    ListLayout.INFO_CARD.toString(),\n  ));\n}\n\nListLayout _listLayoutFromString(String layoutString) {\n  for (var value in ListLayout.values) {\n    if (layoutString == value.toString()) {\n      return value;\n    }\n  }\n  return ListLayout.INFO_CARD;\n}\n\nvoid _chooseListLayout(BuildContext context) async {\n  ListLayout? layout = await chooseMapDialog(context, _listLayoutMap, tr('settings.list_layout.choose'));\n  if (layout != null) {\n    await method.saveProperty(_propertyName, layout.toString());\n    currentLayout = layout;\n    listLayoutEvent.broadcast();\n  }\n}\n\nIconButton chooseLayoutActionButton(BuildContext context) => IconButton(\n      onPressed: () {\n        _chooseListLayout(context);\n      },\n      icon: const Icon(Icons.view_quilt),\n    );\n\nconst chooseListLayout = _chooseListLayout;\n"
  },
  {
    "path": "lib/basic/config/LocalHistorySync.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'ChooserRoot.dart';\n\nconst _dirPathPropertyName = \"localHistorySyncRoot\";\nconst _autoSavePropertyName = \"localHistorySyncAuto\";\n\nlate String _localHistorySyncRoot;\nlate bool _localHistorySyncAuto;\n\nFuture initLocalHistorySync() async {\n  _localHistorySyncRoot = await method.loadProperty(\n    _dirPathPropertyName,\n    \"\",\n  );\n  _localHistorySyncAuto = await method.loadProperty(\n        _autoSavePropertyName,\n        \"false\",\n      ) ==\n      \"true\";\n  if (_localHistorySyncAuto) {\n    localSync();\n  }\n}\n\nFuture localSync() async {\n  if (_localHistorySyncRoot.isEmpty) {\n    return;\n  }\n  return await method.mergeHistoriesFromLocal(join(\n    _localHistorySyncRoot,\n    \"pk.histories\",\n  ));\n}\n\nList<Widget> localHistorySyncTiles() => [\n      localHistorySyncPathTile(),\n      localHistorySyncAutoTile(),\n      localHistorySyncManualTile(),\n    ];\n\nWidget localHistorySyncPathTile() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        onLongPress: () async {\n          bool? clean = await showDialog<bool>(\n            context: context,\n            builder: (BuildContext context) {\n              return AlertDialog(\n                title: Text(tr(\"settings.local_history_sync.clear_path\")),\n                content: Text(tr(\"settings.local_history_sync.clear_path_desc\")),\n                actions: <Widget>[\n                  TextButton(\n                    onPressed: () {\n                      Navigator.of(context).pop(false);\n                    },\n                    child: Text(tr(\"app.cancel\")),\n                  ),\n                  TextButton(\n                    onPressed: () {\n                      Navigator.of(context).pop(true);\n                    },\n                    child: Text(tr(\"app.confirm\")),\n                  ),\n                ],\n              );\n            },\n          );\n          if (clean != null && clean == true) {\n            await method.saveProperty(_dirPathPropertyName, \"\");\n            setState(() {\n              _localHistorySyncRoot = \"\";\n            });\n          }\n        },\n        onTap: () async {\n          if (Platform.isAndroid) {\n            final pState = await Permission.manageExternalStorage.request();\n            if (!pState.isGranted) {\n              return;\n            }\n          }\n          var dir = await FilePicker.platform.getDirectoryPath(\n            dialogTitle: tr(\"settings.local_history_sync.choose_dir\"),\n            initialDirectory:\n                Directory.fromUri(Uri.file(await currentChooserRoot()))\n                    .absolute\n                    .path,\n          );\n          if (dir != null) {\n            await method.saveProperty(_dirPathPropertyName, dir);\n            setState(() {\n              _localHistorySyncRoot = dir;\n            });\n          }\n        },\n        title: Text(tr(\"settings.local_history_sync.sync_to_local\")),\n        subtitle: Text(_localHistorySyncRoot.isEmpty ? tr(\"settings.local_history_sync.not_set\") : _localHistorySyncRoot),\n      );\n    },\n  );\n}\n\nWidget localHistorySyncManualTile() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        onTap: () async {\n          if (_localHistorySyncRoot.isEmpty) {\n            defaultToast(context, tr(\"settings.local_history_sync.not_set\"));\n            return;\n          }\n          try {\n            await localSync();\n            defaultToast(context, tr(\"settings.local_history_sync.sync_success\"));\n          } catch (e, s) {\n            print(\"$e\\n$s\");\n            defaultToast(context, tr(\"settings.local_history_sync.sync_failed\"));\n          }\n        },\n        title: Text(tr(\"settings.local_history_sync.sync_to_local\")),\n      );\n    },\n  );\n}\n\nWidget localHistorySyncAutoTile() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _localHistorySyncAuto,\n        onChanged: (bool value) async {\n          await method.saveProperty(\n            _autoSavePropertyName,\n            value ? \"true\" : \"false\",\n          );\n          setState(() {\n            _localHistorySyncAuto = value;\n          });\n          if (value) {\n            localSync();\n          }\n        },\n        title: Text(tr(\"settings.local_history_sync.auto_sync\")),\n        subtitle: Text(tr(\"settings.local_history_sync.auto_sync_desc\")),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/NoAnimation.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"noAnimation\";\n\nlate bool _noAnimation;\n\nFuture initNoAnimation() async {\n  _noAnimation = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool noAnimation() {\n  return _noAnimation;\n}\n\nFuture<void> setNoAnimation(bool value) async {\n  await method.saveProperty(_propertyName, \"$value\");\n  _noAnimation = value;\n}\n\nWidget noAnimationSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _noAnimation,\n        title: Text(tr(\"settings.no_animation.title\")),\n        onChanged: (target) async {\n          await method.saveProperty(_propertyName, \"$target\");\n          _noAnimation = target;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/PagerAction.dart",
    "content": "/// 列表页下一页的行为\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nenum PagerAction {\n  CONTROLLER,\n  STREAM,\n}\n\n// Map<String, PagerAction> _pagerActionMap = {\n//   \"使用按钮\": PagerAction.CONTROLLER,\n//   \"瀑布流\": PagerAction.STREAM,\n// };\n\nMap<String, PagerAction> _pagerActionMap = {};\n\nconst _propertyName = \"pagerAction\";\nlate PagerAction _pagerAction;\n\nFuture<void> initPagerAction() async {\n  _pagerActionMap.addAll({\n    tr(\"settings.pager_action.controller\"): PagerAction.CONTROLLER,\n    tr(\"settings.pager_action.stream\"): PagerAction.STREAM,\n  });\n  _pagerAction = _pagerActionFromString(await method.loadProperty(\n      _propertyName, PagerAction.CONTROLLER.toString()));\n}\n\nPagerAction currentPagerAction() {\n  return _pagerAction;\n}\n\nPagerAction _pagerActionFromString(String string) {\n  for (var value in PagerAction.values) {\n    if (string == value.toString()) {\n      return value;\n    }\n  }\n  return PagerAction.CONTROLLER;\n}\n\nString _currentPagerActionName() {\n  for (var e in _pagerActionMap.entries) {\n    if (e.value == _pagerAction) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> _choosePagerAction(BuildContext context) async {\n  PagerAction? result =\n      await chooseMapDialog<PagerAction>(context, _pagerActionMap, tr(\"settings.pager_action.choose\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    _pagerAction = result;\n  }\n}\n\nWidget pagerActionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.pager_action.title\")),\n        subtitle: Text(_currentPagerActionName()),\n        onTap: () async {\n          await _choosePagerAction(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/Platform.dart",
    "content": "/// 平台信息\n\nimport 'dart:io';\n\nimport '../Method.dart';\n\nint androidVersion = 0;\n\nFuture<void> initPlatform()async{\n  if (Platform.isAndroid) {\n    androidVersion = await method.androidGetVersion();\n  }\n}"
  },
  {
    "path": "lib/basic/config/Proxy.dart",
    "content": "/// 代理设置\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nlate String _currentProxy;\n\nFuture<String?> initProxy() async {\n  _currentProxy = await method.getProxy();\n  return null;\n}\n\nString currentProxyName() {\n  return _currentProxy == \"\" ? tr(\"settings.proxy.no_proxy\") : _currentProxy;\n}\n\nFuture<dynamic> inputProxy(BuildContext context) async {\n  String? input = await displayTextInputDialog(\n    context,\n    src: _currentProxy,\n    title: tr(\"settings.proxy.title\"),\n    hint: tr(\"settings.proxy.hint\"),\n    desc: tr(\"settings.proxy.desc\"),\n  );\n  if (input != null) {\n    await method.setProxy(input);\n    _currentProxy = input;\n  }\n}\n\nWidget proxySetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.proxy.title\")),\n        subtitle: Text(currentProxyName()),\n        onTap: () async {\n          await inputProxy(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/Quality.dart",
    "content": "/// 图片质量\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _ImageQualityOriginal = \"original\";\nconst _ImageQualityLow = \"low\";\nconst _ImageQualityMedium = \"medium\";\nconst _ImageQualityHigh = \"high\";\n\n// const _LabelOriginal = \"原图\";\n// const _LabelLow = \"低\";\n// const _LabelMedium = \"中\";\n// const _LabelHigh = \"高\";\n\n// var _qualities = {\n//   _LabelOriginal: _ImageQualityOriginal,\n//   _LabelLow: _ImageQualityLow,\n//   _LabelMedium: _ImageQualityMedium,\n//   _LabelHigh: _ImageQualityHigh,\n// };\n\nMap<String, String> _qualities = {};\n\nconst _propertyName = \"quality\";\nlate String _currentQualityCode;\nconst _defaultValue = _ImageQualityOriginal;\n\nFuture<void> initQuality() async {\n  _qualities.addAll({\n    tr(\"settings.quality.original\"): _ImageQualityOriginal,\n    tr(\"settings.quality.low\"): _ImageQualityLow,\n    tr(\"settings.quality.medium\"): _ImageQualityMedium,\n    tr(\"settings.quality.high\"): _ImageQualityHigh,\n  });\n  _currentQualityCode = await method.loadProperty(_propertyName, _defaultValue);\n}\n\nString currentQualityCode() {\n  return _currentQualityCode;\n}\n\nString currentQualityName() {\n  for (var e in _qualities.entries) {\n    if (e.value == _currentQualityCode) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> chooseQuality(BuildContext context) async {\n  String? code = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.quality.choose\")),\n        children: <Widget>[\n          ..._qualities.entries.map(\n            (e) => SimpleDialogOption(\n              child: Text(e.key),\n              onPressed: () {\n                Navigator.of(context).pop(e.value);\n              },\n            ),\n          ),\n        ],\n      );\n    },\n  );\n  if (code != null) {\n    method.saveProperty(_propertyName, code);\n    _currentQualityCode = code;\n  }\n}\n\nWidget qualitySetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.quality.title\")),\n        subtitle: Text(currentQualityName()),\n        onTap: () async {\n          await chooseQuality(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderBackgroundColor.dart",
    "content": "import 'dart:ui';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\n\nfinal List<ReaderBackgroundColor> _colors = [];\n\n//  [\n//   ReaderBackgroundColor(\n//     \"黑色\",\n//     Colors.black,\n//   ),\n//   ReaderBackgroundColor(\n//     \"灰度\",\n//     Colors.grey,\n//   ),\n//   ReaderBackgroundColor(\n//     \"白色\",\n//     Colors.white,\n//   ),\n// ];\n\nconst _propertyName = \"readerBackgroundColor\";\nlate ReaderBackgroundColor readerBackgroundColor;\n\nColor get readerBackgroundColorObj => readerBackgroundColor.color;\n\nFuture<void> initReaderBackgroundColor() async {\n  _colors.addAll([\n    ReaderBackgroundColor(tr(\"settings.reader_background_color.black\"), Colors.black),\n    ReaderBackgroundColor(tr(\"settings.reader_background_color.gray\"), Colors.grey),\n    ReaderBackgroundColor(tr(\"settings.reader_background_color.white\"), Colors.white),\n  ]);\n  readerBackgroundColor =\n      _readerBackgroundColorFromString(await method.loadProperty(\n    _propertyName,\n    _colors[0].name,\n  ));\n}\n\nReaderBackgroundColor _readerBackgroundColorFromString(String string) {\n  for (var value in _colors) {\n    if (string == value.name) {\n      return value;\n    }\n  }\n  return _colors[0];\n}\n\nclass ReaderBackgroundColor {\n  final String name;\n  final Color color;\n\n  ReaderBackgroundColor(this.name, this.color);\n}\n\nFuture<void> chooseReaderBackgroundColor(BuildContext context) async {\n  Map<String, ReaderBackgroundColor> map = {};\n  for (var element in _colors) {\n    map[element.name] = element;\n  }\n  ReaderBackgroundColor? result = await chooseMapDialog<ReaderBackgroundColor>(\n    context,\n    map,\n    tr(\"settings.reader_background_color.choose\"),\n  );\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.name);\n    readerBackgroundColor = result;\n  }\n}\n\nWidget readerBackgroundColorSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.reader_background_color.title\")),\n        subtitle: Text(readerBackgroundColor.name),\n        onTap: () async {\n          await chooseReaderBackgroundColor(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderDirection.dart",
    "content": "/// 阅读器的方向\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nenum ReaderDirection {\n  TOP_TO_BOTTOM,\n  LEFT_TO_RIGHT,\n  RIGHT_TO_LEFT,\n}\n\n// const _types = {\n//   '从上到下': ReaderDirection.TOP_TO_BOTTOM,\n//   '从左到右': ReaderDirection.LEFT_TO_RIGHT,\n//   '从右到左': ReaderDirection.RIGHT_TO_LEFT,\n// };\n\nMap<String, ReaderDirection> _types = {};\n\nconst _propertyName = \"readerDirection\";\nlate ReaderDirection gReaderDirection;\n\nFuture<void> initReaderDirection() async {\n  _types.addAll({\n    tr(\"settings.reader_direction.top_to_bottom\"): ReaderDirection.TOP_TO_BOTTOM,\n    tr(\"settings.reader_direction.left_to_right\"): ReaderDirection.LEFT_TO_RIGHT,\n    tr(\"settings.reader_direction.right_to_left\"): ReaderDirection.RIGHT_TO_LEFT,\n  });\n  gReaderDirection = _pagerDirectionFromString(await method.loadProperty(\n      _propertyName, ReaderDirection.TOP_TO_BOTTOM.toString()));\n}\n\nReaderDirection _pagerDirectionFromString(String pagerDirectionString) {\n  for (var value in ReaderDirection.values) {\n    if (pagerDirectionString == value.toString()) {\n      return value;\n    }\n  }\n  return ReaderDirection.TOP_TO_BOTTOM;\n}\n\nString _currentReaderDirectionName() {\n  for (var e in _types.entries) {\n    if (e.value == gReaderDirection) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nvar gReaderDirectionName  = _currentReaderDirectionName;\n\n/// ?? to ActionButton And Event ??\nFuture<void> choosePagerDirection(BuildContext buildContext) async {\n  ReaderDirection? choose = await showDialog<ReaderDirection>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.reader_direction.choose\")),\n        children: _types.entries\n            .map((e) => SimpleDialogOption(\n                  child: Text(e.key),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.value);\n                  },\n                ))\n            .toList(),\n      );\n    },\n  );\n  if (choose != null) {\n    await method.saveProperty(_propertyName, choose.toString());\n    gReaderDirection = choose;\n  }\n}\n\nWidget readerDirectionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.reader_direction.title\")),\n        subtitle: Text(_currentReaderDirectionName()),\n        onTap: () async {\n          await choosePagerDirection(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderScrollByScreenPercentage.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\n\nconst _propertyName = \"readerScrollByScreenPercentage\";\n\nlate int _readerScrollByScreenPercentage;\n\nFuture initReaderScrollByScreenPercentage() async {\n  _readerScrollByScreenPercentage =\n      int.parse((await method.loadProperty(_propertyName, \"80\")));\n}\n\ndouble get readerScrollByScreenPercentage => _readerScrollByScreenPercentage / 100;\n\nint currentReaderScrollByScreenPercentage() => _readerScrollByScreenPercentage;\n\nFuture<void> setReaderScrollByScreenPercentage(int value) async {\n  _readerScrollByScreenPercentage = value;\n  await method.saveProperty(_propertyName, \"$value\");\n}\n\nWidget readerScrollByScreenPercentageSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.reader_scroll_by_screen_percentage.title\") + \" : $_readerScrollByScreenPercentage%\" + tr(\"settings.reader_scroll_by_screen_percentage.screen_size\")),\n        subtitle: Slider(\n          min: 5.toDouble(),\n          max: 110.toDouble(),\n          value: _readerScrollByScreenPercentage.toDouble(),\n          onChanged: (double value) async {\n            final va = value.toInt();\n            await setReaderScrollByScreenPercentage(va);\n            setState(() {});\n          },\n          divisions: (110 - 5),\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderSliderPosition.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport '../Common.dart';\n\nenum ReaderSliderPosition { BOTTOM, RIGHT, LEFT }\n\n// const _positionNames = {\n//   ReaderSliderPosition.BOTTOM: '下方',\n//   ReaderSliderPosition.RIGHT: '右侧',\n//   ReaderSliderPosition.LEFT: '左侧',\n// };\n\nMap<ReaderSliderPosition, String> _positionNames = {};\n\nconst _propertyName = \"readerSliderPosition\";\nlate ReaderSliderPosition _readerSliderPosition;\n\nFuture initReaderSliderPosition() async {\n  _positionNames.addAll({\n    ReaderSliderPosition.BOTTOM: tr(\"settings.reader_slider_position.bottom\"),\n    ReaderSliderPosition.RIGHT: tr(\"settings.reader_slider_position.right\"),\n    ReaderSliderPosition.LEFT: tr(\"settings.reader_slider_position.left\"),\n  });\n  _readerSliderPosition = _readerSliderPositionFromString(\n    await method.loadProperty(_propertyName, \"\"),\n  );\n}\n\nReaderSliderPosition _readerSliderPositionFromString(String str) {\n  for (var value in ReaderSliderPosition.values) {\n    if (str == value.toString()) return value;\n  }\n  return ReaderSliderPosition.BOTTOM;\n}\n\nReaderSliderPosition currentReaderSliderPosition() => _readerSliderPosition;\n\nString currentReaderSliderPositionName() =>\n    _positionNames[_readerSliderPosition] ?? \"\";\n\nFuture<void> chooseReaderSliderPosition(BuildContext context) async {\n  Map<String, ReaderSliderPosition> map = {};\n  _positionNames.forEach((key, value) {\n    map[value] = key;\n  });\n  ReaderSliderPosition? result =\n      await chooseMapDialog<ReaderSliderPosition>(context, map, tr(\"settings.reader_slider_position.choose\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    _readerSliderPosition = result;\n  }\n}\n\nWidget readerSliderPositionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.reader_slider_position.title\")),\n        subtitle: Text(currentReaderSliderPositionName()),\n        onTap: () async {\n          await chooseReaderSliderPosition(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderTwoPageDirection.dart",
    "content": "/// 阅读器的方向\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nenum ReaderTwoPageDirection {\n  CLOSE_TO,\n  PULL_AWAY,\n  EACH_CENTERED,\n}\n\n// const _types = {\n//   '靠近': ReaderTwoPageDirection.CLOSE_TO,\n//   '远离': ReaderTwoPageDirection.PULL_AWAY,\n//   '各自居中': ReaderTwoPageDirection.EACH_CENTERED,\n// };\n\nMap<ReaderTwoPageDirection, String> _types = {};\n\nconst _propertyName = \"readerTwoPageDirection\";\nlate ReaderTwoPageDirection gReaderTwoPageDirection;\n\nFuture<void> initReaderTwoPageDirection() async {\n  _types.addAll({\n    ReaderTwoPageDirection.CLOSE_TO: tr(\"settings.reader_two_page_direction.close_to\"),\n    ReaderTwoPageDirection.PULL_AWAY: tr(\"settings.reader_two_page_direction.pull_away\"),\n    ReaderTwoPageDirection.EACH_CENTERED: tr(\"settings.reader_two_page_direction.each_centered\"),\n  });\n  gReaderTwoPageDirection = _pagerDirectionFromString(await method.loadProperty(\n      _propertyName, ReaderTwoPageDirection.CLOSE_TO.toString()));\n}\n\nReaderTwoPageDirection _pagerDirectionFromString(String pagerDirectionString) {\n  for (var value in ReaderTwoPageDirection.values) {\n    if (pagerDirectionString == value.toString()) {\n      return value;\n    }\n  }\n  return ReaderTwoPageDirection.CLOSE_TO;\n}\n\nString _currentReaderTwoPageDirectionName() {\n  for (var e in _types.entries) {\n    if (e.key == gReaderTwoPageDirection) {\n      return e.value;\n    }\n  }\n  return '';\n}\n\nvar gReaderTwoPageDirectionName  = _currentReaderTwoPageDirectionName;\n\n/// ?? to ActionButton And Event ??\nFuture<void> chooseTwoPagerDirection(BuildContext buildContext) async {\n  ReaderTwoPageDirection? choose = await showDialog<ReaderTwoPageDirection>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.reader_two_page_direction.choose\")),\n        children: _types.entries\n            .map((e) => SimpleDialogOption(\n                  child: Text(e.value),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.key);\n                  },\n                ))\n            .toList(),\n      );\n    },\n  );\n  if (choose != null) {\n    await method.saveProperty(_propertyName, choose.toString());\n    gReaderTwoPageDirection = choose;\n  }\n}\n\nWidget readerTwoPageDirectionSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.reader_two_page_direction.title\")),\n        subtitle: Text(_currentReaderTwoPageDirectionName()),\n        onTap: () async {\n          await chooseTwoPagerDirection(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderType.dart",
    "content": "/// 阅读器的类型\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nenum ReaderType {\n  WEB_TOON,\n  WEB_TOON_ZOOM,\n  GALLERY,\n  // WEB_TOON_FREE_ZOOM,\n  TWO_PAGE_GALLERY,\n}\n\n// const _types = {\n//   'WebToon (默认)': ReaderType.WEB_TOON,\n//   'WebToon (双击放大)': ReaderType.WEB_TOON_ZOOM,\n//   '相册': ReaderType.GALLERY,\n//   'WebToon (ListView双击放大)\\n(此模式进度条无效)': ReaderType.WEB_TOON_FREE_ZOOM,\n//   '双页模式\\n(实验)': ReaderType.TWO_PAGE_GALLERY,\n// };\n\nMap<String, ReaderType> _types = {};\n\nenum TwoPageDirection {\n  LEFT_TO_RIGHT,\n  RIGHT_TO_LEFT,\n}\n\nString _twoPageDirectionName(TwoPageDirection direction) {\n  switch (direction) {\n    case TwoPageDirection.LEFT_TO_RIGHT:\n      return tr(\"settings.reader_type.left_to_right\");\n    case TwoPageDirection.RIGHT_TO_LEFT:\n      return tr(\"settings.reader_type.right_to_left\");\n  }\n}\n\nTwoPageDirection _twoPageDirectionFromString(String directionString) {\n  for (var value in TwoPageDirection.values) {\n    if (directionString == value.toString()) {\n      return value;\n    }\n  }\n  return TwoPageDirection.LEFT_TO_RIGHT;\n}\n\nconst _propertyName = \"readerType\";\nlate ReaderType _readerType;\n\nconst _twoPageDirectionPropertyName = \"twoPageDirection\";\nlate TwoPageDirection _twoPageDirection;\n\nTwoPageDirection get twoPageDirection => _twoPageDirection;\n\nFuture<dynamic> initReaderType() async {\n  _types.addAll({\n    tr(\"settings.reader_type.web_toon\"): ReaderType.WEB_TOON,\n    tr(\"settings.reader_type.web_toon_zoom\"): ReaderType.WEB_TOON_ZOOM,\n    tr(\"settings.reader_type.gallery\"): ReaderType.GALLERY,\n    // tr(\"settings.reader_type.web_toon_free_zoom\"): ReaderType.WEB_TOON_FREE_ZOOM,\n    tr(\"settings.reader_type.two_page_gallery\"): ReaderType.TWO_PAGE_GALLERY,\n  });\n  _readerType = _readerTypeFromString(\n      await method.loadProperty(_propertyName, ReaderType.WEB_TOON.toString()));\n  _twoPageDirection = _twoPageDirectionFromString(await method.loadProperty(\n      _twoPageDirectionPropertyName,\n      TwoPageDirection.LEFT_TO_RIGHT.toString()));\n}\n\nReaderType currentReaderType() {\n  return _readerType;\n}\n\nReaderType _readerTypeFromString(String pagerTypeString) {\n  for (var value in ReaderType.values) {\n    if (pagerTypeString == value.toString()) {\n      return value;\n    }\n  }\n  return ReaderType.WEB_TOON;\n}\n\nString currentReaderTypeName() {\n  for (var e in _types.entries) {\n    if (e.value == _readerType) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> choosePagerType(BuildContext buildContext) async {\n  ReaderType? t = await showDialog<ReaderType>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.reader_type.choose\")),\n        children: _types.entries\n            .map((e) => SimpleDialogOption(\n                  child: Text(e.key),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.value);\n                  },\n                ))\n            .toList(),\n      );\n    },\n  );\n  if (t != null) {\n    await method.saveProperty(_propertyName, t.toString());\n    _readerType = t;\n  }\n}\n\nWidget readerTypeSettings() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      List<Widget> children = [];\n      children.add(_readerTypeTile(context, setState));\n      // if (_readerType == ReaderType.TWO_PAGE_GALLERY) {\n        children.add(_twoPageDirectionTile(context, setState));\n      // }\n      return Column(children: children);\n    },\n  );\n}\n\nWidget _readerTypeTile(\n  BuildContext context,\n  void Function(void Function()) setState,\n) {\n  return ListTile(\n    title: Text(tr(\"settings.reader_type.title\")),\n    subtitle: Text(currentReaderTypeName()),\n    onTap: () async {\n      await choosePagerType(context);\n      setState(() {});\n    },\n  );\n}\n\nWidget _twoPageDirectionTile(\n  BuildContext context,\n  void Function(void Function()) setState,\n) {\n  return ListTile(\n    title: Text(tr(\"settings.reader_type.two_page_direction\")),\n    subtitle: Text(_twoPageDirectionName(_twoPageDirection)),\n    onTap: () async {\n      TwoPageDirection? t = await showDialog<TwoPageDirection>(\n        context: context,\n        builder: (BuildContext context) {\n          return SimpleDialog(\n            title: Text(tr(\"settings.reader_type.two_page_direction_choose\")),\n            children: TwoPageDirection.values\n                .map((e) => SimpleDialogOption(\n                      child: Text(_twoPageDirectionName(e)),\n                      onPressed: () {\n                        Navigator.of(context).pop(e);\n                      },\n                    ))\n                .toList(),\n          );\n        },\n      );\n      if (t != null) {\n        await method.saveProperty(_twoPageDirectionPropertyName, t.toString());\n        _twoPageDirection = t;\n        setState(() {});\n      }\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/ReaderZoomScale.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../Method.dart';\n\nconst _readerZoomMinPropertyName = \"readerZoomMinScale\";\nconst _readerZoomMaxPropertyName = \"readerZoomMaxScale\";\nconst _readerZoomDoubleTapPropertyName = \"readerZoomDoubleTapScale\";\n\nlate double _readerZoomMinScale;\nlate double _readerZoomMaxScale;\nlate double _readerZoomDoubleTapScale;\n\ndouble get readerZoomMinScale => _readerZoomMinScale;\ndouble get readerZoomMaxScale => _readerZoomMaxScale;\ndouble get readerZoomDoubleTapScale => _readerZoomDoubleTapScale;\n\nFuture<void> setReaderZoomMinScale(double value) async {\n  _readerZoomMinScale = value;\n  await method.saveProperty(\n    _readerZoomMinPropertyName,\n    value.toStringAsFixed(1),\n  );\n}\n\nFuture<void> setReaderZoomMaxScale(double value) async {\n  _readerZoomMaxScale = value;\n  await method.saveProperty(\n    _readerZoomMaxPropertyName,\n    value.toStringAsFixed(1),\n  );\n}\n\nFuture<void> setReaderZoomDoubleTapScale(double value) async {\n  _readerZoomDoubleTapScale = value;\n  await method.saveProperty(\n    _readerZoomDoubleTapPropertyName,\n    value.toStringAsFixed(1),\n  );\n}\n\nFuture<void> initReaderZoomScale() async {\n  _readerZoomMinScale =\n      double.tryParse(await method.loadProperty(_readerZoomMinPropertyName, \"1.0\")) ??\n          1.0;\n  _readerZoomMaxScale =\n      double.tryParse(await method.loadProperty(_readerZoomMaxPropertyName, \"2.0\")) ??\n          2.0;\n  _readerZoomDoubleTapScale =\n      double.tryParse(await method.loadProperty(_readerZoomDoubleTapPropertyName, \"2.0\")) ??\n          2.0;\n}\n\nWidget readerZoomMinScaleSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          \"${tr(\"settings.reader_zoom.out_title\")} : ${_readerZoomMinScale.toStringAsFixed(1)}x\",\n        ),\n        subtitle: Slider(\n          min: 0.1,\n          max: 1.0,\n          divisions: 9,\n          value: _readerZoomMinScale.clamp(0.1, 1.0).toDouble(),\n          label: \"${_readerZoomMinScale.toStringAsFixed(1)}x\",\n          onChanged: (double value) {\n            final newValue = (value * 10).roundToDouble() / 10;\n            setState(() {\n              _readerZoomMinScale = newValue;\n            });\n            setReaderZoomMinScale(newValue);\n          },\n        ),\n      );\n    },\n  );\n}\n\nWidget readerZoomMaxScaleSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          \"${tr(\"settings.reader_zoom.in_title\")} : ${_readerZoomMaxScale.toStringAsFixed(1)}x\",\n        ),\n        subtitle: Slider(\n          min: 1.0,\n          max: 30.0,\n          divisions: 29,\n          value: _readerZoomMaxScale.clamp(1.0, 30.0).toDouble(),\n          label: \"${_readerZoomMaxScale.toStringAsFixed(1)}x\",\n          onChanged: (double value) {\n            final newValue = value.roundToDouble();\n            setState(() {\n              _readerZoomMaxScale = newValue;\n            });\n            setReaderZoomMaxScale(newValue);\n          },\n        ),\n      );\n    },\n  );\n}\n\nWidget readerZoomDoubleTapScaleSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          \"${tr(\"settings.reader_zoom.double_tap_title\")} : ${_readerZoomDoubleTapScale.toStringAsFixed(1)}x\",\n        ),\n        subtitle: Slider(\n          min: 1.5,\n          max: 5.0,\n          divisions: 7,\n          value: _readerZoomDoubleTapScale.clamp(1.5, 5.0).toDouble(),\n          label: \"${_readerZoomDoubleTapScale.toStringAsFixed(1)}x\",\n          onChanged: (double value) {\n            final newValue = (value * 2).roundToDouble() / 2;\n            setState(() {\n              _readerZoomDoubleTapScale = newValue;\n            });\n            setReaderZoomDoubleTapScale(newValue);\n          },\n        ),\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/RecommendLinks.dart",
    "content": "import 'package:event/event.dart';\n\nimport '../Method.dart';\n\nvar recommendLinksEvent = Event<EventArgs>();\n\nMap<String, String> _recommendLinks = {};\n\nMap<String, String> currentRecommendLinks() => _recommendLinks;\n\nFuture<void> initRecommendLinks() async {\n  try {\n    _recommendLinks = await method.configLinks();\n  } catch (_) {\n    _recommendLinks = {};\n  }\n  recommendLinksEvent.broadcast();\n}\n"
  },
  {
    "path": "lib/basic/config/ShadowCategories.dart",
    "content": "/// 屏蔽的分类\n\nimport 'dart:convert';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\nimport '../store/Categories.dart';\nimport 'ShadowCategoriesEvent.dart';\n\nconst _propertyName = \"shadowCategories\";\nlate List<String> shadowCategories;\n\n/// 获取封印的类型\nFuture<List<String>> _loadShadowCategories() async {\n  var value = await method.loadProperty(_propertyName, jsonEncode(<String>[]));\n  return List.of(jsonDecode(value)).map((e) => \"$e\").toList();\n}\n\n/// 保存封印的类型\nFuture<dynamic> _saveShadowCategories(List<String> value) {\n  return method.saveProperty(_propertyName, jsonEncode(value));\n}\n\nFuture<void> initShadowCategories() async {\n  shadowCategories = await _loadShadowCategories();\n}\n\nFuture<void> _chooseShadowCategories(BuildContext context) async {\n  final theme = Theme.of(context);\n  final result = await showDialog<List<String>>(\n    context: context,\n    builder: (ctx) => _ShadowCategoriesDialog(\n      theme: theme,\n      items: storedCategories,\n      initialValue: shadowCategories,\n    ),\n  );\n  if (result != null) {\n    await _saveShadowCategories(result);\n    shadowCategories = result;\n    shadowCategoriesEvent.broadcast();\n  }\n}\n\nWidget shadowCategoriesActionButton(BuildContext context) {\n  return IconButton(\n    onPressed: () {\n      _chooseShadowCategories(context);\n    },\n    icon: const Icon(Icons.hide_source),\n  );\n}\n\nWidget shadowCategoriesSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.shadow_categories.title\")),\n        subtitle: Text(jsonEncode(shadowCategories)),\n        onTap: () async {\n          await _chooseShadowCategories(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nconst chooseShadowCategories = _chooseShadowCategories;\n\nclass _ShadowCategoriesDialog extends StatefulWidget {\n  final ThemeData theme;\n  final List<String> items;\n  final List<String> initialValue;\n\n  const _ShadowCategoriesDialog({\n    required this.theme,\n    required this.items,\n    required this.initialValue,\n  });\n\n  @override\n  State<_ShadowCategoriesDialog> createState() => _ShadowCategoriesDialogState();\n}\n\nclass _ShadowCategoriesDialogState extends State<_ShadowCategoriesDialog> {\n  late final Set<String> _selected = {...widget.initialValue};\n  String _query = \"\";\n\n  @override\n  Widget build(BuildContext context) {\n    final visibleItems = widget.items\n        .where((e) => _query.isEmpty || e.toLowerCase().contains(_query))\n        .toList();\n\n    return AlertDialog(\n      backgroundColor: widget.theme.scaffoldBackgroundColor,\n      title: Text(tr(\"settings.shadow_categories.title\")),\n      content: SizedBox(\n        width: double.maxFinite,\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            TextField(\n              decoration: InputDecoration(\n                hintText: tr(\"settings.shadow_categories.search_hint\"),\n              ),\n              onChanged: (v) {\n                setState(() {\n                  _query = v.trim().toLowerCase();\n                });\n              },\n            ),\n            const SizedBox(height: 12),\n            Flexible(\n              child: Scrollbar(\n                thumbVisibility: true,\n                child: ListView.builder(\n                  shrinkWrap: true,\n                  itemCount: visibleItems.length,\n                  itemBuilder: (ctx, index) {\n                    final item = visibleItems[index];\n                    final checked = _selected.contains(item);\n                    return CheckboxListTile(\n                      value: checked,\n                      dense: true,\n                      controlAffinity: ListTileControlAffinity.leading,\n                      title: Text(item, style: widget.theme.textTheme.bodyMedium),\n                      activeColor: widget.theme.primaryColor,\n                      onChanged: (v) {\n                        setState(() {\n                          if (v == true) {\n                            _selected.add(item);\n                          } else {\n                            _selected.remove(item);\n                          }\n                        });\n                      },\n                    );\n                  },\n                ),\n              ),\n            ),\n          ],\n        ),\n      ),\n      actions: [\n        TextButton(\n          onPressed: () => Navigator.of(context).pop(),\n          child: Text(tr(\"app.cancel\")),\n        ),\n        TextButton(\n          onPressed: () => Navigator.of(context).pop(_selected.toList()),\n          child: Text(tr(\"app.confirm\")),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/ShadowCategoriesEvent.dart",
    "content": "\nimport 'package:event/event.dart';\n\nvar shadowCategoriesEvent = Event<EventArgs>();\n"
  },
  {
    "path": "lib/basic/config/ShadowCategoriesMode.dart",
    "content": "/// 屏蔽方式\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'ShadowCategoriesEvent.dart';\n\nenum ShadowCategoriesMode {\n  BLACK_LIST,\n  WHITE_LIST,\n}\n\n// Map<String, ShadowCategoriesMode> _fullScreenActionMap = {\n//   \"黑名单\": ShadowCategoriesMode.BLACK_LIST,\n//   \"白名单\": ShadowCategoriesMode.WHITE_LIST,\n// };\n\nMap<String, ShadowCategoriesMode> _fullScreenActionMap = {};\n\nconst _propertyName = \"shadowCategoriesMode\";\nlate ShadowCategoriesMode _shadowCategoriesMode;\n\nFuture<void> initShadowCategoriesMode() async {\n  _fullScreenActionMap.addAll({\n    tr(\"settings.shadow_categories_mode.black_list\"): ShadowCategoriesMode.BLACK_LIST,\n    tr(\"settings.shadow_categories_mode.white_list\"): ShadowCategoriesMode.WHITE_LIST,\n  });\n  _shadowCategoriesMode = _shadowCategoriesModeFromString(await method.loadProperty(\n    _propertyName,\n    ShadowCategoriesMode.BLACK_LIST.toString(),\n  ));\n}\n\nShadowCategoriesMode currentShadowCategoriesMode() {\n  return _shadowCategoriesMode;\n}\n\nShadowCategoriesMode _shadowCategoriesModeFromString(String string) {\n  for (var value in ShadowCategoriesMode.values) {\n    if (string == value.toString()) {\n      return value;\n    }\n  }\n  return ShadowCategoriesMode.BLACK_LIST;\n}\n\nString _currentShadowCategoriesMode() {\n  for (var e in _fullScreenActionMap.entries) {\n    if (e.value == _shadowCategoriesMode) {\n      return e.key;\n    }\n  }\n  return '';\n}\n\nFuture<void> _chooseShadowCategoriesMode(BuildContext context) async {\n  ShadowCategoriesMode? result = await chooseMapDialog<ShadowCategoriesMode>(\n      context, _fullScreenActionMap, tr(\"settings.shadow_categories_mode.title\"));\n  if (result != null) {\n    await method.saveProperty(_propertyName, result.toString());\n    _shadowCategoriesMode = result;\n    shadowCategoriesEvent.broadcast();\n  }\n}\n\nWidget shadowCategoriesModeSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.shadow_categories_mode.title\")),\n        subtitle: Text(_currentShadowCategoriesMode()),\n        onTap: () async {\n          await _chooseShadowCategoriesMode(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nWidget shadowSwitchActionButton(BuildContext context) {\n  return IconButton(\n    onPressed: () {\n      _chooseShadowCategoriesMode(context);\n    },\n    icon: const Icon(Icons.do_not_disturb_on_outlined),\n  );\n}\n\nconst chooseShadowCategoriesMode = _chooseShadowCategoriesMode;\n"
  },
  {
    "path": "lib/basic/config/ShowCommentAtDownload.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"showCommentAtDownload\";\n\nlate bool _showCommentAtDownload;\n\nFuture initShowCommentAtDownload() async {\n  _showCommentAtDownload =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool showCommentAtDownload() {\n  return _showCommentAtDownload;\n}\n\nWidget showCommentAtDownloadSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _showCommentAtDownload,\n        title: Text(tr(\"settings.show_comment_at_download.title\")),\n        onChanged: (target) async {\n          await method.saveProperty(_propertyName, \"$target\");\n          _showCommentAtDownload = target;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/StartupPic.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:image_picker/image_picker.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nWidget setStartupPicTile(BuildContext context) {\n  return ListTile(\n    title: Text(tr(\"settings.startup_pic.title\")),\n    subtitle: Text(tr(\"settings.startup_pic.subtitle\")),\n    onTap: () {\n      if (Platform.isAndroid || Platform.isIOS) {\n        _updateStartupPicPhone(context);\n      } else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {\n        _updateStartupPicDesktop(context);\n      }\n    },\n  );\n}\n\nWidget clearStartupPicTile(BuildContext context) {\n  return ListTile(\n    title: Text(tr(\"settings.startup_pic.clear_title\")),\n    subtitle: Text(tr(\"settings.startup_pic.clear_subtitle\")),\n    onTap: () async {\n      await clearStartupPic(context);\n      defaultToast(context, tr(\"settings.startup_pic.clear_success\"));\n    },\n  );\n}\n\nFuture<void> _updateStartupPicPhone(BuildContext context) async {\n  final ImagePicker _picker = ImagePicker();\n  final XFile? image = await _picker.pickImage(source: ImageSource.gallery);\n  if (image != null) {\n    await image.saveTo(p.join(await method.dataLocal(), \"startup_pic\"));\n    defaultToast(context, tr(\"settings.startup_pic.update_success\"));\n  }\n}\n\nFuture<void> _updateStartupPicDesktop(BuildContext context) async {\n  FilePickerResult? result = await FilePicker.platform.pickFiles(\n    type: FileType.image,\n    allowMultiple: false,\n  );\n  if (result != null) {\n    final file = result.files.single;\n    final startupPicPath = p.join(await method.dataLocal(), \"startup_pic\");\n    final destination = File(startupPicPath);\n    await destination.create(recursive: true);\n    await File(file.path!).copy(destination.path);\n    defaultToast(context, tr(\"settings.startup_pic.update_success\"));\n  }\n}\n\nFuture<void> clearStartupPic(BuildContext context) async {\n  final startupPicPath = p.join(await method.dataLocal(), \"startup_pic\");\n  final file = File(startupPicPath);\n  if (await file.exists()) {\n    await file.delete();\n  }\n  defaultToast(context, tr(\"settings.startup_pic.clear_success\"));\n}\n"
  },
  {
    "path": "lib/basic/config/Themes.dart",
    "content": "/// 主题\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport '../Method.dart';\nimport 'Platform.dart';\n\n// 字体相关\n\nconst _fontFamilyProperty = \"fontFamily\";\n\nList<String> _fontFamily = [];\nList<String> _fontList = [];\n\nFuture initFont() async {\n  if (Platform.isIOS) {\n    try {\n      _fontList = await method.fontList();\n    } catch (e, s) {\n      print(\"获取字体列表失败: $e\\n$s\");\n      _fontList = [];\n    }\n  }\n  var defaultFont = \"\";\n  _fontFamily = (await method.loadProperty(_fontFamilyProperty, defaultFont))\n      .split(\",\")\n      .map((e) => e.trim())\n      .where((e) => e.isNotEmpty)\n      .toList();\n}\n\nThemeData _fontThemeData(bool dark) {\n  return ThemeData(\n    brightness: dark ? Brightness.dark : Brightness.light,\n    fontFamily: _fontFamily.isEmpty ? null : _fontFamily.first,\n    ////fontFamilyFallback: _fontFamily.length > 1 ? _fontFamily.sublist(1) : null,\n  );\n}\n\nFuture<void> inputFont(BuildContext context) async {\n  var font = await displayTextInputDialog(\n    context,\n    src: \"$_fontFamily\",\n    title: tr(\"settings.font.title\"),\n    hint: tr(\"settings.font.hint\"),\n    desc: tr(\"settings.font.input_hint\"),\n  );\n  if (font != null) {\n    await method.saveProperty(_fontFamilyProperty, font);\n    _fontFamily = font\n        .split(\",\")\n        .map((e) => e.trim())\n        .where((e) => e.isNotEmpty)\n        .toList();\n    _reloadTheme();\n  }\n}\n\nFuture<String?> chooseFontFromList(BuildContext context) async {\n  var font = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.font.choose_hint\")),\n        children: _fontList.map((e) {\n          return SimpleDialogOption(\n            child: Container(\n              padding: const EdgeInsets.all(8.0),\n              child: Text(\n                \"$e\\n 我能吞下玻璃而不伤身体\\n The quick brown fox jumps over the lazy dog.\",\n                style: TextStyle(fontFamily: e, fontSize: 16),\n              ),\n            ),\n            onPressed: () {\n              Navigator.of(context).pop(e);\n            },\n          );\n        }).toList(),\n      );\n    },\n  );\n  if (font == null || font.isEmpty) {\n    return null;\n  }\n  return font;\n}\n\nFuture<void> chooseFont(BuildContext context) async {\n  List<String> fonts = [];\n  while (true) {\n    var font = await chooseFontFromList(context);\n    if (font == null) {\n      break;\n    }\n    fonts.add(font);\n  }\n  await method.saveProperty(_fontFamilyProperty, fonts.join(\",\"));\n  _fontFamily = fonts;\n  _reloadTheme();\n}\n\nWidget fontSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.font.title\")),\n        subtitle: Text(_fontFamily.join(\",\")),\n        onTap: () async {\n          if (_fontList.isEmpty) {\n            await inputFont(context);\n          } else {\n            await chooseFont(context);\n          }\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\n//\nconst _enableStatusBarColorProperty = \"enableStatusBarColorProperty\";\nvar _enableStatusBarColor = false;\n\nWidget enableStatusBarColorSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.theme.enable_status_bar_color\")),\n        subtitle: Text(tr(\"settings.theme.enable_status_restart_hint\")),\n        value: _enableStatusBarColor,\n        onChanged: (bool value) async {\n          await method.saveProperty(\n              _enableStatusBarColorProperty, \"$value\");\n          _enableStatusBarColor = value;\n          _reloadTheme();\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\n// 主题相关\n\n// 主题包\nabstract class _ThemePackage {\n  String code();\n\n  String name();\n\n  ThemeData themeData(ThemeData rawData);\n\n  bool isDark();\n}\n\nclass _OriginTheme extends _ThemePackage {\n  @override\n  String code() => \"origin\";\n\n  @override\n  String name() => tr(\"settings.theme.origin\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData;\n\n  @override\n  bool isDark() => false;\n}\n\nclass _BookTheme extends _ThemePackage {\n  @override\n  String code() => \"book\";\n\n  @override\n  String name() => tr(\"settings.theme.book\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.dark,\n        appBarTheme: AppBarTheme(\n          elevation: 0,\n          foregroundColor: Colors.grey.shade700,\n          systemOverlayStyle: SystemUiOverlayStyle.light,\n          color: Colors.transparent,\n          iconTheme: IconThemeData(\n            color: Colors.grey.shade700,\n          ),\n          shape: Border(\n            bottom: BorderSide(color: Colors.grey.shade500, width: 2),\n          ),\n          titleTextStyle: TextStyle(\n            color: Colors.grey.shade700,\n            fontSize: 16,\n          ),\n        ),\n      );\n\n  @override\n  bool isDark() => false;\n}\n\nclass _PinkTheme extends _ThemePackage {\n  @override\n  String code() => \"pink\";\n\n  @override\n  String name() => tr(\"settings.theme.pink\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          primary: Colors.pink.shade200,\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(\n            statusBarColor:\n                _enableStatusBarColor ? Colors.pink.shade200 : null,\n          ),\n          color: Colors.pink.shade200,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.pink[300],\n          unselectedItemColor: Colors.grey[500],\n        ),\n        dividerColor: Colors.grey.shade200,\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n      );\n\n  @override\n  bool isDark() => false;\n}\n\nclass _BlackTheme extends _ThemePackage {\n  @override\n  String code() => \"black\";\n\n  @override\n  String name() => tr(\"settings.theme.black\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          primary: Colors.pink.shade200,\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(\n            statusBarColor:\n                _enableStatusBarColor ? Colors.grey.shade800 : null,\n          ),\n          color: Colors.grey.shade800,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.white,\n          unselectedItemColor: Colors.grey[400],\n          backgroundColor: Colors.grey.shade800,\n        ),\n        dividerColor: Colors.grey.shade200,\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n      );\n\n  @override\n  bool isDark() => false;\n}\n\nclass _DarkTheme extends _ThemePackage {\n  @override\n  String code() => \"dark\";\n\n  @override\n  String name() => tr(\"settings.theme.dark\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.dark,\n        colorScheme: ColorScheme.light(\n          primary: Colors.pink.shade200,\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(\n            statusBarColor:\n                _enableStatusBarColor ? const Color(0xFF1E1E1E) : null,\n          ),\n          color: const Color(0xFF1E1E1E),\n          foregroundColor: Colors.white,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.white,\n          unselectedItemColor: Colors.grey.shade300,\n          backgroundColor: Colors.grey.shade900,\n        ),\n        dividerColor: Colors.grey.shade500.withAlpha(70),\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n        sliderTheme: SliderThemeData(\n          activeTrackColor: Colors.pink.shade200,\n          inactiveTrackColor: Colors.pink.shade300.withAlpha(150),\n          thumbColor: Colors.pink.shade200,\n          overlayColor: Colors.pink.shade300.withAlpha(150),\n          thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),\n        ),\n      );\n\n  @override\n  bool isDark() => true;\n}\n\nclass _DustyBlueTheme extends _ThemePackage {\n  @override\n  String code() => \"dustyBlue\";\n\n  @override\n  String name() => tr(\"settings.theme.dusty_blue\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.dark,\n        scaffoldBackgroundColor: Color.alphaBlend(\n          const Color(0x11999999),\n          const Color(0xff20253b),\n        ),\n        cardColor: Color.alphaBlend(\n          const Color(0x11AAAAAA),\n          const Color(0xff20253b),\n        ),\n        colorScheme: ColorScheme.light(\n          primary: Colors.blue.shade200,\n          secondary: Colors.blue.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(\n            statusBarColor:\n                _enableStatusBarColor ? const Color(0xff20253b) : null,\n          ),\n          color: const Color(0xff20253b),\n          foregroundColor: Colors.white,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        dialogTheme: const DialogTheme(\n          backgroundColor: Color(0xff20253b),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          backgroundColor: const Color(0xff191b26),\n          selectedItemColor: Colors.blue.shade200,\n          unselectedItemColor: Colors.grey.shade500,\n        ),\n        dividerColor: Colors.grey.shade800,\n        primaryColor: Colors.blue.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.blue.shade200,\n          selectionColor: Colors.blue.shade900,\n          selectionHandleColor: Colors.blue.shade800,\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.blue.shade500),\n          ),\n        ),\n        sliderTheme: SliderThemeData(\n          activeTrackColor: Colors.blue.shade200,\n          inactiveTrackColor: Colors.blue.shade300.withAlpha(150),\n          thumbColor: Colors.blue.shade200,\n          overlayColor: Colors.blue.shade300.withAlpha(150),\n          thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),\n        ),\n      );\n\n  @override\n  bool isDark() => true;\n}\n\nclass _DarkBlackTheme extends _ThemePackage {\n  @override\n  String code() => \"dark_black\";\n\n  @override\n  String name() => tr(\"settings.theme.dark_black\");\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData.copyWith(\n        brightness: Brightness.dark,\n        colorScheme: ColorScheme.light(\n          primary: Colors.pink.shade200,\n          secondary: Colors.pink.shade200,\n        ),\n        scaffoldBackgroundColor: Colors.black,\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light.copyWith(\n            statusBarColor: _enableStatusBarColor\n                ? const Color.fromARGB(0xff, 10, 10, 10)\n                : null,\n          ),\n          color: const Color.fromARGB(0xff, 10, 10, 10),\n          foregroundColor: Colors.white,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.white,\n          unselectedItemColor: Colors.grey.shade300,\n          backgroundColor: const Color.fromARGB(0xff, 10, 10, 10),\n        ),\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n        dividerColor: const Color.fromARGB(0xff, 64, 64, 64),\n      );\n\n  @override\n  bool isDark() => true;\n}\n\nfinal _themePackages = <_ThemePackage>[\n  _OriginTheme(),\n  _PinkTheme(),\n  _BlackTheme(),\n  _DarkTheme(),\n  _DustyBlueTheme(),\n  _DarkBlackTheme(),\n  _BookTheme(),\n];\n\n// 主题更换事件\nvar themeEvent = Event<EventArgs>();\n\nconst _nightModePropertyName = \"androidNightMode\";\nconst _lightThemePropertyName = \"theme\";\nconst _darkThemePropertyName = \"theme.dark\";\nconst _defaultLightThemeCode = \"pink\";\nconst _defaultDarkThemeCode = \"dark\";\nbool androidNightModeDisplay = false;\nbool androidNightMode = false;\n\nString? _lightThemeCode;\nThemeData? _lightThemeData;\nString? _darkThemeCode;\nThemeData? _darkThemeData;\n\n// _changeThemeByCode\n\nString _codeToName(String? code) {\n  for (var package in _themePackages) {\n    if (code == package.code()) {\n      return package.name();\n    }\n  }\n  return \"\";\n}\n\nString currentLightThemeName() {\n  return _codeToName(_lightThemeCode);\n}\n\nString currentDarkThemeName() {\n  return _codeToName(_darkThemeCode);\n}\n\nThemeData? currentLightThemeData() {\n  return _lightThemeData;\n}\n\nThemeData? currentDarkThemeData() {\n  return _darkThemeData;\n}\n\n// 根据Code选择主题, 并发送主题更换事件\n\nThemeData? _themeByCode(String? themeCode) {\n  for (var package in _themePackages) {\n    if (themeCode == package.code()) {\n      return package.themeData(_fontThemeData(package.isDark()));\n    }\n  }\n  return null;\n}\n\nvoid _reloadTheme() {\n  _lightThemeData = _themeByCode(_lightThemeCode);\n  if (androidNightMode) {\n    _darkThemeData = _themeByCode(_darkThemeCode);\n  } else {\n    _darkThemeData = _lightThemeData;\n  }\n  themeEvent.broadcast();\n}\n\nFuture<dynamic> initTheme() async {\n  androidNightModeDisplay = androidVersion >= 29 || Platform.isIOS;\n  androidNightMode =\n      await method.loadProperty(_nightModePropertyName, \"true\") == \"true\";\n  _lightThemeCode = await method.loadProperty(\n      _lightThemePropertyName, _defaultLightThemeCode);\n  _darkThemeCode =\n      await method.loadProperty(_darkThemePropertyName, _defaultDarkThemeCode);\n  _enableStatusBarColor =\n      await method.loadProperty(_enableStatusBarColorProperty, \"false\") == \"true\";\n  _reloadTheme();\n}\n\n// 选择主题的对话框\nFuture<String?> _chooseTheme(BuildContext buildContext) {\n  return showDialog<String>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return StatefulBuilder(\n          builder: (BuildContext context, StateSetter setState) {\n        var list = <SimpleDialogOption>[];\n        list.addAll(_themePackages.map((e) => SimpleDialogOption(\n              child: Text(e.name()),\n              onPressed: () {\n                Navigator.of(context).pop(e.code());\n              },\n            )));\n        return SimpleDialog(\n          title: Text(tr(\"settings.theme.choose_theme\")),\n          children: list,\n        );\n      });\n    },\n  );\n}\n\nFuture<dynamic> chooseLightTheme(BuildContext buildContext) async {\n  String? theme = await _chooseTheme(buildContext);\n  if (theme != null) {\n    await method.saveProperty(_lightThemePropertyName, theme);\n    _lightThemeCode = theme;\n    _reloadTheme();\n  }\n}\n\nFuture<dynamic> chooseDarkTheme(BuildContext buildContext) async {\n  String? theme = await _chooseTheme(buildContext);\n  if (theme != null) {\n    await method.saveProperty(_darkThemePropertyName, theme);\n    _darkThemeCode = theme;\n    _reloadTheme();\n  }\n}\n\nFuture setAndroidNightMode(bool value) async {\n  await method.saveProperty(_nightModePropertyName, \"$value\");\n  androidNightMode = value;\n  _reloadTheme();\n}\n"
  },
  {
    "path": "lib/basic/config/ThreeKeepRight.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"threeKeepRight\";\n\nlate bool _threeKeepRight;\n\nbool get threeKeepRight => _threeKeepRight;\n\nFuture initThreeKeepRight() async {\n  _threeKeepRight = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget threeKeepRightSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.three_keep_right.title\")),\n        value: _threeKeepRight,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _threeKeepRight = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/TimeOffsetHour.dart",
    "content": "/// 时区设置\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"timeOffsetHour\";\nint _timeOffsetHour = 8;\n\nFuture<void> initTimeZone() async {\n  _timeOffsetHour = int.parse(await method.loadProperty(_propertyName, \"8\"));\n}\n\nint currentTimeOffsetHour() {\n  return _timeOffsetHour;\n}\n\nFuture<void> _chooseTimeZone(BuildContext context) async {\n  List<String> timeZones = [];\n  for (var i = -12; i <= 12; i++) {\n    var str = i.toString();\n    if (!str.startsWith(\"-\")) {\n      str = \"+\" + str;\n    }\n    timeZones.add(str);\n  }\n  String? result = await chooseListDialog<String>(context, tr(\"settings.time_zone.title\"), timeZones);\n  if (result != null) {\n    if (result.startsWith(\"+\")) {\n      result = result.substring(1);\n    }\n    _timeOffsetHour = int.parse(result);\n    await method.saveProperty(_propertyName, result);\n  }\n}\n\nWidget timeZoneSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      var c = \"$_timeOffsetHour\";\n      if (!c.startsWith(\"-\")) {\n        c = \"+\" + c;\n      }\n      return ListTile(\n        title: Text(tr(\"settings.time_zone.title\")),\n        subtitle: Text(c),\n        onTap: () async {\n          await _chooseTimeZone(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/TimeoutLock.dart",
    "content": "/// 自动清理\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\n// const _lockTimeOutMap = {\n//   \"一小时\": \"${60 * 60}\",\n//   \"十分钟\": \"${60 * 10}\",\n//   \"三分钟\": \"${60 * 3}\",\n//   \"一分钟\": \"${60}\",\n//   \"十秒\": \"${10}\",\n//   \"一秒\": \"${1}\",\n//   \"不锁定\": \"${0}\",\n// };\n\nMap<String, String> _lockTimeOutMap = {};\n\nlate String _lockTimeOutSec;\n\nint get timeoutLock => int.tryParse(_lockTimeOutSec) ?? 0;\n\nFuture<dynamic> initLockTimeOut() async {\n  _lockTimeOutMap.addAll({\n    tr(\"settings.timeout_lock.1_hour\"): \"${60 * 60}\",\n    tr(\"settings.timeout_lock.10_minutes\"): \"${60 * 10}\",\n    tr(\"settings.timeout_lock.3_minutes\"): \"${60 * 3}\",\n    tr(\"settings.timeout_lock.1_minute\"): \"${60}\",\n    tr(\"settings.timeout_lock.10_seconds\"): \"${10}\",\n    tr(\"settings.timeout_lock.1_second\"): \"${1}\",\n    tr(\"settings.timeout_lock.no_lock\"): \"${0}\",\n  });\n  _lockTimeOutSec = await method.loadProperty(\"lockTimeOutSec\", \"${0}\");\n}\n\nString _currentLockTimeOutSec() {\n  for (var value in _lockTimeOutMap.entries) {\n    if (value.value == _lockTimeOutSec) {\n      return value.key;\n    }\n  }\n  return \"$_lockTimeOutSec seconds\";\n}\n\nFuture<void> _chooseLockTimeOutSec(BuildContext context) async {\n  String? choose = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.timeout_lock.title\")),\n        children: <Widget>[\n          ..._lockTimeOutMap.entries.map(\n            (e) => SimpleDialogOption(\n              child: Text(e.key),\n              onPressed: () {\n                Navigator.of(context).pop(e.value);\n              },\n            ),\n          ),\n        ],\n      );\n    },\n  );\n  if (choose != null) {\n    await method.saveProperty(\"lockTimeOutSec\", choose);\n    _lockTimeOutSec = choose;\n  }\n}\n\nWidget lockTimeOutSecSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.timeout_lock.title\")),\n        subtitle: Text(_currentLockTimeOutSec()),\n        onTap: () async {\n          await _chooseLockTimeOutSec(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nWidget lockTimeOutSecNotice() {\n  return ListTile(\n    subtitle: Text(tr(\"settings.timeout_lock.notice\")),\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/UseApiLoadImage.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\n\nlate bool _currentUseApiLoadImage;\n\nFuture<void> initUseApiLoadImage() async {\n  _currentUseApiLoadImage = await method.getUseApiClientLoadImage() == \"true\";\n}\n\nString currentUseApiLoadImageName() =>\n    _currentUseApiLoadImage ? tr(\"app.yes\") : tr(\"app.no\");\n\nWidget useApiLoadImageSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"net.use_api_load_image\")),\n        subtitle: Text(currentUseApiLoadImageName()),\n        value: _currentUseApiLoadImage,\n        onChanged: (bool value) async {\n          _currentUseApiLoadImage = !_currentUseApiLoadImage;\n          await method\n              .setUseApiClientLoadImage(_currentUseApiLoadImage.toString());\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nFuture<void> chooseUseApiLoadImage(BuildContext context) async {\n  String? choose = await showDialog<String>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"net.use_api_load_image\")),\n        children: <Widget>[\n          SimpleDialogOption(\n            child: Text(tr(\"app.yes\")),\n            onPressed: () {\n              Navigator.of(context).pop(\"true\");\n            },\n          ),\n          SimpleDialogOption(\n            child: Text(tr(\"app.no\")),\n            onPressed: () {\n              Navigator.of(context).pop(\"false\");\n            },\n          ),\n        ],\n      );\n    },\n  );\n  if (choose != null) {\n    await method.setUseApiClientLoadImage(choose);\n    _currentUseApiLoadImage = choose == \"true\";\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/UsingRightClickPop.dart",
    "content": "/// 自动全屏\n\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"usingRightClickPop\";\nlate bool _usingRightClickPop;\n\nFuture<void> initUsingRightClickPop() async {\n  _usingRightClickPop =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentUsingRightClickPop() {\n  return _usingRightClickPop;\n}\n\nWidget usingRightClickPopSetting() {\n  if (!(Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {\n    return Container();\n  }\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.using_right_click_pop.title\")),\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"${value ? tr(\"app.yes\") : tr(\"app.no\")}\");\n          _usingRightClickPop = value;\n          setState(() {});\n        },\n        value: _usingRightClickPop,\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/Version.dart",
    "content": "import 'dart:async' show Future;\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart' show rootBundle;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\n\nimport '../Method.dart';\nimport 'IgnoreUpgradeConfirm.dart';\n\nconst _versionAssets = 'lib/assets/version.txt';\n\nlate String _version;\nString? _latestVersion;\nString? _latestVersionInfo;\nString? _downloadUrl;\n\nFuture initVersion() async {\n  // 当前版本\n  try {\n    _version = (await rootBundle.loadString(_versionAssets)).trim();\n  } catch (e) {\n    _version = \"dirty\";\n  }\n}\n\nvar versionEvent = Event<EventArgs>();\n\nString currentVersion() {\n  return _version;\n}\n\nString? latestVersion() {\n  return _latestVersion;\n}\n\nString? latestVersionInfo() {\n  return _latestVersionInfo;\n}\n\nString? downloadUrl() {\n  return _downloadUrl;\n}\n\nFuture autoCheckNewVersion() {\n  // if (!isPro) {\n  //   return Future.value();\n  // }\n  return _versionCheck();\n}\n\nFuture manualCheckNewVersion(BuildContext context) async {\n  try {\n    defaultToast(context, \"检查更新中\");\n    await _versionCheck();\n    defaultToast(context, \"检查更新成功\");\n  } catch (e) {\n    defaultToast(context, \"检查更新失败 : $e\");\n  }\n}\n\nbool dirtyVersion() {\n  return \"dirty\" == _version;\n}\n\n// maybe exception\nFuture _versionCheck() async {\n  if (!dirtyVersion()) {\n    var config = await method.appConfig();\n    if (config[\"latestVersion\"] != null) {\n      String latestVersion = config[\"latestVersion\"];\n      if (latestVersion != _version && _isServerNewer(_version, latestVersion)) {\n        _latestVersion = latestVersion;\n        _latestVersionInfo = config[\"changeLog\"] ?? \"\";\n        _downloadUrl = config[\"downloadUrl\"];\n      }\n    }\n  } // else dirtyVersion\n  versionEvent.broadcast();\n}\n\nbool _isServerNewer(String current, String latest) {\n  final c = _SemVer.parse(current);\n  final l = _SemVer.parse(latest);\n  if (c == null || l == null) return false;\n  if (l.major != c.major) return l.major > c.major;\n  if (l.minor != c.minor) return l.minor > c.minor;\n  return l.patch > c.patch;\n}\n\nvar _display = true;\n\nvoid versionPop(BuildContext context) {\n  final latest = latestVersion();\n  if (latest == null || !_display) {\n    return;\n  }\n\n  final force = _isForceUpgrade(currentVersion(), latest);\n  if (!force || ignoreUpgradeConfirm) {\n    return;\n  }\n\n  _display = false;\n  TopConfirm.topConfirm(\n    context,\n    \"发现新版本\",\n    force ? \"发现新版本 $latest，请立即更新后继续使用\" : \"发现新版本 $latest，建议更新\",\n    force: force,\n    primaryText: \"去下载\",\n    onPrimary: () async {\n      _openRelease(context);\n    },\n  );\n}\n\nclass _SemVer {\n  final int major;\n  final int minor;\n  final int patch;\n\n  const _SemVer(this.major, this.minor, this.patch);\n\n  static _SemVer? parse(String input) {\n    // todo remove first v\n    if (input.startsWith('v')) {\n      input = input.substring(1);\n    }\n    final regExp = RegExp(r'^(\\d+)\\.(\\d+)\\.(\\d+)$');\n    final m = regExp.firstMatch(input);\n    if (m == null) return null;\n    return _SemVer(\n      int.parse(m.group(1)!),\n      int.parse(m.group(2)!),\n      int.parse(m.group(3)!),\n    );\n  }\n\n  @override\n  String toString() {\n    return '$major.$minor.$patch'; \n  }\n}\n\nbool _isForceUpgrade(String current, String latest) {\n  print(\"checking force upgrade...\");\n  print(\"current version string: $current, latest version string: $latest\");\n  final c = _SemVer.parse(current);\n  final l = _SemVer.parse(latest);\n  print(\"current version: $c, latest version: $l\");\n  if (c == null || l == null) return false;\n\n  if (l.major != c.major) return true;\n  if (l.minor != c.minor) return true;\n  return false;\n}\n\nFuture<void> _openRelease(BuildContext context) async {\n  try {\n    if (_downloadUrl != null && _downloadUrl!.isNotEmpty) {\n      await openUrl(_downloadUrl!);\n    }\n  } catch (_) {\n    defaultToast(context, \"下载失败\");\n  }\n}\n\nclass TopConfirm {\n  static topConfirm(BuildContext context, String title, String message,\n      {bool force = false,\n      String primaryText = \"朕知道了\",\n      Future<void> Function()? onPrimary,\n      Function()? afterIKnown}) {\n    late OverlayEntry overlayEntry;\n    overlayEntry = OverlayEntry(builder: (BuildContext context) {\n      return LayoutBuilder(\n        builder: (\n          BuildContext context,\n          BoxConstraints constraints,\n        ) {\n          var mq = MediaQuery.of(context).size.width - 30;\n          return Material(\n            color: Colors.transparent,\n            child: Container(\n              width: constraints.maxWidth,\n              height: constraints.maxHeight,\n              decoration: BoxDecoration(\n                color: Colors.black.withOpacity(.35),\n              ),\n              child: Column(\n                children: [\n                  Expanded(child: Container()),\n                  Container(\n                    width: mq,\n                    decoration: BoxDecoration(\n                      color: Colors.white,\n                      borderRadius: BorderRadius.circular(10),\n                    ),\n                    child: Column(\n                      children: [\n                        Container(height: 30),\n                        Text(\n                          title,\n                          style: const TextStyle(\n                            color: Colors.black,\n                            fontSize: 28,\n                          ),\n                        ),\n                        Container(height: 15),\n                        Text(\n                          message,\n                          style: const TextStyle(\n                            color: Colors.black,\n                            fontSize: 16,\n                          ),\n                        ),\n                        Container(height: 25),\n                        MaterialButton(\n                          elevation: 0,\n                          color: Colors.black.withOpacity(.1),\n                          onPressed: () {\n                            if (onPrimary != null) {\n                              onPrimary();\n                            }\n                            if (!force) {\n                              overlayEntry.remove();\n                            }\n                            afterIKnown?.call();\n                          },\n                          child: Text(primaryText),\n                        ),\n                        Container(height: 30),\n                      ],\n                    ),\n                  ),\n                  Expanded(child: Container()),\n                ],\n              ),\n            ),\n          );\n        },\n      );\n    });\n    final overlay = Overlay.of(context);\n    overlay?.insert(overlayEntry);\n  }\n}\n"
  },
  {
    "path": "lib/basic/config/VolumeController.dart",
    "content": "/// 音量键翻页\n\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\nimport 'package:pikapika/i18.dart';\n\nconst _propertyName = \"volumeController\";\nlate bool volumeController;\n\nFuture<void> initVolumeController() async {\n  volumeController =\n      (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nWidget volumeControllerSetting() {\n  if (Platform.isAndroid) {\n    return StatefulBuilder(builder:\n        (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n          value: volumeController,\n          title: Text(tr('settings.volume_controller.title')),\n          onChanged: (target) async {\n            await method.saveProperty(_propertyName, \"$target\");\n            volumeController = target;\n            setState(() {});\n          });\n    });\n  }\n  return Container();\n}\n"
  },
  {
    "path": "lib/basic/config/VolumeNextChapter.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"volumeNextChapter\";\n\nlate bool _volumeNextChapter;\n\nFuture initVolumeNextChapter() async {\n  _volumeNextChapter =\n      (await method.loadProperty(_propertyName, \"true\")) == \"true\";\n}\n\nbool volumeNextChapter() {\n  return _volumeNextChapter;\n}\n\nWidget volumeNextChapterSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr(\"settings.volume_next_chapter.title\")),\n        value: _volumeNextChapter,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _volumeNextChapter = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/WebDav.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Common.dart';\nimport '../Method.dart';\nimport 'IsPro.dart';\n\nconst _webdavRootPropertyName = \"webdavRoot\";\nconst _webdavUsernamePropertyName = \"webdavUsername\";\nconst _webdavPasswordPropertyName = \"webdavPassword\";\nconst _autoSyncHistoryToWebdavPropertyName = \"autoSyncHistoryToWebdav\";\nconst _useLocalFavoritePropertyName = \"useLocalFavorite\";\nconst _autoSyncLocalFavoriteToWebdavPropertyName = \"autoSyncLocalFavoriteToWebdav\";\n\nlate String _webdavRoot;\nlate String _webdavUsername;\nlate String _webdavPassword;\nlate bool _autoSyncHistoryToWebdav;\nlate bool _useLocalFavorite;\nlate bool _autoSyncLocalFavoriteToWebdav;\n\nString get webdavRoot => _webdavRoot;\nString get webdavUsername => _webdavUsername;\nString get webdavPassword => _webdavPassword;\nbool get useLocalFavorite => _useLocalFavorite;\n\nfinal useLocalFavoriteEvent = Event();\n\nFuture initWebDav() async {\n  _webdavRoot = await method.loadProperty(\n    _webdavRootPropertyName,\n    \"\",\n  );\n  if (_webdavRoot == \"https://your.dav.host/folder\") {\n    _webdavRoot = \"\";\n  }\n  _webdavUsername = await method.loadProperty(\n    _webdavUsernamePropertyName,\n    \"\",\n  );\n  _webdavPassword = await method.loadProperty(\n    _webdavPasswordPropertyName,\n    \"\",\n  );\n  _useLocalFavorite = await method.loadProperty(\n        _useLocalFavoritePropertyName,\n        \"false\",\n      ) ==\n      \"true\";\n  if (!isPro) {\n    _autoSyncHistoryToWebdav = false;\n    _autoSyncLocalFavoriteToWebdav = false;\n    return;\n  }\n  _autoSyncHistoryToWebdav = await method.loadProperty(\n        _autoSyncHistoryToWebdavPropertyName,\n        \"false\",\n      ) ==\n      \"true\";\n  _autoSyncLocalFavoriteToWebdav = await method.loadProperty(\n        _autoSyncLocalFavoriteToWebdavPropertyName,\n        \"false\",\n      ) ==\n      \"true\";\n  if (_autoSyncLocalFavoriteToWebdav && _webdavRoot.isNotEmpty) {\n    try {\n      await method.mergeLocalFavoritesFromWebDav(\n        _webdavRoot,\n        _webdavUsername,\n        _webdavPassword,\n      );\n    } catch (e, s) {\n      print(\"$e\\n$s\");\n    }\n  }\n}\n\nFuture setUseLocalFavorite(bool value) async {\n  await method.saveProperty(\n    _useLocalFavoritePropertyName,\n    value ? \"true\" : \"false\",\n  );\n  _useLocalFavorite = value;\n  useLocalFavoriteEvent.broadcast();\n}\n\nWidget useLocalFavoriteSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _useLocalFavorite,\n        onChanged: (bool value) async {\n          await setUseLocalFavorite(value);\n          setState(() {});\n        },\n        title: Text(tr(\"settings.use_local_favorite\")),\n        subtitle: Text(tr(\"settings.use_local_favorite_desc\")),\n      );\n    },\n  );\n}\n\nFuture syncLocalFavoriteToWebdav(BuildContext context) async {\n  if (_webdavRoot.isEmpty) {\n    defaultToast(context, tr(\"settings.webdav.not_set\"));\n    return;\n  }\n  try {\n    await method.mergeLocalFavoritesFromWebDav(\n      _webdavRoot,\n      _webdavUsername,\n      _webdavPassword,\n    );\n    defaultToast(context, tr(\"settings.local_favorite_sync.sync_success\"));\n  } catch (e, s) {\n    print(\"$e\\n$s\");\n    defaultToast(context, tr(\"settings.local_favorite_sync.sync_failed\"));\n  }\n}\n\nWidget localFavoriteSyncAutoTile() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        value: _autoSyncLocalFavoriteToWebdav && isPro,\n        onChanged: isPro\n            ? (bool value) async {\n                await method.saveProperty(\n                  _autoSyncLocalFavoriteToWebdavPropertyName,\n                  value ? \"true\" : \"false\",\n                );\n                setState(() {\n                  _autoSyncLocalFavoriteToWebdav = value;\n                });\n                if (value) {\n                  syncLocalFavoriteToWebdav(context);\n                }\n              }\n            : null,\n        title: Text(\n          tr(\"settings.local_favorite_sync.auto_sync\") +\n              (isPro ? \"\" : \" (${tr('app.pro')})\"),\n          style: TextStyle(\n            color: isPro ? null : Colors.grey,\n          ),\n        ),\n        subtitle: Text(tr(\"settings.local_favorite_sync.auto_sync_desc\")),\n      );\n    },\n  );\n}\n\nWidget localFavoriteSyncManualTile() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        onTap: () async {\n          await syncLocalFavoriteToWebdav(context);\n        },\n        title: Text(tr(\"settings.local_favorite_sync.manual_sync\")),\n      );\n    },\n  );\n}\n\nFuture syncWebDavIfAuto(BuildContext context) async {\n  if (_autoSyncHistoryToWebdav) {\n    try {\n      await method.mergeHistoriesFromWebDav(\n        _webdavRoot,\n        _webdavUsername,\n        _webdavPassword,\n        \"pk.histories\",\n        \"all\",\n      );\n    } catch (e, s) {\n      print(\"$e\\n$s\");\n      defaultToast(context, tr(\"settings.webdav.sync_failed\"));\n    }\n  }\n}\n\nFuture syncHistoryToWebdav(BuildContext context) async {\n  try {\n    await method.mergeHistoriesFromWebDav(\n      _webdavRoot,\n      _webdavUsername,\n      _webdavPassword,\n      \"pk.histories\",\n      \"all\",\n    );\n    defaultToast(context, tr(\"settings.webdav.sync_success\"));\n  } catch (e, s) {\n    print(\"$e\\n$s\");\n    defaultToast(context, tr(\"settings.webdav.sync_failed\"));\n  }\n}\n\nFuture uploadHistoryToWebdav(BuildContext context) async {\n  try {\n    await method.mergeHistoriesFromWebDav(\n      _webdavRoot,\n      _webdavUsername,\n      _webdavPassword,\n      \"pk.histories\",\n      \"up\",\n    );\n    defaultToast(context, tr(\"settings.webdav.sync_success\"));\n  } catch (e, s) {\n    print(\"$e\\n$s\");\n    defaultToast(context, tr(\"settings.webdav.sync_failed\"));\n  }\n}\n\nList<Widget> webDavSettings(BuildContext context) {\n  return [\n    //\n    StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n            title: Text(\n              tr(\"settings.webdav.path\"),\n            ),\n            subtitle:\n                Text(_webdavRoot.isEmpty ? tr(\"settings.webdav.not_set\") : _webdavRoot),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _webdavRoot,\n                title: tr(\"settings.webdav.path\"),\n                hint: tr(\"settings.webdav.path_hint\"),\n              );\n              if (input != null) {\n                await method.saveProperty(_webdavRootPropertyName, input);\n                setState(() {\n                  _webdavRoot = input == \"https://your.dav.host/folder\" ? \"\" : input;\n                });\n              }\n            });\n      },\n    ),\n    //\n    StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(\n              tr(\"settings.webdav.username\"),\n            ),\n            subtitle: Text(_webdavUsername),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _webdavUsername,\n                title: tr(\"settings.webdav.username\"),\n                hint: tr(\"settings.webdav.username_hint\"),\n              );\n              if (input != null) {\n                await method.saveProperty(_webdavUsernamePropertyName, input);\n                setState(() {\n                  _webdavUsername = input;\n                });\n              }\n            });\n      },\n    ),\n    //\n    StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n            title: Text(\n              tr(\"settings.webdav.password\"),\n            ),\n            subtitle: Text(_webdavPassword),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _webdavPassword,\n                title: tr(\"settings.webdav.password\"),\n                hint: tr(\"settings.webdav.password_hint\"),\n              );\n              if (input != null) {\n                await method.saveProperty(_webdavPasswordPropertyName, input);\n                setState(() {\n                  _webdavPassword = input;\n                });\n              }\n            });\n      },\n    ),\n    //\n    ListTile(\n      title: Text(tr('settings.history_sync')),\n      dense: true,\n    ),\n    //\n    StatefulBuilder(\n      builder: (BuildContext context, void Function(void Function()) setState) {\n        return ListTile(\n          title: Text(\n            tr(\"settings.webdav.auto_sync_history_to_webdav\") +\n                (isPro ? \"\" : \"(${tr(\"app.pro\")})\"),\n            style: TextStyle(\n              color: !isPro ? Colors.grey : null,\n            ),\n          ),\n          subtitle: Text(\n            _autoSyncHistoryToWebdav ? tr(\"app.yes\") : tr(\"app.no\"),\n            style: TextStyle(\n              color: !isPro ? Colors.grey : null,\n            ),\n          ),\n          onTap: () async {\n            if (!isPro) {\n              return;\n            }\n            String? result = await chooseListDialog<String>(\n                context,\n                tr(\"settings.webdav.auto_sync_history_to_webdav\"),\n                [tr(\"app.yes\"), tr(\"app.no\")]);\n            if (result != null) {\n              var target = result == tr(\"app.yes\");\n              await method.saveProperty(\n                  _autoSyncHistoryToWebdavPropertyName, \"$target\");\n              _autoSyncHistoryToWebdav = target;\n            }\n            setState(() {});\n          },\n        );\n      },\n    ),\n    //\n    ListTile(\n        title: Text(tr(\"settings.webdav.sync_history_to_webdav\")),\n        onTap: () async {\n          await syncHistoryToWebdav(context);\n        }),\n    //\n    ListTile(\n        title: Text(tr(\"settings.webdav.upload_history_to_webdav\")),\n        subtitle: Text(tr(\"settings.webdav.upload_history_to_webdav_desc\")),\n        onTap: () async {\n          await uploadHistoryToWebdav(context);\n        }),\n    //\n    const Divider(),\n    ListTile(\n      title: Text(tr('settings.local_favorite_sync_title')),\n      dense: true,\n    ),\n    localFavoriteSyncAutoTile(),\n    localFavoriteSyncManualTile(),\n  ];\n}\n"
  },
  {
    "path": "lib/basic/config/WebToonScrollMode.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../Method.dart';\n\nenum WebToonScrollMode {\n  IMAGE,\n  SCREEN, // Distance\n}\n\nconst _propertyName = \"webToonScrollMode\";\n\nWebToonScrollMode _webToonScrollMode = WebToonScrollMode.IMAGE;\n\nFuture initWebToonScrollMode() async {\n  var value = await method.loadProperty(_propertyName, \"0\");\n  if (value == \"1\") {\n    _webToonScrollMode = WebToonScrollMode.SCREEN;\n  } else {\n    _webToonScrollMode = WebToonScrollMode.IMAGE;\n  }\n}\n\nWebToonScrollMode currentWebToonScrollMode() => _webToonScrollMode;\n\nString currentWebToonScrollModeName() =>\n    _webToonScrollMode == WebToonScrollMode.SCREEN\n        ? tr(\"settings.web_toon_scroll_mode.screen\")\n        : tr(\"settings.web_toon_scroll_mode.image\");\n\nFuture<void> chooseWebToonScrollMode(BuildContext context) async {\n  var result = await showDialog<WebToonScrollMode>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(tr(\"settings.web_toon_scroll_mode.choose\")),\n        children: [\n          SimpleDialogOption(\n            onPressed: () {\n              Navigator.pop(context, WebToonScrollMode.IMAGE);\n            },\n            child: Text(tr(\"settings.web_toon_scroll_mode.image\")),\n          ),\n          SimpleDialogOption(\n            onPressed: () {\n              Navigator.pop(context, WebToonScrollMode.SCREEN);\n            },\n            child: Text(tr(\"settings.web_toon_scroll_mode.screen\")),\n          ),\n        ],\n      );\n    },\n  );\n  if (result != null) {\n    await method.saveProperty(\n      _propertyName,\n      result == WebToonScrollMode.SCREEN ? \"1\" : \"0\",\n    );\n    _webToonScrollMode = result;\n  }\n}\n\nWidget webToonScrollModeSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr(\"settings.web_toon_scroll_mode.title\")),\n        subtitle: Text(currentWebToonScrollModeName()),\n        onTap: () async {\n          await chooseWebToonScrollMode(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/WillPopNotice.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../Method.dart';\n\nconst _propertyName = \"willPopNotice\";\n\nlate bool _willPopNotice;\n\nFuture initWillPopNotice() async {\n  _willPopNotice = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool willPopNotice() {\n  return _willPopNotice;\n}\n\nWidget willPopNoticeSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return SwitchListTile(\n        title: Text(tr('settings.will_pop_notice')),\n        value: _willPopNotice,\n        onChanged: (value) async {\n          await method.saveProperty(_propertyName, \"$value\");\n          _willPopNotice = value;\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/i18n.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/i18b.dart';\nimport 'package:flutter/material.dart';\n\nWidget languageListTile() {\n  if (Platform.isIOS || Platform.isAndroid) {\n  } else {\n    return Container();\n  }\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(tr('language.title')),\n        subtitle: Text(tr('language.name')),\n        onTap: () async {\n          var choose = await showDialog<Locale>(\n              context: context,\n              builder: (context) {\n                return AlertDialog(\n                  title: Text(tr('language.title')),\n                  content: Column(\n                    children: [\n                      ListTile(\n                        title: const Text(\"English - United States\"),\n                        onTap: () {\n                          Navigator.pop(context, const Locale('en', 'US'));\n                        },\n                      ),\n                      ListTile(\n                        title: const Text(\"简体中文 - 中国大陆\"),\n                        onTap: () {\n                          Navigator.pop(context, const Locale('zh', 'CN'));\n                        },\n                      ),\n                      ListTile(\n                        title: const Text(\"繁體中文 - 中國台灣\"),\n                        onTap: () {\n                          Navigator.pop(context, const Locale('zh', 'TW'));\n                        },\n                      ),\n                      ListTile(\n                        title: const Text(\"日本語 - 日本\"),\n                        onTap: () {\n                          Navigator.pop(context, const Locale('ja', 'JP'));\n                        },\n                      ),\n                      ListTile(\n                        title: const Text(\"한국어 - 대한민국\"),\n                        onTap: () {\n                          Navigator.pop(context, const Locale('ko', 'KR'));\n                        },\n                      ),\n                    ],\n                  ),\n                );\n              });\n          if (choose != null) {\n              setLocale(context, choose);\n          }\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/config/passed.dart",
    "content": "import 'package:pikapika/basic/Method.dart';\n\nconst _propertyName = \"passed\";\nlate bool _passed;\n\nFuture<void> initPassed() async {\n  _passed = (await method.loadProperty(_propertyName, \"false\")) == \"true\";\n}\n\nbool currentPassed() {\n  return _passed;\n}\n\nFuture<void> firstPassed() async {\n  await method.saveProperty(_propertyName, \"true\");\n}\n"
  },
  {
    "path": "lib/basic/connect.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/services.dart';\n\nconst MethodChannel _networkChannel = MethodChannel('network');\n\nFuture<bool> isMobileNetwork() async {\n  if (!Platform.isAndroid && !Platform.isIOS) {\n    return false;\n  }\n  final result = await _networkChannel.invokeMethod<bool>('getIsMobile');\n  return result ?? false;\n}\n\nFuture<void> checkConnectivity() async {\n  final isMobile = await isMobileNetwork();\n  if (isMobile) {\n    print('使用移动网络');\n  } else {\n    print('非移动网络');\n  }\n}\n"
  },
  {
    "path": "lib/basic/define.dart",
    "content": "import 'package:flutter/material.dart';\n\nconst supportedLocales = [Locale('en', 'US'), Locale(\"zh\", \"CN\"), Locale('zh', 'TW'), Locale('ja', 'JP'), Locale('ko', 'KR')];\nconst fallbackLocale = Locale('en', 'US');\nconst translationsPath = 'lib/assets/translations';\n"
  },
  {
    "path": "lib/basic/enum/ErrorTypes.dart",
    "content": "const ERROR_TYPE_NETWORK = \"NETWORK_ERROR\";\nconst ERROR_TYPE_PERMISSION = \"PERMISSION_ERROR\";\nconst ERROR_TYPE_TIME = \"TIME_ERROR\";\nconst ERROR_TYPE_UNDER_REVIEW = \"UNDER_VIEW_ERROR\";\n\n// 错误的类型, 方便照展示和谐的提示\nString errorType(String error) {\n  // EXCEPTION\n  // Get \"https://picaapi.picacomic.com/categories\": net/http: TLS handshake timeout\n  // Get \"https://picaapi.picacomic.com/comics?c=%E9%95%B7%E7%AF%87&s=ua&page=1\": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused\n  // Get \"https://picaapi.picacomic.com/comics?c=%E5%85%A8%E5%BD%A9&s=ua&page=1\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)\n  if (error.contains(\"timeout\") ||\n      error.contains(\"connection refused\") ||\n      error.contains(\"deadline\") ||\n      error.contains(\"connection abort\") ||\n      error.contains(\"certificate\") ||\n      error.contains(\"x509\") ||\n      error.contains(\"ssl\")) {\n    return ERROR_TYPE_NETWORK;\n  }\n  if (error.contains(\"permission denied\")) {\n    return ERROR_TYPE_PERMISSION;\n  }\n  if (error.contains(\"time is not synchronize\")) {\n    return ERROR_TYPE_TIME;\n  }\n  if (error.contains(\"under review\")) {\n    return ERROR_TYPE_UNDER_REVIEW;\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "lib/basic/enum/Sort.dart",
    "content": "/// 官方提供的排序方式\n\nimport 'package:flutter/material.dart';\n\nconst SORT_DEFAULT = \"ua\";\nconst SORT_TIME_NEWEST = \"dd\";\nconst SORT_TIME_OLDEST = \"da\";\nconst SORT_LIKE_MOST = \"ld\";\nconst SORT_GIVE_MOST = \"vd\";\n\nconst LABEL_DEFAULT = '默认排序';\nconst LABEL_TIME_NEWEST = \"时间最新\";\nconst LABEL_TIME_OLDEST = \"时间最久\";\nconst LABEL_LIKE_MOST = \"点赞最多\";\nconst LABEL_GIVE_MOST = \"查看最多\";\n\nclass _Sort {\n  final String code;\n  final String label;\n\n  _Sort.of({\n    required this.code,\n    required this.label,\n  });\n}\n\nfinal sortList = [\n  _Sort.of(code: SORT_DEFAULT, label: LABEL_DEFAULT),\n  _Sort.of(code: SORT_TIME_NEWEST, label: LABEL_TIME_NEWEST),\n  _Sort.of(code: SORT_TIME_OLDEST, label: LABEL_TIME_OLDEST),\n  _Sort.of(code: SORT_LIKE_MOST, label: LABEL_LIKE_MOST),\n  _Sort.of(code: SORT_GIVE_MOST, label: LABEL_GIVE_MOST),\n];\n\nList<DropdownMenuItem<String>> items = sortList\n    .map((e) => DropdownMenuItem(\n          value: e.code,\n          child: Text(e.label),\n        ))\n    .toList();\n\n\nfinal collSortList = [\n  _Sort.of(code: SORT_TIME_NEWEST, label: LABEL_TIME_NEWEST),\n  _Sort.of(code: SORT_TIME_OLDEST, label: LABEL_TIME_OLDEST),\n];\n\nList<DropdownMenuItem<String>> collItems = collSortList\n    .map((e) => DropdownMenuItem(\n          value: e.code,\n          child: Text(e.label),\n        ))\n    .toList();"
  },
  {
    "path": "lib/basic/store/Categories.dart",
    "content": "/// 全局配置文件, 项目启动时加载\n\n\n// 数据缓存\nvar storedCategories = <String>[];\n\n"
  },
  {
    "path": "lib/i18.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'package:easy_localization/easy_localization.dart' as el;\nimport 'package:flutter/services.dart';\n\nMap<String, String> translations = {};\n\nFuture<void> loadTranslations() async {\n  String data =\n      await rootBundle.loadString('lib/assets/translations/zh-CN.json');\n  Map<String, dynamic> jsonData = json.decode(data);\n  putMap(\"\", jsonData);\n}\n\nvoid putMap(String prefix, Map<String, dynamic> map) {\n  for (String key in map.keys) {\n    if (map[key] is Map<String, dynamic>) {\n      putMap(\"$prefix$key.\", map[key] as Map<String, dynamic>);\n    } else if (map[key] is String) {\n      translations[\"$prefix$key\"] = map[key] as String;\n    } else {\n      throw Exception(\"Unsupported type for key: $prefix$key\");\n    }\n  }\n}\n\nString tr(String key) {\n  if (Platform.isIOS || Platform.isAndroid) {\n    return el.tr(key);\n  }\n  return translations[key] ?? key;\n}\n"
  },
  {
    "path": "lib/i18b.dart",
    "content": "\n\nimport 'package:flutter/material.dart';\nimport 'package:easy_localization/easy_localization.dart';\n\nvoid setLocale(BuildContext context, Locale locale) {\n  context.setLocale(locale);\n}"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:io';\n\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/screens/InitScreen.dart';\nimport 'package:pikapika/basic/Navigator.dart';\nimport 'package:pikapika/screens/components/MouseAndTouchScrollBehavior.dart';\n\nimport 'basic/config/Themes.dart';\n\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:easy_localization/easy_localization.dart';\nimport 'i18.dart' as i18;\n\nimport 'basic/define.dart';\n\nmain() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  if (Platform.isAndroid || Platform.isIOS) {\n    await EasyLocalization.ensureInitialized();\n    runApp(\n      EasyLocalization(\n          supportedLocales: supportedLocales,\n          path: translationsPath,\n          fallbackLocale: fallbackLocale,\n          child: const PikapikaApp()),\n    );\n  } else {\n    await i18.loadTranslations();\n    runApp(const PikapikaApp());\n  }\n}\n\nclass PikapikaApp extends StatefulWidget {\n  const PikapikaApp({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PikapikaAppState();\n}\n\nclass _PikapikaAppState extends State<PikapikaApp> {\n  @override\n  void initState() {\n    themeEvent.subscribe(_onChangeTheme);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    themeEvent.unsubscribe(_onChangeTheme);\n    super.dispose();\n  }\n\n  void _onChangeTheme(EventArgs? args) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if ((Platform.isAndroid || Platform.isIOS)) {\n      return MaterialApp(\n        scrollBehavior: mouseAndTouchScrollBehavior,\n        theme: currentLightThemeData(),\n        darkTheme: currentDarkThemeData(),\n        navigatorObservers: [navigatorObserver, routeObserver],\n        home: const InitScreen(),\n        localizationsDelegates: context.localizationDelegates,\n        supportedLocales: context.supportedLocales,\n        locale: context.locale,\n      );\n    }\n    return MaterialApp(\n      scrollBehavior: mouseAndTouchScrollBehavior,\n      theme: currentLightThemeData(),\n      darkTheme: currentDarkThemeData(),\n      navigatorObservers: [navigatorObserver, routeObserver],\n      home: const InitScreen(),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/main_desktop.dart",
    "content": "import 'main.dart' as original_main;\n\n// This file is the default main entry-point for go-flutter application.\nvoid main() {\n  original_main.main();\n}\n"
  },
  {
    "path": "lib/screens/AboutScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/config/Version.dart';\nimport 'package:pikapika/screens/components/Badge.dart';\n\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 关于\nclass AboutScreen extends StatefulWidget {\n  const AboutScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _AboutScreenState();\n}\n\nclass _AboutScreenState extends State<AboutScreen> {\n  @override\n  void initState() {\n    versionEvent.subscribe(_onVersion);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    versionEvent.unsubscribe(_onVersion);\n    super.dispose();\n  }\n\n  void _onVersion(dynamic a) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    var size = MediaQuery.of(context).size;\n    var min = size.width < size.height ? size.width : size.height;\n    var _currentVersion = currentVersion();\n    var _latestVersion = latestVersion();\n    var _latestVersionInfo = latestVersionInfo();\n    var _dirty = dirtyVersion();\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.about.title\")),\n      ),\n      body: PikaListView(\n        children: [\n          Container(height: 20),\n          SizedBox(\n            width: min / 2,\n            height: min / 2,\n            child: Center(\n              child:\n              //  isPro ?\n                   SvgPicture.asset(\n                      'lib/assets/github.svg',\n                      width: min / 3,\n                      height: min / 3,\n                      color: Colors.grey.shade500,\n                    )\n                  // : SizedBox(\n                  //     width: min / 3,\n                  //     height: min / 3,\n                  //   )\n                    ,\n            ),\n          ),\n          Container(height: 20),\n          const Divider(),\n          Container(\n            padding: const EdgeInsets.only(left: 20, right: 20),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Text(\n                  tr(\"screen.about.version\") + \" : $_currentVersion\",\n                  style: const TextStyle(\n                    height: 1.3,\n                  ),\n                ),\n                Text.rich(\n                  TextSpan(\n                    children: [\n                      TextSpan(text: tr(\"screen.about.check_update\") + \" : \"),\n                      ...(_dirty\n                          ? _buildDirty()\n                          : _buildNewVersion(_latestVersion)),\n                    ],\n                  ),\n                ),\n                _buildNewVersionInfo(_latestVersionInfo),\n              ],\n            ),\n          ),\n          const Divider(),\n          Container(\n            padding: const EdgeInsets.all(20),\n            child: SelectableText(\n              tr(\"screen.about.tips\"),\n              style: const TextStyle(\n                height: 1.3,\n              ),\n            ),\n          ),\n          const Divider(),\n        ],\n      ),\n    );\n  }\n\n  List<InlineSpan> _buildNewVersion(String? latestVersion) {\n    // if (!isPro) {\n    //   return [\n    //     TextSpan(\n    //       text: tr(\"screen.about.download_new_version\"),\n    //     )\n    //   ];\n    // }\n    if (latestVersion != null) {\n      return [\n        WidgetSpan(\n          child: Badged(\n            child: Container(\n              padding: const EdgeInsets.only(right: 12),\n              child: Text(\n                latestVersion,\n                style: const TextStyle(height: 1.3),\n              ),\n            ),\n            badge: \"1\",\n          ),\n        ),\n        const TextSpan(text: \"  \"),\n        TextSpan(\n          text: \"去下载\",\n          style: TextStyle(\n            height: 1.3,\n            color: Theme.of(context).colorScheme.primary,\n          ),\n          recognizer: TapGestureRecognizer()\n            ..onTap = _openRelease,\n        ),\n      ];\n    }\n    return [\n      TextSpan(\n          text: tr(\"screen.about.no_new_version\"),\n          style: const TextStyle(height: 1.3)),\n      WidgetSpan(\n        alignment: PlaceholderAlignment.middle,\n        child: Container(\n          padding: const EdgeInsets.all(4),\n          margin: const EdgeInsets.only(left: 3, right: 3),\n          decoration: const BoxDecoration(\n            borderRadius: BorderRadius.all(Radius.circular(20)),\n          ),\n        ),\n      ),\n      TextSpan(\n        text: tr(\"screen.about.check_update\"),\n        style: TextStyle(\n          height: 1.3,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n        recognizer: TapGestureRecognizer()\n          ..onTap = () => manualCheckNewVersion(context),\n      ),\n    ];\n  }\n\n  List<InlineSpan> _buildDirty() {\n    return [\n      TextSpan(\n        text: tr(\"screen.about.download_release_version\"),\n        style: TextStyle(\n          height: 1.3,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n        recognizer: TapGestureRecognizer()..onTap = _openRelease,\n      )\n    ];\n  }\n\n  Future _openRelease() async {\n    var url = downloadUrl();\n    if (url != null && url.isNotEmpty) {\n      await openUrl(url);\n    }\n  }\n\n  Widget _buildNewVersionInfo(String? latestVersionInfo) {\n    // if (!isPro) {\n    //   return const Text(\"\");\n    // }\n    if (latestVersionInfo != null) {\n      return Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Divider(),\n          Text(tr(\"screen.about.update_content\") + \":\"),\n          Container(\n            padding: const EdgeInsets.all(15),\n            child: Text(\n              latestVersionInfo,\n              style: const TextStyle(),\n            ),\n          ),\n        ],\n      );\n    }\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        const Divider(),\n        Container(\n          padding: const EdgeInsets.all(15),\n          child: Text.rich(\n            TextSpan(\n              text: tr(\"screen.about.go_to_release_repository\"),\n              style: TextStyle(\n                height: 1.3,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = _openRelease,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/AccessKeyReplaceScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\n\nimport '../basic/config/IsPro.dart';\n\nclass AccessKeyReplaceScreen extends StatefulWidget {\n  final String accessKey;\n\n  const AccessKeyReplaceScreen({Key? key, required this.accessKey})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _AccessKeyReplaceScreenState();\n}\n\nclass _AccessKeyReplaceScreenState extends State<AccessKeyReplaceScreen> {\n  var _loading = false;\n  var _message = \"\";\n  var _success = false;\n\n  _set() async {\n    setState(() {\n      _loading = true;\n    });\n    try {\n      await method.setPatAccessKey(widget.accessKey);\n      await reloadIsPro();\n      _success = true;\n    } catch (e) {\n      _message = tr(\"app.error\") + \" : $e\";\n    } finally {\n      setState(() {\n        _loading = false;\n      });\n    }\n  }\n\n  Widget _content() {\n    if (_loading) {\n      return ContentLoading(label: tr('app.loading'));\n    }\n    if (_success) {\n      return Text(tr('app.pat.success'));\n    }\n    return Column(\n      children: [\n        Expanded(child: Container()),\n        Text(widget.accessKey),\n        Text(_message),\n        Container(\n          height: 10,\n        ),\n        MaterialButton(\n          color: Colors.grey,\n          onPressed: _set,\n          child: Text(tr(\"app.confirm\")),\n        ),\n        Expanded(child: Container()),\n      ],\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.access_key_replace.title\")),\n      ),\n      body: Center(\n        child: _content(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/AccountScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/IsPro.dart';\nimport 'package:pikapika/basic/enum/ErrorTypes.dart';\nimport 'package:pikapika/screens/RegisterScreen.dart';\nimport 'package:pikapika/screens/components/NetworkSetting.dart';\nimport 'package:pikapika/screens/components/RecommendLinksPanel.dart';\n\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/Version.dart';\nimport 'AppScreen.dart';\nimport 'DownloadListScreen.dart';\nimport 'ForgotPasswordScreen.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\n\n// 账户设置\nclass AccountScreen extends StatefulWidget {\n  const AccountScreen({Key? key}) : super(key: key);\n\n  @override\n  _AccountScreenState createState() => _AccountScreenState();\n}\n\nclass _AccountScreenState extends State<AccountScreen> {\n  late bool _logging = false;\n  late String _username = \"\";\n  late String _password = \"\";\n  late StreamSubscription<String?> _linkSubscription;\n  late int _versionClick = 0;\n\n  @override\n  void initState() {\n    _linkSubscription = linkSubscript(context);\n    _loadProperties();\n    super.initState();\n    Future.delayed(Duration.zero, () async {\n      versionPop(context);\n      versionEvent.subscribe(_versionSub);\n    });\n  }\n\n  @override\n  void dispose() {\n    _linkSubscription.cancel();\n    versionEvent.unsubscribe(_versionSub);\n    super.dispose();\n  }\n\n  _versionSub(_) {\n    versionPop(context);\n  }\n\n  Future _loadProperties() async {\n    var username = await method.getUsername();\n    var password = await method.getPassword();\n    setState(() {\n      _username = username;\n      _password = password;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_logging) {\n      return _buildLogging();\n    }\n    return _buildGui();\n  }\n\n  Widget _buildLogging() {\n    return Scaffold(\n      body: ContentLoading(label: tr('app.loading')),\n    );\n  }\n\n  Widget _buildGui() {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.account.title')),\n        actions: [\n          SizedBox(\n            width: 80,\n            child: IconButton(\n              onPressed: () {\n                setState(() {\n                  _versionClick++;\n                });\n              },\n              icon: Text(currentVersion()),\n            ),\n          ),\n          IconButton(\n            onPressed: _toDownloadList,\n            icon: const Icon(Icons.download_rounded),\n          ),\n          IconButton(\n            onPressed: _logIn,\n            icon: const Icon(Icons.save),\n          ),\n        ],\n      ),\n      body: PikaListView(\n        children: [\n          ListTile(\n            title: Text(tr(\"screen.account.username\")),\n            subtitle: Text(\n                _username == \"\" ? tr(\"screen.account.not_set\") : _username),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _username,\n                title: tr(\"screen.account.username\"),\n                hint: tr(\"screen.account.username_hint\"),\n              );\n              if (input != null) {\n                await method.setUsername(input);\n                setState(() {\n                  _username = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr(\"screen.account.password\")),\n            subtitle: Text(\n                _password == \"\" ? tr(\"screen.account.not_set\") : '\\u2022' * 10),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _password,\n                title: tr(\"screen.account.password\"),\n                hint: tr(\"screen.account.password_hint\"),\n                isPasswd: true,\n              );\n              if (input != null) {\n                await method.setPassword(input);\n                setState(() {\n                  _password = input;\n                });\n              }\n            },\n          ),\n          const NetworkSetting(),\n          ..._versionClick >= 7\n              ? [\n                  Container(\n                    padding: const EdgeInsets.all(15),\n                    child: Text.rich(TextSpan(\n                      text: tr(\"screen.account.no_account_register\"),\n                      style: TextStyle(\n                        color: Theme.of(context).colorScheme.secondary,\n                        decoration: TextDecoration.underline,\n                      ),\n                      recognizer: TapGestureRecognizer()\n                        ..onTap = () => Navigator.push(\n                              context,\n                              mixRoute(\n                                  builder: (BuildContext context) =>\n                                      const RegisterScreen()),\n                            ).then((value) => _loadProperties()),\n                    )),\n                  ),\n                ]\n              : [],\n          Container(\n            padding: const EdgeInsets.all(15),\n            child: Text.rich(TextSpan(\n              text: tr(\"screen.account.password_reset\"),\n              style: TextStyle(\n                color: Theme.of(context).colorScheme.secondary,\n                decoration: TextDecoration.underline,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => Navigator.push(\n                      context,\n                      mixRoute(\n                          builder: (BuildContext context) =>\n                              const ForgotPasswordScreen()),\n                    ).then((value) => _loadProperties()),\n            )),\n          ),\n          const RecommendLinksPanel(\n            padding: EdgeInsets.fromLTRB(16, 0, 16, 16),\n          ),\n        ],\n      ),\n    );\n  }\n\n  _logIn() async {\n    setState(() {\n      _logging = true;\n    });\n    try {\n      await method.login();\n      await reloadIsPro();\n      Navigator.pushReplacement(\n        context,\n        mixRoute(builder: (context) => const AppScreen()),\n      );\n    } catch (e, s) {\n      print(\"$e\\n$s\");\n      setState(() {\n        _logging = false;\n      });\n      var message = tr(\"screen.account.check_username_password_or_network\");\n      switch (errorType(\"$e\")) {\n        case ERROR_TYPE_NETWORK:\n          message = tr(\"screen.account.network_不通\");\n          break;\n        case ERROR_TYPE_TIME:\n          message = tr(\"screen.account.check_device_time\");\n          break;\n      }\n      if (\"$e\".contains(\"email\") && \"$e\".contains(\"password\")) {\n        message = tr(\"screen.account.username_or_password_error\");\n      }\n      alertDialog(\n        context,\n        tr(\"screen.account.login_failed\"),\n        \"$message\\n$e\",\n      );\n    }\n  }\n\n  _toDownloadList() {\n    Navigator.push(\n      context,\n      mixRoute(builder: (context) => const DownloadListScreen()),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/AppScreen.dart",
    "content": "import 'dart:async';\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_styled_toast/flutter_styled_toast.dart';\nimport 'package:pikapika/basic/config/Version.dart';\nimport 'package:pikapika/basic/config/WillPopNotice.dart';\nimport 'package:pikapika/screens/components/Badge.dart';\nimport 'package:pikapika/screens/components/TimeoutLock.dart';\nimport '../basic/Common.dart';\nimport 'CategoriesScreen.dart';\nimport 'SpaceScreen.dart';\n\n// MAIN UI 底部导航栏\nclass AppScreen extends StatefulWidget {\n  const AppScreen({Key? key}) : super(key: key);\n\n  @override\n  State<AppScreen> createState() => _AppScreenState();\n}\n\nclass _AppScreenState extends State<AppScreen> {\n  late StreamSubscription<String?> _linkSubscription;\n\n  @override\n  void initState() {\n    versionEvent.subscribe(_onVersion);\n    _linkSubscription = linkSubscript(context);\n    super.initState();\n    Future.delayed(Duration.zero, () async {\n      versionPop(context);\n      versionEvent.subscribe(_versionSub);\n    });\n  }\n\n  @override\n  void dispose() {\n    versionEvent.unsubscribe(_onVersion);\n    _linkSubscription.cancel();\n    versionEvent.unsubscribe(_versionSub);\n    super.dispose();\n  }\n\n  _versionSub(_) {\n    versionPop(context);\n  }\n\n  void _onVersion(dynamic a) {\n    setState(() {});\n  }\n\n  static const List<Widget> _widgetOptions = <Widget>[\n    CategoriesScreen(),\n    SpaceScreen(),\n  ];\n\n  late int _selectedIndex = 0;\n\n  void _onItemTapped(int index) {\n    setState(() {\n      _selectedIndex = index;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final body = Scaffold(\n      body: IndexedStack(\n        index: _selectedIndex,\n        children: _widgetOptions,\n      ),\n      bottomNavigationBar: BottomNavigationBar(\n        items: <BottomNavigationBarItem>[\n          BottomNavigationBarItem(\n            icon:  const Icon(Icons.public),\n            label: tr('app.categories'),\n          ),\n          BottomNavigationBarItem(\n            icon: Badged(\n              child: const Icon(Icons.face),\n              badge: latestVersion() == null ? null : \"1\",\n            ),\n            label: tr('app.my'),\n          ),\n        ],\n        currentIndex: _selectedIndex,\n        iconSize: 20,\n        selectedFontSize: 12,\n        unselectedFontSize: 12,\n        onTap: _onItemTapped,\n      ),\n    );\n    return TimeoutLock(child: willPop(body));\n  }\n\n  int _noticeTime = 0;\n\n  Widget willPop(Scaffold body) {\n    return WillPopScope(\n      child: body,\n      onWillPop: () async {\n        if (willPopNotice()) {\n          final now = DateTime.now().millisecondsSinceEpoch;\n          if (_noticeTime + 3000 > now) {\n            return true;\n          } else {\n            _noticeTime = now;\n            showToast(\n              tr(\"screen.app.will_pop_notice\"),\n              context: context,\n              position: StyledToastPosition.center,\n              animation: StyledToastAnimation.scale,\n              reverseAnimation: StyledToastAnimation.fade,\n              duration: const Duration(seconds: 3),\n              animDuration: const Duration(milliseconds: 300),\n              curve: Curves.elasticOut,\n              reverseCurve: Curves.linear,\n            );\n            return false;\n          }\n        }\n        return true;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/CategoriesScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'SearchAuthorScreen.dart';\nimport 'components/flutter_search_bar.dart' as fsb;\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/config/HiddenSubIcon.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesEvent.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesMode.dart';\nimport 'package:pikapika/basic/store/Categories.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/screens/ComicCollectionsScreen.dart';\nimport 'package:pikapika/screens/RankingsScreen.dart';\nimport 'package:pikapika/screens/SearchScreen.dart';\nimport 'package:pikapika/screens/components/ContentError.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../basic/config/Address.dart';\nimport '../basic/config/CategoriesColumnCount.dart';\nimport '../basic/config/CategoriesSort.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'ComicSubscribesScreen.dart';\nimport 'ComicsScreen.dart';\nimport 'GamesScreen.dart';\nimport 'RandomComicsScreen.dart';\nimport 'components/Common.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/Images.dart';\nimport 'components/ListView.dart';\n\n// 分类\nclass CategoriesScreen extends StatefulWidget {\n  const CategoriesScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _CategoriesScreenState();\n}\n\nclass _CategoriesScreenState extends State<CategoriesScreen> {\n  late final fsb.SearchBar _searchBar = fsb.SearchBar(\n    hintText: tr('screen.categories.search_hint'),\n    inBar: false,\n    setState: setState,\n    onSubmitted: (value) {\n      if (value.isNotEmpty) {\n        Navigator.push(\n          context,\n          mixRoute(\n            builder: (context) => SearchScreen(keyword: value),\n          ),\n        );\n      }\n    },\n    buildDefaultAppBar: (BuildContext context) {\n      return AppBar(\n        title: Text(tr('app.categories')),\n        actions: [\n          if (!hiddenSubIcon) const IntoComicSubscribesScreenButton(),\n          commonPopMenu(context),\n          addressPopMenu(context),\n          if (!hiddenSubIcon) const ComicSearchAuthorScreenButton(),\n          _searchBar.getSearchAction(context),\n        ],\n      );\n    },\n  );\n\n  late Future<List<Category>> _categoriesFuture = _fetch();\n\n  Future<List<Category>> _fetch() async {\n    List<Category> categories = await method.categories();\n    storedCategories = [];\n    for (var element in categories) {\n      if (!element.isWeb) {\n        storedCategories.add(element.title);\n      }\n    }\n    return categories;\n  }\n\n  void _reloadCategories() {\n    setState(() {\n      this._categoriesFuture = _fetch();\n    });\n  }\n\n  @override\n  void initState() {\n    shadowCategoriesEvent.subscribe(_onShadowChange);\n    categoriesColumnCountEvent.subscribe(_setState);\n    categoriesSortEvent.subscribe(_onShadowChange);\n    hiddenSubIconEvent.subscribe(_setState);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    shadowCategoriesEvent.unsubscribe(_onShadowChange);\n    categoriesColumnCountEvent.unsubscribe(_setState);\n    categoriesSortEvent.unsubscribe(_onShadowChange);\n    hiddenSubIconEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  void _onShadowChange(EventArgs? args) {\n    _reloadCategories();\n  }\n\n  _setState(_) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    var themeBackground = theme.scaffoldBackgroundColor;\n    var shadeBackground = Color.fromARGB(\n      0x11,\n      255 - themeBackground.red,\n      255 - themeBackground.green,\n      255 - themeBackground.blue,\n    );\n    return Scaffold(\n      appBar: _searchBar.build(context),\n      body: Container(\n        color: shadeBackground,\n        child: FutureBuilder(\n          future: _categoriesFuture,\n          builder:\n              ((BuildContext context, AsyncSnapshot<List<Category>> snapshot) {\n            if (snapshot.hasError) {\n              return ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  _reloadCategories();\n                },\n              );\n            }\n            if (snapshot.connectionState != ConnectionState.done) {\n              return ContentLoading(label: tr('app.loading'));\n            }\n            //\n            late double blockSize;\n            late double imageSize;\n            late double imageRs;\n            if (categoriesColumnCount == 0) {\n              var size = MediaQuery.of(context).size;\n              var min = size.width < size.height ? size.width : size.height;\n              blockSize = (min ~/ 3).floorToDouble();\n            } else {\n              var size = MediaQuery.of(context).size;\n              var min = size.width;\n              blockSize = (min ~/ categoriesColumnCount).floorToDouble();\n            }\n            imageSize = blockSize - 15;\n            imageRs = imageSize / 10;\n            List<CategoriesItem> items = [];\n            //\n            items.addAll(_buildChannels(imageSize));\n            items.addAll(_buildCategories(snapshot.data!, imageSize));\n            var names = items.map((e) => e.title).toList();\n            var sort = getCategoriesSort();\n            items.sort((a, b) {\n              var aIndex = sort.indexOf(a.title);\n              var bIndex = sort.indexOf(b.title);\n              if (aIndex == bIndex) {\n                aIndex = names.indexOf(a.title);\n                bIndex = names.indexOf(b.title);\n              }\n              if (aIndex == -1) {\n                return 1;\n              } else if (bIndex == -1) {\n                return -1;\n              } else {\n                return aIndex - bIndex;\n              }\n            });\n            List<Widget> wrapItems = _wrapItems(items, blockSize, imageRs);\n            return PikaListView(\n              children: [\n                Container(height: 20),\n                Wrap(\n                  runSpacing: 20,\n                  alignment: WrapAlignment.spaceAround,\n                  children: wrapItems,\n                ),\n                Container(height: 20),\n              ],\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  List<Widget> _wrapItems(\n    List<CategoriesItem> items,\n    double blockSize,\n    double imageRs,\n  ) {\n    List<Widget> list = [];\n\n    append(Widget widget, String title, Function() onTap) {\n      list.add(\n        GestureDetector(\n          onTap: onTap,\n          child: SizedBox(\n            width: blockSize,\n            child: Column(\n              children: [\n                Card(\n                  elevation: .5,\n                  child: ClipRRect(\n                    borderRadius: BorderRadius.all(Radius.circular(imageRs)),\n                    child: widget,\n                  ),\n                  shape: RoundedRectangleBorder(\n                    borderRadius: BorderRadius.all(Radius.circular(imageRs)),\n                  ),\n                ),\n                Container(height: 5),\n                Center(\n                  child: Text(title),\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n    }\n\n    for (var value in items) {\n      append(value.icon, value.title, value.onTap);\n    }\n\n    return list;\n  }\n\n  List<CategoriesItem> _buildCategories(\n    List<Category> cList,\n    double imageSize,\n  ) {\n    List<CategoriesItem> items = [];\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/books.svg', imageSize, imageSize, margin: 20),\n      tr('categories.all'),\n      () => _navigateToCategory(null),\n    ));\n\n    items.add(CategoriesItem(\n      Icon(\n        Icons.recommend_outlined,\n        size: imageSize,\n        color: Colors.grey,\n      ),\n      tr('categories.recommend'),\n      () {\n        Navigator.push(\n          context,\n          mixRoute(\n            builder: (context) => const ComicCollectionsScreen(),\n          ),\n        );\n      },\n    ));\n\n    for (var i = 0; i < cList.length; i++) {\n      var c = cList[i];\n      if (c.isWeb) continue;\n      switch (currentShadowCategoriesMode()) {\n        case ShadowCategoriesMode.BLACK_LIST:\n          if (shadowCategories.contains(c.title)) continue;\n          break;\n        case ShadowCategoriesMode.WHITE_LIST:\n          if (!shadowCategories.contains(c.title)) continue;\n          break;\n      }\n      items.add(CategoriesItem(\n        RemoteImage(\n          fileServer: c.thumb.fileServer,\n          path: c.thumb.path,\n          width: imageSize,\n          height: imageSize,\n        ),\n        c.title,\n        () => _navigateToCategory(c.title),\n      ));\n    }\n\n    return items;\n  }\n\n  List<CategoriesItem> _buildChannels(double imageSize) {\n    List<CategoriesItem> items = [];\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/rankings.svg', imageSize, imageSize,\n          margin: 20, color: Colors.red.shade700),\n      tr('categories.rankings'),\n      () {\n        Navigator.push(\n          context,\n          mixRoute(builder: (context) => const RankingsScreen()),\n        );\n      },\n    ));\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/random.svg', imageSize, imageSize,\n          margin: 20, color: Colors.orangeAccent.shade700),\n      tr('categories.random'),\n      () {\n        Navigator.push(\n          context,\n          mixRoute(builder: (context) => const RandomComicsScreen()),\n        );\n      },\n    ));\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/gamepad.svg', imageSize, imageSize,\n          margin: 20, color: Colors.blue.shade500),\n      tr('categories.game'),\n      () {\n        Navigator.push(\n          context,\n          mixRoute(builder: (context) => const GamesScreen()),\n        );\n      },\n    ));\n\n    return items;\n  }\n\n  void _navigateToCategory(String? categoryTitle) {\n    Navigator.push(\n      context,\n      mixRoute(\n        builder: (context) => ComicsScreen(category: categoryTitle),\n      ),\n    );\n  }\n}\n\nclass CategoriesItem {\n  final Widget icon;\n  final String title;\n  final Function() onTap;\n\n  const CategoriesItem(\n    this.icon,\n    this.title,\n    this.onTap,\n  );\n}\n"
  },
  {
    "path": "lib/screens/CategoriesSortScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentError.dart';\nimport 'package:pikapika/screens/components/ListView.dart';\n\nimport '../basic/Entities.dart';\nimport '../basic/config/CategoriesColumnCount.dart';\nimport '../basic/config/CategoriesSort.dart';\nimport 'CategoriesScreen.dart';\nimport 'components/Images.dart';\n\nclass CategoriesSortScreen extends StatefulWidget {\n  const CategoriesSortScreen({Key? key}) : super(key: key);\n\n  @override\n  _CategoriesSortScreenState createState() => _CategoriesSortScreenState();\n}\n\nclass _CategoriesSortScreenState extends State<CategoriesSortScreen> {\n  late Key _key = UniqueKey();\n  late Future<List<Category>> _future = method.categories();\n\n  _reload() {\n    setState(() {\n      _key = UniqueKey();\n      _future = method.categories();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      key: _key,\n      future: _future,\n      builder: (BuildContext context, AsyncSnapshot<List<Category>> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr('screen.categories_sort.title')),\n            ),\n            body: ContentError(\n              error: snapshot.error,\n              stackTrace: snapshot.stackTrace,\n              onRefresh: () async {\n                _reload();\n              },\n            ),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr('screen.categories_sort.title')),\n            ),\n            body: const Center(\n              child: CircularProgressIndicator(),\n            ),\n          );\n        }\n        return CategoriesSortPanel(snapshot.requireData);\n      },\n    );\n  }\n}\n\nclass CategoriesSortPanel extends StatefulWidget {\n  final List<Category> requireData;\n\n  const CategoriesSortPanel(this.requireData, {Key? key}) : super(key: key);\n\n  @override\n  _CategoriesSortPanelState createState() => _CategoriesSortPanelState();\n}\n\nclass _CategoriesSortPanelState extends State<CategoriesSortPanel> {\n  final List<String> _categoriesSort = [];\n\n  _switch(String value) {\n    setState(() {\n      if (_categoriesSort.contains(value)) {\n        _categoriesSort.remove(value);\n      } else {\n        _categoriesSort.add(value);\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    //\n    late double blockSize;\n    late double imageSize;\n    late double imageRs;\n    if (categoriesColumnCount == 0) {\n      var size = MediaQuery.of(context).size;\n      var min = size.width < size.height ? size.width : size.height;\n      blockSize = (min ~/ 3).floorToDouble();\n    } else {\n      var size = MediaQuery.of(context).size;\n      var min = size.width;\n      blockSize = (min ~/ categoriesColumnCount).floorToDouble();\n    }\n    imageSize = blockSize - 15;\n    imageRs = imageSize / 10;\n    List<CategoriesItem> items = [];\n    //\n    items.addAll(_buildChannels(imageSize));\n    items.addAll(_buildCategories(widget.requireData, imageSize));\n    var names = items.map((e) => e.title).toList();\n    var sort = getCategoriesSort();\n    items.sort((a, b) {\n      var aIndex = sort.indexOf(a.title);\n      var bIndex = sort.indexOf(b.title);\n      if (aIndex == bIndex) {\n        aIndex = names.indexOf(a.title);\n        bIndex = names.indexOf(b.title);\n      }\n      if (aIndex == -1) {\n        return 1;\n      } else if (bIndex == -1) {\n        return -1;\n      } else {\n        return aIndex - bIndex;\n      }\n    });\n    List<Widget> wrapItems = _wrapItems(items, blockSize, imageRs, imageSize);\n    //\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.categories_sort.title')),\n        actions: [\n          _saveIcon(),\n        ],\n      ),\n      body: PikaListView(\n        children: [\n          Container(height: 20),\n          Wrap(\n            runSpacing: 20,\n            alignment: WrapAlignment.spaceAround,\n            children: wrapItems,\n          ),\n          Container(height: 20),\n        ],\n      ),\n    );\n  }\n\n  List<Widget> _wrapItems(\n    List<CategoriesItem> items,\n    double blockSize,\n    double imageRs,\n    double imageSize,\n  ) {\n    List<Widget> list = [];\n\n    append(Widget widget, String title, Function() onTap) {\n      list.add(\n        GestureDetector(\n          onTap: onTap,\n          child: SizedBox(\n            width: blockSize,\n            child: Column(\n              children: [\n                Stack(\n                  children: [\n                    Card(\n                      elevation: .5,\n                      child: ClipRRect(\n                        borderRadius:\n                            BorderRadius.all(Radius.circular(imageRs)),\n                        child: widget,\n                      ),\n                      shape: RoundedRectangleBorder(\n                        borderRadius:\n                            BorderRadius.all(Radius.circular(imageRs)),\n                      ),\n                    ),\n                    if (!_categoriesSort.contains(title))\n                      Container(\n                        width: imageSize,\n                        height: imageSize,\n                        color: Colors.black.withOpacity(.6),\n                        margin: const EdgeInsets.all(4.0),\n                      ),\n                    if (_categoriesSort.contains(title))\n                      Container(\n                        width: imageSize,\n                        height: imageSize,\n                        color: Colors.black.withOpacity(.2),\n                        margin: const EdgeInsets.all(4.0),\n                      ),\n                    if (_categoriesSort.contains(title))\n                      Container(\n                        color: Colors.black.withOpacity(.2),\n                        padding: const EdgeInsets.all(10),\n                        child: Text(\n                          \"${_categoriesSort.indexOf(title) + 1}\",\n                          style: const TextStyle(\n                            color: Colors.white,\n                            fontSize: 22,\n                            fontWeight: FontWeight.bold,\n                          ),\n                        ),\n                      ),\n                  ],\n                ),\n                Container(height: 5),\n                Center(\n                  child: Text(title),\n                ),\n              ],\n            ),\n          ),\n        ),\n      );\n    }\n\n    for (var value in items) {\n      append(value.icon, value.title, value.onTap);\n    }\n\n    return list;\n  }\n\n  List<CategoriesItem> _buildCategories(\n    List<Category> cList,\n    double imageSize,\n  ) {\n    List<CategoriesItem> items = [];\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/books.svg', imageSize, imageSize, margin: 20),\n      tr('categories.all'),\n      () => _switch(tr('categories.all')),\n    ));\n\n    items.add(CategoriesItem(\n      Icon(\n        Icons.recommend_outlined,\n        size: imageSize,\n        color: Colors.grey,\n      ),\n      tr('screen.categories.recommend'),\n      () => _switch(tr('screen.categories.recommend')),\n    ));\n\n    for (var i = 0; i < cList.length; i++) {\n      var c = cList[i];\n      if (c.isWeb) continue;\n      items.add(CategoriesItem(\n        RemoteImage(\n          fileServer: c.thumb.fileServer,\n          path: c.thumb.path,\n          width: imageSize,\n          height: imageSize,\n        ),\n        c.title,\n        () => _switch(c.title),\n      ));\n    }\n\n    return items;\n  }\n\n  List<CategoriesItem> _buildChannels(double imageSize) {\n    List<CategoriesItem> items = [];\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/rankings.svg', imageSize, imageSize,\n          margin: 20, color: Colors.red.shade700),\n      tr('categories.rankings'),\n      () => _switch(tr('categories.rankings')),\n    ));\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/random.svg', imageSize, imageSize,\n          margin: 20, color: Colors.orangeAccent.shade700),\n      tr('categories.random'),\n      () => _switch(tr('categories.random')),\n    ));\n\n    items.add(CategoriesItem(\n      buildSvg('lib/assets/gamepad.svg', imageSize, imageSize,\n          margin: 20, color: Colors.blue.shade500),\n      tr('categories.game'),\n      () => _switch(tr('categories.game')),\n    ));\n\n    return items;\n  }\n\n  Widget _saveIcon() {\n    return IconButton(\n      onPressed: () async {\n        await saveCategoriesSort(_categoriesSort);\n        Navigator.of(context).pop();\n      },\n      icon: const Icon(Icons.save),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/CleanScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Channels.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/FitButton.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/RightClickPop.dart';\n\n// 清理\nclass CleanScreen extends StatefulWidget {\n  const CleanScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _CleanScreenState();\n}\n\nclass _CleanScreenState extends State<CleanScreen> {\n  late bool _cleaning = false;\n  late String _cleaningMessage = tr('screen.clean.cleaning');\n  late String _cleanResult = \"\";\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(String event) {\n    setState(() {\n      _cleaningMessage = event;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    if (_cleaning) {\n      return Scaffold(\n        body: ContentLoading(label: _cleaningMessage),\n      );\n    }\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.clean.title')),\n      ),\n      body: ListView(\n        children: [\n          Container(\n            padding: const EdgeInsets.all(8),\n            child: _cleanResult != \"\" ? Text(_cleanResult) : Container(),\n          ),\n          SizedBox(\n            height: 50,\n            child: FitButton(\n              text: tr('screen.clean.clean_network_cache'),\n              onPressed: () {\n                processCleanAction(method.cleanNetworkCache);\n              },\n            ),\n          ),\n          SizedBox(\n            height: 50,\n            child: FitButton(\n              text: tr('screen.clean.clean_image_cache'),\n              onPressed: () {\n                processCleanAction(method.cleanImageCache);\n              },\n            ),\n          ),\n          SizedBox(\n            height: 50,\n            child: FitButton(\n              text: tr('screen.clean.clean_all_cache'),\n              onPressed: () {\n                processCleanAction(method.clean);\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future processCleanAction(Future Function() action) async {\n    try {\n      setState(() {\n        _cleaning = true;\n      });\n      await action();\n      setState(() {\n        _cleanResult = tr('screen.clean.clean_success');\n      });\n    } catch (e) {\n      setState(() {\n        _cleanResult = tr('screen.clean.clean_failed') + \" $e\";\n      });\n    } finally {\n      setState(() {\n        _cleaning = false;\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/CloseAppScreen.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nclass CloseAppScreen extends StatelessWidget {\n  const CloseAppScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.close_app.title')),\n      ),\n      body: Center(\n        child: ElevatedButton(\n          onPressed: () {\n            // 关闭应用\n            exit(0);\n          },\n          child: Text(tr('screen.close_app.close_app')),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/ComicCollectionsScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\nimport 'package:pikapika/screens/components/ContentMessage.dart';\n\nimport 'components/RightClickPop.dart';\n\nclass ComicCollectionsScreen extends StatefulWidget {\n  const ComicCollectionsScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicCollectionsScreenState();\n}\n\nclass _ComicCollectionsScreenState extends State<ComicCollectionsScreen> {\n  late Future<List<Collection>> _future;\n  late Key _key = UniqueKey();\n\n  @override\n  void initState() {\n    _future = method.collections();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    // TODO: implement dispose\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(tr('categories.recommend'))),\n      body: ContentBuilder(\n        key: _key,\n        future: _future,\n        onRefresh: () async {\n          setState(() {\n            _future = method.collections();\n            _key = UniqueKey();\n          });\n        },\n        successBuilder: (\n          BuildContext context,\n          AsyncSnapshot<List<Collection>> snapshot,\n        ) {\n          final collection = snapshot.requireData;\n          if (collection.isEmpty) {\n            return ContentMessage(\n              message: tr('screen.comic_collections.no_resource'),\n              icon: Icons.no_sim_outlined,\n              onRefresh: () async {\n                setState(() {\n                  _future = method.collections();\n                });\n              },\n            );\n          }\n          final ThemeData theme = Theme.of(context);\n          final AppBarTheme appBarTheme = AppBarTheme.of(context);\n          return DefaultTabController(\n            length: collection.length,\n            child: Scaffold(\n              appBar: PreferredSizeContainer(\n                color: appBarTheme.backgroundColor,\n                child: TabBar(\n                  indicatorColor: theme.dividerColor,\n                  tabs: collection\n                      .map((e) => Tab(\n                          text: e.title.indexOf(\"推薦\") > 0\n                              ? e.title.substring(0, e.title.indexOf(\"推薦\"))\n                              : e.title))\n                      .toList(),\n                ),\n              ),\n              body: TabBarView(\n                children: collection.map((e) => ComicList(e.comics)).toList(),\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass PreferredSizeContainer extends StatelessWidget\n    implements PreferredSizeWidget {\n  final PreferredSizeWidget child;\n  final Color? color;\n\n  const PreferredSizeContainer({\n    required this.child,\n    this.color,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Size get preferredSize => child.preferredSize;\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      color: color,\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/ComicInfoScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/Navigator.dart';\nimport 'package:pikapika/basic/config/IgnoreInfoHistory.dart';\nimport 'package:pikapika/screens/ComicsScreen.dart';\nimport 'package:pikapika/screens/components/CommentMainType.dart';\nimport 'package:pikapika/screens/components/ItemBuilder.dart';\nimport 'package:pikapika/screens/components/Recommendation.dart';\n\nimport '../basic/config/HiddenSubIcon.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'ComicReaderScreen.dart';\nimport 'DownloadConfirmScreen.dart';\nimport 'components/ComicDescriptionCard.dart';\nimport 'components/ComicInfoCard.dart';\nimport 'components/ComicTagsCard.dart';\nimport 'components/CommentList.dart';\nimport 'components/CommonData.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ContinueReadButton.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 漫画详情\nclass ComicInfoScreen extends StatefulWidget {\n  final String comicId;\n  final bool holdPkz;\n\n  const ComicInfoScreen({Key? key, required this.comicId, this.holdPkz = false})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicInfoScreenState();\n}\n\nclass _ComicInfoScreenState extends State<ComicInfoScreen> with RouteAware {\n  late var _tabIndex = 0;\n  late Future<ComicInfo> _comicFuture = _loadComic();\n  late Key _comicFutureKey = UniqueKey();\n  late Future<ViewLog?> _viewFuture = _loadViewLog();\n  late Future<ComicSubscribe?> _subscribedFuture = _loadSubscribed();\n  late Future<List<Ep>> _epListFuture = _loadEps();\n  StreamSubscription<String?>? _linkSubscription;\n\n  Future<ComicInfo> _loadComic() async {\n    return await method\n        .comicInfo(widget.comicId, currentIgnoreInfoHistory())\n        .then((value) async {\n      subscribedViewed(widget.comicId);\n      return value;\n    });\n  }\n\n  Future<List<Ep>> _loadEps() async {\n    List<Ep> eps = [];\n    var page = 0;\n    late EpPage rsp;\n    do {\n      rsp = await method.comicEpPage(widget.comicId, ++page);\n      eps.addAll(rsp.docs);\n    } while (rsp.page < rsp.pages);\n    return eps;\n  }\n\n  Future<ViewLog?> _loadViewLog() {\n    return method.loadView(widget.comicId);\n  }\n\n  Future<ComicSubscribe?> _loadSubscribed() {\n    return method.loadSubscribed(widget.comicId);\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    routeObserver.subscribe(this, ModalRoute.of(context)!);\n  }\n\n  @override\n  void didPopNext() {\n    setState(() {\n      _viewFuture = _loadViewLog();\n    });\n  }\n\n  @override\n  void initState() {\n    if (widget.holdPkz) {\n      _linkSubscription = linkSubscript(context);\n    }\n    hiddenSubIconEvent.subscribe(_setState);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _linkSubscription?.cancel();\n    routeObserver.unsubscribe(this);\n    hiddenSubIconEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  void _setState(dynamic args) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return FutureBuilder(\n      key: _comicFutureKey,\n      future: _comicFuture,\n      builder: (BuildContext context, AsyncSnapshot<ComicInfo> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: AppBar(),\n            body: ContentError(\n              error: snapshot.error,\n              stackTrace: snapshot.stackTrace,\n              onRefresh: () async {\n                setState(() {\n                  _comicFuture = _loadComic();\n                  _comicFutureKey = UniqueKey();\n                });\n              },\n            ),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: AppBar(),\n            body: ContentLoading(label: tr('app.loading')),\n          );\n        }\n        var _comicInfo = snapshot.data!;\n        var theme = Theme.of(context);\n        var _tabs = <Widget>[\n          Tab(text: tr('screen.comic_info.chapter') + ' (${_comicInfo.epsCount})'),\n          Tab(text: tr('screen.comic_info.comment') + ' (${_comicInfo.commentsCount})'),\n          Tab(text: tr('screen.comic_info.recommend')),\n        ];\n        var _views = <Widget>[\n          _buildEpWrap(_epListFuture, _comicInfo),\n          CommentList(CommentMainType.COMIC, _comicInfo.id),\n          Recommendation(comicId: _comicInfo.id),\n        ];\n        return DefaultTabController(\n          length: _tabs.length,\n          child: Scaffold(\n            appBar: AppBar(\n              title: Text(_comicInfo.title),\n              actions: [\n                _buildSubscribeAction(_subscribedFuture, _comicInfo),\n                _buildDownloadAction(_epListFuture, _comicInfo),\n              ],\n            ),\n            body: PikaListView(\n              children: [\n                ComicInfoCard(_comicInfo, linkItem: true),\n                ComicTagsCard(_comicInfo.tags),\n                ComicDescriptionCard(description: _comicInfo.description),\n                Container(\n                  padding: const EdgeInsets.all(10),\n                  decoration: BoxDecoration(\n                    border: Border(\n                      bottom: BorderSide(\n                        color: theme.dividerColor,\n                      ),\n                    ),\n                  ),\n                  child: Wrap(\n                    alignment: WrapAlignment.spaceBetween,\n                    children: [\n                      Text.rich(TextSpan(\n                        children: [\n                          WidgetSpan(\n                            child: GestureDetector(\n                              onTap: () {\n                                if (_comicInfo.creator.id != \"\") {\n                                  navPushOrReplace(\n                                    context,\n                                    (context) => ComicsScreen(\n                                      creatorId: _comicInfo.creator.id,\n                                      creatorName: _comicInfo.creator.name,\n                                    ),\n                                  );\n                                }\n                              },\n                              onLongPress: () {\n                                confirmCopy(\n                                  context,\n                                  _comicInfo.creator.name,\n                                );\n                              },\n                              child: Text(\n                                _comicInfo.creator.name,\n                                style: const TextStyle(\n                                  fontSize: 14,\n                                  color: Colors.grey,\n                                ),\n                              ),\n                            ),\n                          ),\n                          const TextSpan(\n                            text: \"  \",\n                            style: TextStyle(\n                              fontSize: 14,\n                              color: Colors.grey,\n                            ),\n                          ),\n                          TextSpan(\n                            text:\n                                \"( ${formatTimeToDate(_comicInfo.updatedAt)} )\",\n                            style: const TextStyle(\n                              fontSize: 13,\n                              color: Colors.grey,\n                            ),\n                          ),\n                        ],\n                      )),\n                      GestureDetector(\n                        onTap: () {\n                          if (_comicInfo.chineseTeam != \"\") {\n                            navPushOrReplace(\n                              context,\n                              (context) => ComicsScreen(\n                                chineseTeam: _comicInfo.chineseTeam,\n                              ),\n                            );\n                          }\n                        },\n                        onLongPress: () {\n                          confirmCopy(context, _comicInfo.chineseTeam);\n                        },\n                        child: Text(\n                          _comicInfo.chineseTeam,\n                          style: const TextStyle(\n                            fontSize: 13,\n                            color: Colors.grey,\n                          ),\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                Container(height: 5),\n                Container(\n                  height: 40,\n                  color: theme.colorScheme.secondary.withOpacity(.025),\n                  child: TabBar(\n                    tabs: _tabs,\n                    indicatorColor: theme.colorScheme.secondary,\n                    labelColor: theme.colorScheme.secondary,\n                    onTap: (val) async {\n                      setState(() {\n                        _tabIndex = val;\n                      });\n                    },\n                  ),\n                ),\n                Container(height: 15),\n                _views[_tabIndex],\n                Container(height: 5),\n              ],\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildSubscribeAction(\n    Future<ComicSubscribe?> _subscribedFuture,\n    ComicInfo _comicInfo,\n  ) {\n    if (hiddenSubIcon) {\n      return Container();\n    }\n    return FutureBuilder(\n      future: _subscribedFuture,\n      builder: (BuildContext context, AsyncSnapshot<ComicSubscribe?> snapshot) {\n        if (snapshot.hasError) {\n          return IconButton(\n            onPressed: () {\n              setState(() {\n                this._subscribedFuture = _loadSubscribed();\n              });\n            },\n            icon: const Icon(Icons.sync_problem),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return IconButton(onPressed: () {}, icon: const Icon(Icons.sync));\n        }\n        var _subscribed = snapshot.data;\n        return IconButton(\n          onPressed: () async {\n            if (_subscribed == null) {\n              await method.addSubscribed(_comicInfo.id);\n            } else {\n              await method.removeSubscribed(_comicInfo.id);\n            }\n            setState(() {\n              this._subscribedFuture = _loadSubscribed();\n            });\n          },\n          icon: Icon(\n            _subscribed == null\n                ? Icons.notifications_none\n                : Icons.notifications,\n          ),\n        );\n      },\n    );\n  }\n\n  Widget _buildDownloadAction(\n    Future<List<Ep>> _epListFuture,\n    ComicInfo _comicInfo,\n  ) {\n    return FutureBuilder(\n      future: _epListFuture,\n      builder: (BuildContext context, AsyncSnapshot<List<Ep>> snapshot) {\n        if (snapshot.hasError) {\n          return IconButton(\n            onPressed: () {\n              setState(() {\n                this._epListFuture = _loadEps();\n              });\n            },\n            icon: const Icon(Icons.sync_problem),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return IconButton(onPressed: () {}, icon: const Icon(Icons.sync));\n        }\n        var _epList = snapshot.data!;\n        return IconButton(\n          onPressed: () async {\n            Navigator.push(\n              context,\n              mixRoute(\n                builder: (context) => DownloadConfirmScreen(\n                  comicInfo: _comicInfo,\n                  epList: _epList.reversed.toList(),\n                ),\n              ),\n            );\n          },\n          icon: const Icon(Icons.download_rounded),\n        );\n      },\n    );\n  }\n\n  Widget _buildEpWrap(Future<List<Ep>> _epListFuture, ComicInfo _comicInfo) {\n    return ItemBuilder(\n      future: _epListFuture,\n      successBuilder: (BuildContext context, AsyncSnapshot<List<Ep>> snapshot) {\n        var _epList = snapshot.data!;\n        return Column(\n          children: [\n            ContinueReadButton(\n              viewFuture: _viewFuture,\n              onChoose: (int? epOrder, int? pictureRank) {\n                if (epOrder != null && pictureRank != null) {\n                  for (var i in _epList) {\n                    if (i.order == epOrder) {\n                      _push(_comicInfo, _epList, epOrder, pictureRank);\n                      return;\n                    }\n                  }\n                } else {\n                  _push(\n                      _comicInfo, _epList, _epList.reversed.first.order, null);\n                  return;\n                }\n              },\n            ),\n            Wrap(\n              spacing: 10,\n              runSpacing: 10,\n              alignment: WrapAlignment.spaceAround,\n              children: [\n                ..._epList.map((e) {\n                  return MaterialButton(\n                    onPressed: () {\n                      _push(_comicInfo, _epList, e.order, null);\n                    },\n                    color: Colors.white,\n                    child: Text(\n                      e.title,\n                      style: const TextStyle(color: Colors.black),\n                    ),\n                  );\n                }),\n              ],\n            ),\n          ],\n        );\n      },\n      onRefresh: () async {\n        setState(() {\n          _epListFuture = _loadEps();\n        });\n      },\n    );\n  }\n\n  Future _push(ComicInfo comicInfo, List<Ep> epList, int order, int? rank) async {\n    if (currentIgnoreInfoHistory()) {\n      await method.comicInfo(widget.comicId, false);\n    }\n    Navigator.push(\n      context,\n      mixRoute(\n        builder: (context) => ComicReaderScreen(\n          comicInfo: comicInfo,\n          epList: epList,\n          currentEpOrder: order,\n          initPicturePosition: rank,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/ComicReaderScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/FullScreenUI.dart';\nimport 'package:pikapika/basic/config/Quality.dart';\nimport 'package:pikapika/screens/components/ContentError.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'DownloadConfirmScreen.dart';\nimport 'components/ImageReader.dart';\nimport 'components/RightClickPop.dart';\n\n// 在线阅读漫画\nclass ComicReaderScreen extends StatefulWidget {\n  final ComicInfo comicInfo;\n  final List<Ep> epList;\n  final int currentEpOrder;\n  final int? initPicturePosition;\n  late final bool autoFullScreen;\n\n  ComicReaderScreen({\n    Key? key,\n    required this.comicInfo,\n    required this.epList,\n    required this.currentEpOrder,\n    this.initPicturePosition,\n    bool? autoFullScreen,\n  }) : super(key: key) {\n    this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen();\n  }\n\n  @override\n  State<StatefulWidget> createState() => _ComicReaderScreenState();\n}\n\nclass _ComicReaderScreenState extends State<ComicReaderScreen> {\n  late Ep _ep;\n  late bool _fullScreen = false;\n  late Future<List<RemoteImageInfo>> _future;\n  int? _lastChangeRank;\n  bool _replacement = false;\n\n  Future<List<RemoteImageInfo>> _load() async {\n    if (widget.initPicturePosition == null) {\n      await method.storeViewEp(widget.comicInfo.id, _ep.order, _ep.title, 0);\n    }\n    List<RemoteImageInfo> list = [];\n    var _needLoadPage = 0;\n    late PicturePage page;\n    do {\n      page = await method.comicPicturePageWithQuality(\n        widget.comicInfo.id,\n        widget.currentEpOrder,\n        ++_needLoadPage,\n        currentQualityCode(),\n      );\n      list.addAll(page.docs.map((element) => element.media));\n    } while (page.pages > page.page);\n    if (widget.autoFullScreen) {\n      setState(() {\n        SystemChrome.setEnabledSystemUIMode(\n          SystemUiMode.manual,\n          overlays: [],\n        );\n        _fullScreen = true;\n      });\n    }\n    return list;\n  }\n\n  Future _onPositionChange(int position) async {\n    _lastChangeRank = position;\n    return method.storeViewEp(\n        widget.comicInfo.id, _ep.order, _ep.title, position);\n  }\n\n  FutureOr<dynamic> _onChangeEp(int epOrder) {\n    var orderMap = <int, Ep>{};\n    for (var element in widget.epList) {\n      orderMap[element.order] = element;\n    }\n    if (orderMap.containsKey(epOrder)) {\n      _replacement = true;\n      Navigator.of(context).pushReplacement(\n        mixRoute(\n          builder: (context) => ComicReaderScreen(\n            comicInfo: widget.comicInfo,\n            epList: widget.epList,\n            currentEpOrder: epOrder,\n            autoFullScreen: _fullScreen,\n          ),\n        ),\n      );\n    }\n  }\n\n  FutureOr<dynamic> _onReloadEp() {\n    _replacement = true;\n    Navigator.of(context).pushReplacement(mixRoute(\n      builder: (context) => ComicReaderScreen(\n        comicInfo: widget.comicInfo,\n        epList: widget.epList,\n        currentEpOrder: widget.currentEpOrder,\n        initPicturePosition: _lastChangeRank ?? widget.initPicturePosition,\n        // maybe null\n        autoFullScreen: _fullScreen,\n      ),\n    ));\n  }\n\n  FutureOr<dynamic> _onDownload() {\n    Navigator.push(\n      context,\n      mixRoute(\n        builder: (context) => DownloadConfirmScreen(\n          comicInfo: widget.comicInfo,\n          epList: widget.epList.reversed.toList(),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void initState() {\n    // EP\n    for (var element in widget.epList) {\n      if (element.order == widget.currentEpOrder) {\n        _ep = element;\n      }\n    }\n    // INIT\n    _future = _load();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (!_replacement) {\n      switchFullScreenUI();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return readerKeyboardHolder(_build(context));\n  }\n\n  Widget _build(BuildContext context) {\n    return FutureBuilder(\n      future: _future,\n      builder: (BuildContext context,\n          AsyncSnapshot<List<RemoteImageInfo>> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: ContentError(\n              error: snapshot.error,\n              stackTrace: snapshot.stackTrace,\n              onRefresh: () async {\n                setState(() {\n                  _future = _load();\n                });\n              },\n            ),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: ContentLoading(label: tr('app.loading')),\n          );\n        }\n        var epNameMap = <int, String>{};\n        for (var element in widget.epList) {\n          epNameMap[element.order] = element.title;\n        }\n        return Scaffold(\n          body: ImageReader(\n            ImageReaderStruct(\n              images: snapshot.data!\n                  .map((e) => ReaderImageInfo(\n                        e.fileServer,\n                        e.path,\n                        null,\n                        null,\n                        null,\n                        null,\n                        null,\n                      ))\n                  .toList(),\n              fullScreen: _fullScreen,\n              onFullScreenChange: _onFullScreenChange,\n              onPositionChange: _onPositionChange,\n              initPosition: widget.initPicturePosition,\n              epNameMap: epNameMap,\n              epOrder: _ep.order,\n              comicTitle: widget.comicInfo.title,\n              onChangeEp: _onChangeEp,\n              onReloadEp: _onReloadEp,\n              onDownload: _onDownload,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Future _onFullScreenChange(bool fullScreen) async {\n    setState(() {\n      if (fullScreen) {\n        if (Platform.isAndroid || Platform.isIOS) {\n          SystemChrome.setEnabledSystemUIMode(\n            SystemUiMode.manual,\n            overlays: [],\n          );\n        }\n      } else {\n        switchFullScreenUI();\n      }\n      _fullScreen = fullScreen;\n    });\n  }\n}\n"
  },
  {
    "path": "lib/screens/ComicSubscribesScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\nimport 'package:pikapika/screens/components/CommonData.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\n\nimport '../basic/config/Address.dart';\nimport 'components/Badge.dart';\nimport 'components/Common.dart';\n\nclass IntoComicSubscribesScreenButton extends StatefulWidget {\n  const IntoComicSubscribesScreenButton({Key? key}) : super(key: key);\n\n  @override\n  State<IntoComicSubscribesScreenButton> createState() =>\n      _IntoComicSubscribesScreenButtonState();\n}\n\nclass _IntoComicSubscribesScreenButtonState\n    extends State<IntoComicSubscribesScreenButton> {\n  @override\n  void initState() {\n    super.initState();\n    subscribedEvent.subscribe(_setState);\n    _sync();\n  }\n\n  @override\n  void dispose() {\n    subscribedEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  _setState(_) {\n    setState(() {});\n  }\n\n  void _sync() async {\n    await updateSubscribed();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final count = allSubscribed.values.isEmpty\n        ? 0\n        : allSubscribed.values\n            .map((e) => e.newEpCount)\n            .reduce((value, element) => value + element);\n    return Badged(\n      badge: count == 0 ? null : count.toString(),\n      child: IconButton(\n        onPressed: () {\n          Navigator.push(\n            context,\n            MaterialPageRoute(\n              builder: (context) => const ComicSubscribesScreen(),\n            ),\n          );\n        },\n        icon: const Icon(Icons.alarm),\n      ),\n    );\n  }\n}\n\nclass ComicSubscribesScreen extends StatefulWidget {\n  const ComicSubscribesScreen({Key? key}) : super(key: key);\n\n  @override\n  State<ComicSubscribesScreen> createState() => _ComicSubscribesScreenState();\n}\n\nclass _ComicSubscribesScreenState extends State<ComicSubscribesScreen> {\n  @override\n  void initState() {\n    super.initState();\n    subscribedEvent.subscribe(_setState);\n  }\n\n  @override\n  void dispose() {\n    subscribedEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  void _setState(_) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.comic_subscribes.update_reminder')),\n        actions: [\n          commonPopMenu(context),\n          addressPopMenu(context),\n          _popMenu(context),\n        ],\n      ),\n      body: _body(context),\n    );\n  }\n\n  Widget _body(BuildContext context) {\n    final subs = allSubscribed.values.toList();\n    List<ComicSimple> comicList = [];\n    for (var comicSubscribe in subs) {\n      comicList.add(ComicSimple.fromJson(comicSubscribe.toSimpleJson()));\n    }\n    return ComicList(comicList);\n  }\n}\n\nWidget _popMenu(BuildContext context) {\n  return PopupMenuButton<int>(\n    icon: const Icon(Icons.more_vert),\n    itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n      PopupMenuItem<int>(\n        value: 0,\n        child: ListTile(\n          leading: const Icon(Icons.share),\n          title: Text(tr('screen.comic_subscribes.check_update')),\n        ),\n      ),\n      PopupMenuItem<int>(\n        value: 1,\n        child: ListTile(\n          leading: const Icon(Icons.image_search),\n          title: Text(tr('screen.comic_subscribes.cancel_all_update_reminder')),\n        ),\n      ),\n    ],\n    onSelected: (int value) {\n      switch (value) {\n        case 0:\n          updateSubscribedForce();\n          break;\n        case 1:\n          removeAllSubscribed();\n          break;\n      }\n    },\n  );\n}\n"
  },
  {
    "path": "lib/screens/ComicsScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'components/flutter_search_bar.dart' as fsb;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/PagerAction.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesMode.dart';\nimport 'package:pikapika/basic/store/Categories.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\nimport '../basic/Entities.dart';\nimport '../basic/config/Address.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'SearchScreen.dart';\nimport 'components/ComicPager.dart';\nimport 'components/Common.dart';\nimport 'components/GoDownloadSelect.dart';\nimport 'components/RightClickPop.dart';\n\n// 漫画列表\nclass ComicsScreen extends StatefulWidget {\n  final String? category; // 指定分类\n  final String? tag; // 指定标签\n  final String? creatorId; // 指定上传者\n  final String? creatorName; // 上传者名称 (仅显示)\n  final String? chineseTeam;\n\n  const ComicsScreen({\n    Key? key,\n    this.category,\n    this.tag,\n    this.creatorId,\n    this.creatorName,\n    this.chineseTeam,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicsScreenState();\n}\n\nclass _ComicsScreenState extends State<ComicsScreen> {\n  late final _comicListController = ComicListController();\n  late final fsb.SearchBar _categorySearchBar = fsb.SearchBar(\n    hintText: tr('screen.comics.search_hint') + ' - ${categoryTitle(widget.category)}',\n    inBar: false,\n    setState: setState,\n    onSubmitted: (value) {\n      if (value.isNotEmpty) {\n        Navigator.push(\n          context,\n          mixRoute(\n            builder: (context) =>\n                SearchScreen(keyword: value, category: widget.category),\n          ),\n        );\n      }\n    },\n    buildDefaultAppBar: (BuildContext context) {\n      return AppBar(\n        title: Text(categoryTitle(widget.category)),\n        actions: [\n          commonPopMenu(\n            context,\n            setState: setState,\n            comicListController: _comicListController,\n          ),\n          addressPopMenu(context),\n          _chooseCategoryAction(),\n          _categorySearchBar.getSearchAction(context),\n        ],\n      );\n    },\n  );\n\n  Widget _chooseCategoryAction() => IconButton(\n        onPressed: () async {\n          String? category = await chooseListDialog(context, tr('screen.comics.choose_category'), [\n            categoryTitle(null),\n            ...filteredList(\n              storedCategories,\n              (c) {\n                switch (currentShadowCategoriesMode()) {\n                  case ShadowCategoriesMode.BLACK_LIST:\n                    if (shadowCategories.contains(c)) return false;\n                    break;\n                  case ShadowCategoriesMode.WHITE_LIST:\n                    if (!shadowCategories.contains(c)) return false;\n                    break;\n                }\n                return true;\n              },\n            ),\n          ]);\n          if (category != null) {\n            if (category == categoryTitle(null)) {\n              category = null;\n            }\n            Navigator.of(context).pushReplacement(mixRoute(\n              builder: (context) {\n                return ComicsScreen(\n                  category: category,\n                  tag: widget.tag,\n                  creatorId: widget.creatorId,\n                  creatorName: widget.creatorName,\n                  chineseTeam: widget.chineseTeam,\n                );\n              },\n            ));\n          }\n        },\n        icon: const Icon(Icons.category),\n      );\n\n  Future<ComicsPage> _load(String _currentSort, int _currentPage) {\n    if (currentPagerAction() == PagerAction.CONTROLLER &&\n        _comicListController.selecting) {\n      setState(() {\n        _comicListController.selecting = false;\n      });\n    }\n    return method.comics(\n      _currentSort,\n      _currentPage,\n      category: widget.category ?? \"\",\n      tag: widget.tag ?? \"\",\n      creatorId: widget.creatorId ?? \"\",\n      chineseTeam: widget.chineseTeam ?? \"\",\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    PreferredSizeWidget? appBar;\n    if (widget.tag == null &&\n        widget.creatorId == null &&\n        widget.chineseTeam == null) {\n      // 只有只传分类或不传参数时时才开放搜索\n      appBar = _categorySearchBar.build(context);\n    } else {\n      var title = \"\";\n      if (widget.category != null) {\n        title += \"${widget.category} \";\n      }\n      if (widget.tag != null) {\n        title += \"${widget.tag} \";\n      }\n      if (widget.creatorName != null) {\n        title += \"${widget.creatorName} \";\n      }\n      if (widget.chineseTeam != null) {\n        title += \"${widget.chineseTeam} \";\n      }\n      appBar = AppBar(\n        title: Text(title),\n        actions: [\n          commonPopMenu(\n            context,\n            setState: setState,\n            comicListController: _comicListController,\n          ),\n          addressPopMenu(context),\n          _chooseCategoryAction(),\n        ],\n      );\n    }\n\n    if (_comicListController.selecting) {\n      appBar = downAppBar(context, _comicListController, setState);\n    }\n\n    var a = Scaffold(\n      appBar: appBar,\n      body: ComicPager(\n        fetchPage: _load,\n        comicListController: _comicListController,\n      ),\n    );\n    return WillPopScope(\n      onWillPop: () async {\n        if (_comicListController.selecting) {\n          setState(() {\n            _comicListController.selecting = false;\n          });\n          return false;\n        }\n        return true;\n      },\n      child: a,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/CommentScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Entities.dart' as e;\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/CommentItem.dart';\nimport 'package:pikapika/screens/components/CommentMainType.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\n\nimport 'components/BottomSheetInput.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass _CommentChildPage extends e.Page {\n  late List<ChildOfComment> docs;\n\n  _CommentChildPage.ofComic(CommentChildrenPage commentPage)\n      : super.of(commentPage.total, commentPage.limit, commentPage.page,\n            commentPage.pages) {\n    this.docs = commentPage.docs;\n  }\n\n  _CommentChildPage.ofGame(GameCommentChildrenPage commentPage)\n      : super.of(commentPage.total, commentPage.limit, commentPage.page,\n            commentPage.pages) {\n    this.docs = commentPage.docs;\n  }\n}\n\nclass CommentScreen extends StatefulWidget {\n  final CommentMainType mainType;\n  final String mainId;\n  final CommentBase comment;\n\n  const CommentScreen(this.mainType, this.mainId, this.comment, {Key? key})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _CommentScreenState();\n}\n\nclass _CommentScreenState extends State<CommentScreen> {\n  late int _currentPage = 1;\n  late Future<_CommentChildPage> _future = _loadPage();\n  late Key _key = UniqueKey();\n\n  Future<_CommentChildPage> _loadPage() async {\n    switch (widget.mainType) {\n      case CommentMainType.COMIC:\n        return _CommentChildPage.ofComic(await method.commentChildren(\n          widget.mainId,\n          widget.comment.id,\n          _currentPage,\n        ));\n      case CommentMainType.GAME:\n        return _CommentChildPage.ofGame(await method.gameCommentChildren(\n          widget.mainId,\n          widget.comment.id,\n          _currentPage,\n        ));\n    }\n  }\n\n  Widget _buildChildrenPager() {\n    return ContentBuilder(\n      key: _key,\n      future: _future,\n      onRefresh: _loadPage,\n      successBuilder:\n          (BuildContext context, AsyncSnapshot<_CommentChildPage> snapshot) {\n        var page = snapshot.data!;\n        return PikaListView(\n          children: [\n            _buildPrePage(page),\n            ...page.docs.map((e) => _buildComment(e)),\n            _buildNextPage(page),\n            _buildPostComment(),\n          ],\n        );\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.comment.title')),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.add_comment),\n            onPressed: _onReply,\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          ComicCommentItem(widget.mainType, widget.mainId, widget.comment),\n          Container(\n            height: 3,\n            color:\n                (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                    .withOpacity(.05),\n          ),\n          Expanded(child: _buildChildrenPager())\n        ],\n      ),\n    );\n  }\n\n  Widget _buildComment(CommentBase e) {\n    return ComicCommentItem(widget.mainType, widget.mainId, e);\n  }\n\n  Future _onReply() async {\n    showInputModalBottomSheet(\n      context: context,\n      onSubmitted: (text) async {\n        switch (widget.mainType) {\n          case CommentMainType.COMIC:\n            await method.postChildComment(widget.comment.id, text);\n            break;\n          case CommentMainType.GAME:\n            await method.postGameChildComment(widget.comment.id, text);\n            break;\n        }\n        setState(() {\n          _future = _loadPage();\n          _key = UniqueKey();\n          widget.comment.commentsCount++;\n        });\n        defaultToast(context, tr('screen.comment.success'));\n      },\n      hintText: tr('screen.comment.hint_text'),\n    );\n  }\n\n  Widget _buildPostComment() {\n    return InkWell(\n      onTap: _onReply,\n      child: Container(\n        decoration: BoxDecoration(\n          border: Border(\n            top: BorderSide(\n              width: .25,\n              style: BorderStyle.solid,\n              color: Colors.grey.shade500.withOpacity(.5),\n            ),\n            bottom: BorderSide(\n              width: .25,\n              style: BorderStyle.solid,\n              color: Colors.grey.shade500.withOpacity(.5),\n            ),\n          ),\n        ),\n        padding: const EdgeInsets.all(30),\n        child: Center(\n          child: Text(tr('screen.comment.i_have_something_to_say')),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPrePage(_CommentChildPage page) {\n    if (page.page > 1) {\n      return InkWell(\n        onTap: () {\n          setState(() {\n            _currentPage = page.page - 1;\n            _future = _loadPage();\n            _key = UniqueKey();\n          });\n        },\n        child: Container(\n          padding: const EdgeInsets.all(30),\n          child: Center(\n            child: Text(tr('app.previous_page')),\n          ),\n        ),\n      );\n    }\n    return Container();\n  }\n\n  Widget _buildNextPage(_CommentChildPage page) {\n    if (page.page < page.pages) {\n      return InkWell(\n        onTap: () {\n          setState(() {\n            _currentPage = page.page + 1;\n            _future = _loadPage();\n            _key = UniqueKey();\n          });\n        },\n        child: Container(\n          padding: const EdgeInsets.all(30),\n          child: Center(\n            child: Text(tr('app.next_page')),\n          ),\n        ),\n      );\n    }\n    return Container();\n  }\n}\n"
  },
  {
    "path": "lib/screens/DesktopAuthenticationScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nconst _key = \"desktopAuthPassword\";\n\nFuture<bool> needDesktopAuthentication() async {\n  return await method.loadProperty(_key, \"\") != \"\";\n}\n\nclass VerifyPassword extends StatefulWidget {\n  const VerifyPassword({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _VerifyPasswordState();\n}\n\nclass _VerifyPasswordState extends State<VerifyPassword> {\n  String _password = \"\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: Padding(\n          padding: const EdgeInsets.all(30),\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              TextField(\n                decoration: InputDecoration(labelText: tr('screen.desktop_authentication.current_password')),\n                onChanged: (value) {\n                  _password = value;\n                },\n              ),\n              Container(height: 10),\n              ElevatedButton(\n                onPressed: () async {\n                  String savedPassword = await method.loadProperty(_key, \"\");\n                  if (_password == savedPassword) {\n                    Navigator.of(context).pop(true);\n                  } else {\n                    ScaffoldMessenger.of(context)\n                        .showSnackBar(SnackBar(content: Text(tr('screen.desktop_authentication.password_error'))));\n                  }\n                },\n                child: Text(tr('app.confirm')),\n              ),\n              Expanded(child: Container()),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass SetPassword extends StatefulWidget {\n  const SetPassword({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _SetPasswordState();\n}\n\nclass _SetPasswordState extends State<SetPassword> {\n  String _password = \"\";\n  String _password2 = \"\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: Center(\n        child: Padding(\n          padding: const EdgeInsets.all(30),\n          child: Column(\n            children: [\n              Text(\n                tr('screen.desktop_authentication.password_initialization'),\n                style: TextStyle(\n                  height: 18,\n                ),\n              ),\n              Container(\n                height: 10,\n              ),\n              TextField(\n                decoration: InputDecoration(labelText: tr('screen.desktop_authentication.password')),\n                onChanged: (value) {\n                  _password = value;\n                },\n              ),\n              Container(\n                height: 10,\n              ),\n              TextField(\n                decoration: InputDecoration(labelText: tr('screen.desktop_authentication.re_enter_password')),\n                onChanged: (value) {\n                  _password2 = value;\n                },\n              ),\n              Container(\n                height: 10,\n              ),\n              Row(\n                children: [\n                  ElevatedButton(\n                    onPressed: () async {\n                      Navigator.of(context).pop(false);\n                    },\n                    child: Text(tr('app.cancel')),\n                  ),\n                  Container(width: 10),\n                  Expanded(\n                    child: ElevatedButton(\n                      onPressed: () async {\n                        if (_password != _password2) {\n                          ScaffoldMessenger.of(context).showSnackBar(\n                              SnackBar(content: Text(tr('screen.desktop_authentication.password_mismatch'))));\n                          return;\n                        }\n                        await method.saveProperty(_key, _password);\n                        Navigator.of(context).pop(true);\n                      },\n                      child: Text(tr('screen.desktop_authentication.set_password')),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadConfirmScreen.dart",
    "content": "import 'dart:convert';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport 'components/ComicInfoCard.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 确认下载\nclass DownloadConfirmScreen extends StatefulWidget {\n  final ComicInfo comicInfo;\n  final List<Ep> epList;\n\n  const DownloadConfirmScreen({\n    Key? key,\n    required this.comicInfo,\n    required this.epList,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadConfirmScreenState();\n}\n\nclass _DownloadConfirmScreenState extends State<DownloadConfirmScreen> {\n  DownloadComic? _task; // 之前的下载任务\n  final List<int> _taskedEps = []; // 已经下载的EP\n  final List<int> _selectedEps = []; // 选中的EP\n  late Future f = _load();\n\n  Future<dynamic> _load() async {\n    _taskedEps.clear();\n    _task = await method.loadDownloadComic(widget.comicInfo.id);\n    if (_task != null) {\n      var epList = await method.downloadEpList(widget.comicInfo.id);\n      _taskedEps.addAll(epList.map((e) => e.epOrder));\n    }\n  }\n\n  void _selectAll() {\n    setState(() {\n      _selectedEps.clear();\n      for (var element in widget.epList) {\n        if (!_taskedEps.contains(element.order)) {\n          _selectedEps.add(element.order);\n        }\n      }\n    });\n  }\n\n  Future<dynamic> _download() async {\n    // 必须选中才能下载\n    if (_selectedEps.isEmpty) {\n      defaultToast(context, tr('screen.download_confirm.please_select_ep'));\n      return;\n    }\n    // 下载对象\n    Map<String, dynamic> create = {\n      \"id\": widget.comicInfo.id,\n      \"createdAt\": widget.comicInfo.createdAt,\n      \"updatedAt\": widget.comicInfo.updatedAt,\n      \"title\": widget.comicInfo.title,\n      \"author\": widget.comicInfo.author,\n      \"pagesCount\": widget.comicInfo.pagesCount,\n      \"epsCount\": widget.comicInfo.epsCount,\n      \"finished\": widget.comicInfo.finished,\n      \"categories\": json.encode(widget.comicInfo.categories),\n      \"thumbOriginalName\": widget.comicInfo.thumb.originalName,\n      \"thumbFileServer\": widget.comicInfo.thumb.fileServer,\n      \"thumbPath\": widget.comicInfo.thumb.path,\n      \"description\": widget.comicInfo.description,\n      \"chineseTeam\": widget.comicInfo.chineseTeam,\n      \"tags\": json.encode(widget.comicInfo.tags),\n    };\n    // 下载EP列表\n    List<Map<String, dynamic>> list = [];\n    for (var element in widget.epList) {\n      if (_selectedEps.contains(element.order)) {\n        list.add({\n          \"comicId\": widget.comicInfo.id,\n          \"id\": element.id,\n          \"updatedAt\": element.updatedAt,\n          \"epOrder\": element.order,\n          \"title\": element.title,\n        });\n      }\n    }\n    try {\n      // 如果之前下载过就将EP加入下载\n      // 如果之前没有下载过就创建下载\n      if (_task != null) {\n        await method.addDownload(create, list);\n      } else {\n        await method.createDownload(create, list);\n      }\n      // 退出\n      defaultToast(context, tr('screen.download_confirm.already_added_to_download_list'));\n      Navigator.pop(context);\n    } catch (e, s) {\n      defaultToast(context, e.toString());\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(\"下载 - ${widget.comicInfo.title}\"),\n      ),\n      body: FutureBuilder(\n        future: f,\n        builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n          if (snapshot.hasError) {\n            print(snapshot.error);\n            print(snapshot.stackTrace);\n            return const Text('error');\n          }\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr('app.loading'));\n          }\n          return PikaListView(\n            children: [\n              ComicInfoCard(widget.comicInfo),\n              _buildButtons(),\n              Wrap(\n                alignment: WrapAlignment.spaceAround,\n                runSpacing: 10,\n                spacing: 10,\n                children: [\n                  ...widget.epList.map((e) {\n                    return Container(\n                      padding: const EdgeInsets.all(5),\n                      child: MaterialButton(\n                        onPressed: () {\n                          _clickOfEp(e);\n                        },\n                        color: _colorOfEp(e),\n                        child: Row(\n                          mainAxisSize: MainAxisSize.min,\n                          mainAxisAlignment: MainAxisAlignment.center,\n                          children: [\n                            _iconOfEp(e),\n                            Container(\n                              width: 10,\n                            ),\n                            Text(e.title,\n                                style: const TextStyle(color: Colors.black)),\n                          ],\n                        ),\n                      ),\n                    );\n                  }),\n                ],\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildButtons() {\n    var theme = Theme.of(context);\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: Colors.grey.shade200,\n          ),\n        ),\n      ),\n      child: Wrap(\n        spacing: 10,\n        runSpacing: 10,\n        alignment: WrapAlignment.spaceAround,\n        children: [\n          MaterialButton(\n            color: theme.colorScheme.secondary,\n            textColor: Colors.white,\n            onPressed: _selectAll,\n            child: Text(tr('app.select_all')),\n          ),\n          MaterialButton(\n            color: theme.colorScheme.secondary,\n            textColor: Colors.white,\n            onPressed: _download,\n            child: Text(tr('app.confirm_download')),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Color _colorOfEp(Ep e) {\n    if (_taskedEps.contains(e.order)) {\n      return Colors.grey.shade300;\n    }\n    if (_selectedEps.contains(e.order)) {\n      return Colors.blueGrey.shade300;\n    }\n    return Colors.grey.shade200;\n  }\n\n  Icon _iconOfEp(Ep e) {\n    if (_taskedEps.contains(e.order)) {\n      return const Icon(Icons.download_rounded, color: Colors.black);\n    }\n    if (_selectedEps.contains(e.order)) {\n      return const Icon(Icons.check_box, color: Colors.black);\n    }\n    return const Icon(Icons.check_box_outline_blank, color: Colors.black);\n  }\n\n  void _clickOfEp(Ep e) {\n    if (_taskedEps.contains(e.order)) {\n      return;\n    }\n    if (_selectedEps.contains(e.order)) {\n      setState(() {\n        _selectedEps.remove(e.order);\n      });\n    } else {\n      setState(() {\n        _selectedEps.add(e.order);\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadExportGroupScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\n\nimport '../basic/Entities.dart';\nimport '../basic/Method.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'DownloadExportingGroupScreen.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/DownloadInfoCard.dart';\nimport 'components/ListView.dart';\n\nclass DownloadExportGroupScreen extends StatefulWidget {\n  const DownloadExportGroupScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadExportGroupScreenState();\n}\n\nclass _DownloadExportGroupScreenState extends State<DownloadExportGroupScreen> {\n  late Future<List<DownloadComic>> _f = method.allDownloads(\"\");\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _f,\n      builder:\n          (BuildContext context, AsyncSnapshot<List<DownloadComic>> snapshot) {\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr(\"screen.download_export_group.title\")),\n            ),\n            body: ContentLoading(label: tr(\"app.loading\")),\n          );\n        }\n\n        if (snapshot.hasError) {\n          print(\"${snapshot.error}\");\n          print(\"${snapshot.stackTrace}\");\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr(\"screen.download_export_group.title\")),\n            ),\n            body: Center(child: Text(tr(\"app.load_failed\"))),\n          );\n        }\n\n        var data = snapshot.data!;\n\n        List<Widget> ws = [];\n        List<DownloadComic> exportable = [];\n        List<String> exportableIds = [];\n        for (var value in data) {\n          if (!value.deleting && value.downloadFinished) {\n            ws.add(downloadWidget(value));\n            exportable.add(value);\n            exportableIds.add(value.id);\n          }\n        }\n\n        return Scaffold(\n          appBar: AppBar(\n            title: Text(tr(\"screen.download_export_group.title\")),\n            actions: [\n              _selectAllButton(exportableIds),\n              _goToExport(),\n            ],\n          ),\n          body: RefreshIndicator(\n            onRefresh: () async {\n              setState(() {\n                selected.clear();\n                _f = method.allDownloads(\"\");\n              });\n            },\n            child: PikaListView(\n              children: ws,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  List<String> selected = [];\n\n  Widget downloadWidget(DownloadComic e) {\n    return InkWell(\n      onTap: () {\n        if (selected.contains(e.id)) {\n          selected.remove(e.id);\n        } else {\n          selected.add(e.id);\n        }\n        setState(() {});\n      },\n      child: Stack(children: [\n        DownloadInfoCard(\n          task: e,\n        ),\n        Row(children: [\n          Expanded(child: Container()),\n          Padding(\n            padding: const EdgeInsets.all(5),\n            child: Icon(\n              selected.contains(e.id)\n                  ? Icons.check_circle_sharp\n                  : Icons.circle_outlined,\n              color: Theme\n                  .of(context)\n                  .colorScheme\n                  .secondary,\n            ),\n          ),\n        ]),\n      ]),\n    );\n  }\n\n  Widget _selectAllButton(List<String> exportableIds) {\n    return MaterialButton(\n        minWidth: 0,\n        onPressed: () async {\n          setState(() {\n            if (selected.length >= exportableIds.length) {\n              selected.clear();\n            } else {\n              selected.clear();\n              selected.addAll(exportableIds);\n            }\n          });\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            const Icon(\n              Icons.select_all,\n              size: 18,\n              color: Colors.white,\n            ),\n            Text(\n              tr(\"app.select_all\"),\n              style: TextStyle(fontSize: 14, color: Colors.white),\n            ),\n            Expanded(child: Container()),\n          ],\n        ));\n  }\n\n  Widget _goToExport() {\n    return MaterialButton(\n        minWidth: 0,\n        onPressed: () async {\n          if (selected.isEmpty) {\n            defaultToast(context, tr(\"screen.download_export_group.please_select_content\"));\n            return;\n          }\n          final exported = await Navigator.of(context).push(\n            mixRoute(\n              builder: (context) =>\n                  DownloadExportingGroupScreen(\n                    idList: selected,\n                  ),\n            ),\n          );\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            const Icon(\n              Icons.check,\n              size: 18,\n              color: Colors.white,\n            ),\n            Text(\n              tr(\"app.confirm\"),\n              style: TextStyle(fontSize: 14, color: Colors.white),\n            ),\n            Expanded(child: Container()),\n          ],\n        ));\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadExportToFileScreen.dart",
    "content": "import 'dart:async';\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Channels.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/ExportRename.dart';\nimport 'package:pikapika/screens/DownloadExportToSocketScreen.dart';\nimport '../basic/config/ExportPath.dart';\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/IsPro.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/DownloadInfoCard.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 导出\nclass DownloadExportToFileScreen extends StatefulWidget {\n  final String comicId;\n  final String comicTitle;\n\n  const DownloadExportToFileScreen({\n    required this.comicId,\n    required this.comicTitle,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadExportToFileScreenState();\n}\n\nclass _DownloadExportToFileScreenState\n    extends State<DownloadExportToFileScreen> {\n  late DownloadComic _task;\n  late Future _future = _load();\n  late bool exporting = false;\n  late String exportMessage = tr(\"screen.download_export_group.exporting\");\n  late String exportResult = \"\";\n\n  Future _load() async {\n    _task = (await method.loadDownloadComic(widget.comicId))!;\n  }\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    setState(() {\n      exportMessage = event;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: !exporting,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    if (exporting) {\n      return Scaffold(\n        body: ContentLoading(label: exportMessage),\n      );\n    }\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.download_export_to_file.title\") + \" - \" + widget.comicTitle),\n      ),\n      body: FutureBuilder(\n        future: _future,\n        builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n          if (snapshot.hasError) {\n            return ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  setState(() {\n                    _future = _load();\n                  });\n                });\n          }\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr(\"app.loading\"));\n          }\n          return PikaListView(\n            children: [\n              DownloadInfoCard(task: _task),\n              Container(\n                padding: const EdgeInsets.all(8),\n                child: exportResult != \"\" ? Text(exportResult) : Container(),\n              ),\n              displayExportPathInfo(),\n              Container(height: 15),\n              _exportPkzButton(),\n              Container(height: 10),\n              _exportPkiButton(),\n              Container(height: 10),\n              _exportHtmlZipButton(),\n              Container(height: 10),\n              _exportToHtmlJPEGButton(),\n              Container(height: 10),\n              _exportToHtmlPdfButton(),\n              Container(height: 10),\n              _exportToHtmlPdfFolderButton(),\n              Container(height: 10),\n              _exportToHtmlEpubButton(),\n              Container(height: 10),\n              _exportToJPEGSZIPButton(),\n              Container(height: 10),\n              _exportToHtmlJPEGNotDownOverButton(),\n              Container(height: 10),\n              _exportComicDownloadToCbzsZipButton(),\n              Container(height: 10),\n              MaterialButton(\n                onPressed: () async {\n                  Navigator.of(context).push(\n                    mixRoute(\n                      builder: (context) => DownloadExportToSocketScreen(\n                        task: _task,\n                        comicId: widget.comicId,\n                        comicTitle: widget.comicTitle,\n                      ),\n                    ),\n                  );\n                },\n                child: _buildButtonInner(tr(\"screen.download_export_to_file.transfer_to_other_device\")),\n              ),\n              Container(height: 40),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _exportPkzButton() {\n    return MaterialButton(\n      onPressed: () async {\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context, \n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_pkz_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToPkz(\n            [widget.comicId],\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_group.export_to_pkz_title\") + showExportPath(),\n      ),\n    );\n  }\n\n  Widget _exportPkiButton() {\n    return MaterialButton(\n      onPressed: () async {\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_pki_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToPki(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_group.export_to_pki_title\") + showExportPath(),\n      ),\n    );\n  }\n\n  Widget _exportHtmlZipButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_zip_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownload(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_to_file.export_to_zip_desc\") + showExportPath(),\n      ),\n    );\n  }\n\n  Widget _exportToHtmlJPEGButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_jpeg_zip_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToJPG(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(tr(\"screen.download_export_group.export_to_jpeg_zip_title\") + showExportPath()),\n    );\n  }\n\n  Widget _exportToHtmlPdfButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_pdf_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToPDF(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(tr(\"screen.download_export_group.export_to_pdf_title\") + showExportPath()),\n    );\n  }\n\n  Widget _exportToHtmlEpubButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_epub_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToEpub(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(tr(\"screen.download_export_group.export_to_epub_title\") + showExportPath()),\n    );\n  }\n\n  Widget _exportToHtmlPdfFolderButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_to_file.export_to_pdf_folder_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToPDFFolder(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(tr(\"screen.download_export_group.export_to_pdf_folder_title\") + showExportPath()),\n    );\n  }\n\n  Widget _exportToJPEGSZIPButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_group.export_to_jpeg_zip_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadJpegZip(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_group.export_to_jpeg_zip_title\")+ (!isPro ? \"\\n(${tr('app.pro')})\" : \"\"),\n      ),\n    );\n  }\n\n  Widget _exportToHtmlJPEGNotDownOverButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_group.export_to_jpeg_zip_title_not_down_over') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicJpegsEvenNotFinish(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_group.export_to_jpeg_zip_title_not_down_over\") + (!isPro ? \"\\n(${tr('app.pro')})\" : \"\"),\n      ),\n    );\n  }\n\n  Widget _exportComicDownloadToCbzsZipButton() {\n    return MaterialButton(\n      onPressed: () async {\n        if (!isPro) {\n          defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n          return;\n        }\n        var name = \"\";\n        if (currentExportRename()) {\n          var rename = await inputString(\n            context,\n            tr(\"screen.download_export_to_file.input_save_name\"),\n            defaultValue: _task.title,\n          );\n          if (rename != null && rename.isNotEmpty) {\n            name = rename;\n          } else {\n            return;\n          }\n        } else {\n          if (!await confirmDialog(\n              context,\n              tr('screen.download_export_to_file.export_confirm'),\n              tr('screen.download_export_group.export_to_cbz_title') + showExportPath(),\n          )) {\n            return;\n          }\n        }\n        try {\n          setState(() {\n            exporting = true;\n          });\n          await method.exportComicDownloadToCbzsZip(\n            widget.comicId,\n            await attachExportPath(),\n            name,\n          );\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_success\");\n          });\n        } catch (e) {\n          setState(() {\n            exportResult = tr(\"screen.download_export_group.export_failed\") + \" $e\";\n          });\n        } finally {\n          setState(() {\n            exporting = false;\n          });\n        }\n      },\n      child: _buildButtonInner(\n        tr(\"screen.download_export_to_file.export_to_cbz_desc\") + (!isPro ? \"\\n(${tr('app.pro')})\" : \"\"),\n      ),\n    );\n  }\n\n  Widget _buildButtonInner(String text) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return Container(\n          width: constraints.maxWidth,\n          padding: const EdgeInsets.all(15),\n          color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n              .withOpacity(.05),\n          child: Text(\n            text,\n            textAlign: TextAlign.center,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadExportToSocketScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Channels.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/DownloadInfoCard.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 传输到其他设备\nclass DownloadExportToSocketScreen extends StatefulWidget {\n  final DownloadComic task;\n  final String comicId;\n  final String comicTitle;\n\n  const DownloadExportToSocketScreen({\n    required this.task,\n    required this.comicId,\n    required this.comicTitle,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadExportToSocketScreenState();\n}\n\nclass _DownloadExportToSocketScreenState\n    extends State<DownloadExportToSocketScreen> {\n  late Future<int> _future = method.exportComicUsingSocket(widget.comicId);\n  late final Future<String> _ipFuture = method.clientIpSet();\n\n  late String exportMessage = \"\";\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    method.exportComicUsingSocketExit();\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    if (event is String) {\n      setState(() {\n        exportMessage = event;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.download_export_to_socket.title\") + \" - \" + widget.comicTitle),\n      ),\n      body: FutureBuilder(\n        future: _future,\n        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {\n          if (snapshot.hasError) {\n            return ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  setState(() {\n                    _future = method.exportComicUsingSocket(widget.comicId);\n                  });\n                });\n          }\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr(\"screen.download_export_to_socket.loading\"));\n          }\n          return PikaListView(\n            children: [\n              DownloadInfoCard(task: widget.task),\n              Container(\n                padding: const EdgeInsets.all(8),\n                child: Column(\n                  children: [\n                     Text(\n                        tr(\"screen.download_export_to_socket.tips\")),\n                    FutureBuilder(\n                      future: _ipFuture,\n                      builder: (BuildContext context,\n                          AsyncSnapshot<String> snapshot) {\n                        if (snapshot.hasError) {\n                          return Text(tr(\"screen.download_export_to_socket.get_ip_failed\"));\n                        }\n                        if (snapshot.connectionState != ConnectionState.done) {\n                          return Text(tr(\"screen.download_export_to_socket.getting_ip\"));\n                        }\n                        return Text('${snapshot.data}');\n                      },\n                    ),\n                    Text(tr(\"screen.download_export_to_socket.port\") + ':${snapshot.data}'),\n                    Text(exportMessage),\n                  ],\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadExportingGroupScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport '../basic/Channels.dart';\nimport '../basic/Method.dart';\nimport '../basic/config/ExportPath.dart';\nimport '../basic/config/ExportRename.dart';\nimport '../basic/config/IsPro.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\n\nclass DownloadExportingGroupScreen extends StatefulWidget {\n  final List<String> idList;\n\n  const DownloadExportingGroupScreen({Key? key, required this.idList})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadExportingGroupScreenState();\n}\n\nclass _DownloadExportingGroupScreenState\n    extends State<DownloadExportingGroupScreen> {\n  bool exporting = false;\n  bool exported = false;\n  bool exportFail = false;\n  dynamic e;\n  String exportMessage = tr(\"screen.download_export_group.exporting\");\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    setState(() {\n      exportMessage = event;\n    });\n  }\n\n  Widget _body() {\n    if (exporting) {\n      return ContentLoading(label: exportMessage);\n    }\n    if (exportFail) {\n      return Center(child: Text(tr(\"screen.download_export_group.export_failed\") + \"\\n$e\"));\n    }\n    if (exported) {\n      return Center(child: Text(tr(\"screen.download_export_group.export_success\")));\n    }\n    return PikaListView(\n      children: [\n        Container(height: 20),\n        displayExportPathInfo(),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportPkz,\n          child: _buildButtonInner(tr(\"screen.download_export_group.export_to_pkz\")),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportPkis,\n          child: _buildButtonInner(tr(\"screen.download_export_group.export_to_pki\")),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportZips,\n          child: _buildButtonInner(\n              tr(\"screen.download_export_group.export_to_zip\") +\n                  (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\")),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportToJPEGSZips,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_jpeg_zip\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportToJPEGSFolders,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_jpeg_folder\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportToPdf,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_pdf\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportToPdfFolder,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_pdf_folder\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportToEpub,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_epub\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _exportComicDownloadToCbzsZip,\n          child: _buildButtonInner(\n            tr(\"screen.download_export_group.export_to_cbz\") +\n                (!isPro ? \"\\n\" + tr(\"screen.download_export_group.after_power_use\") : \"\"),\n          ),\n        ),\n        Container(height: 20),\n      ],\n    );\n  }\n\n  _exportPkz() async {\n    var name = \"\";\n    if (currentExportRename()) {\n      var rename = await inputString(\n        context,\n        tr(\"screen.download_export_group.input_save_name\"),\n        defaultValue: \"${DateTime.now().millisecondsSinceEpoch}\",\n      );\n      if (rename != null && rename.isNotEmpty) {\n        name = rename;\n      } else {\n        return;\n      }\n    } else {\n      if (!await confirmDialog(\n          context,\n          tr(\"screen.download_export_group.export_confirm\"),\n          tr(\"screen.download_export_group.export_to_pkz_title\") + showExportPath())) {\n        return;\n      }\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      await method.exportComicDownloadToPkz(\n        widget.idList,\n        await attachExportPath(),\n        name,\n      );\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportPkis() async {\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_pki_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      await method.exportAnyComicDownloadsToPki(\n        widget.idList,\n        await attachExportPath(),\n      );\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportZips() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_zip_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      await method.exportAnyComicDownloadsToZip(\n        widget.idList,\n        await attachExportPath(),\n      );\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportToJPEGSZips() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_jpeg_zip_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadJpegZip(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportToJPEGSFolders() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_jpeg_folder_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadToJPG(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportToPdf() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_pdf_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadToPDF(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportToEpub() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_epub_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadToEpub(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportToPdfFolder() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_pdf_folder_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadToPDFFolder(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  _exportComicDownloadToCbzsZip() async {\n    if (!isPro) {\n      defaultToast(context, tr(\"screen.download_export_group.please_power_up\"));\n      return;\n    }\n    if (!await confirmDialog(\n        context,\n        tr(\"screen.download_export_group.export_confirm\"),\n        tr(\"screen.download_export_group.export_to_cbz_title\") + showExportPath())) {\n      return;\n    }\n    try {\n      setState(() {\n        exporting = true;\n      });\n      final path = await attachExportPath();\n      for (var id in widget.idList) {\n        await method.exportComicDownloadToCbzsZip(\n          id,\n          path,\n          \"\",\n        );\n      }\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return WillPopScope(\n      child: Scaffold(\n        appBar: AppBar(\n          title: Text(tr(\"screen.download_export_group.title\")),\n        ),\n        body: _body(),\n      ),\n      onWillPop: () async {\n        if (exporting) {\n          defaultToast(context, tr(\"screen.download_export_group.exporting_please_wait\"));\n          return false;\n        }\n        return true;\n      },\n    );\n  }\n\n  Widget _buildButtonInner(String text) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return Container(\n          width: constraints.maxWidth,\n          padding: const EdgeInsets.all(15),\n          color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n              .withOpacity(.05),\n          child: Text(\n            text,\n            textAlign: TextAlign.center,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadImportScreen.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:filesystem_picker/filesystem_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Channels.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/ChooserRoot.dart';\n\nimport '../basic/Cross.dart';\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/ImportNotice.dart';\nimport '../basic/config/IsPro.dart';\nimport 'PkzArchiveScreen.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 导入\nclass DownloadImportScreen extends StatefulWidget {\n  const DownloadImportScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadImportScreenState();\n}\n\nclass _DownloadImportScreenState extends State<DownloadImportScreen> {\n  bool _importing = false;\n  String _importMessage = \"\";\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    if (event is String) {\n      setState(() {\n        _importMessage = event;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: !_importing,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    if (_importing) {\n      return Scaffold(\n        body: ContentLoading(label: _importMessage),\n      );\n    }\n\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.download_import.title\")),\n      ),\n      body: PikaListView(\n        children: [\n          Container(\n            padding: const EdgeInsets.all(10),\n            child: Text(_importMessage),\n          ),\n          Container(height: 20),\n          importNotice(context),\n          Container(height: 20),\n          _fileImportButton(),\n          Container(height: 20),\n          _networkImportButton(),\n          Container(height: 20),\n          _importDirFilesZipButton(),\n          Container(height: 40),\n        ],\n      ),\n    );\n  }\n\n  Widget _fileImportButton() {\n    return MaterialButton(\n      height: 80,\n      onPressed: () async {\n        late String chooseRoot;\n        try {\n          chooseRoot = await currentChooserRoot();\n        } catch (e) {\n          defaultToast(context, \"$e\");\n          return;\n        }\n        String? path;\n        if (Platform.isAndroid) {\n          path = await FilesystemPicker.open(\n            title: tr(\"screen.download_import.open_file\"),\n            context: context,\n            rootDirectory: Directory(chooseRoot),\n            fsType: FilesystemType.file,\n            folderIconColor: Colors.teal,\n            allowedExtensions: ['.pkz', '.zip', '.pki'],\n            fileTileSelectMode: FileTileSelectMode.wholeTile,\n          );\n        } else {\n          var ls = await FilePicker.platform.pickFiles(\n            dialogTitle: tr(\"screen.download_import.select_file\"),\n            allowMultiple: false,\n            initialDirectory: chooseRoot,\n            type: FileType.custom,\n            allowedExtensions: ['pkz', 'zip', 'pki'],\n            allowCompression: false,\n          );\n          path = ls != null && ls.count > 0 ? ls.paths[0] : null;\n        }\n        if (path != null) {\n          if (path.endsWith(\".pkz\")) {\n            Navigator.of(context).push(\n              mixRoute(\n                builder: (BuildContext context) =>\n                    PkzArchiveScreen(pkzPath: path!),\n              ),\n            );\n          } else if (path.endsWith(\".zip\") || path.endsWith(\".pki\")) {\n            try {\n              setState(() {\n                _importing = true;\n              });\n              if (path.endsWith(\".zip\")) {\n                await method.importComicDownload(path);\n              } else if (path.endsWith(\".pki\")) {\n                await method.importComicDownloadPki(path);\n              }\n              setState(() {\n                _importMessage = tr(\"screen.download_import.import_success\");\n              });\n            } catch (e) {\n              setState(() {\n                _importMessage = tr(\"screen.download_import.import_failed\") + \" $e\";\n              });\n            } finally {\n              setState(() {\n                _importing = false;\n              });\n            }\n          }\n        }\n      },\n      child: LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return Container(\n            width: constraints.maxWidth,\n            padding: const EdgeInsets.only(top: 15, bottom: 15),\n            color:\n                (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                    .withOpacity(.05),\n            child: Text(\n              tr(\"screen.download_import.select_file_desc\"),\n              textAlign: TextAlign.center,\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _networkImportButton() {\n    return MaterialButton(\n      height: 80,\n      onPressed: () async {\n        var path =\n            await inputString(context, tr(\"screen.download_import.input_address\"));\n        if (path != null) {\n          try {\n            setState(() {\n              _importing = true;\n            });\n            await method.importComicDownloadUsingSocket(path);\n            setState(() {\n              _importMessage = tr(\"screen.download_import.import_success\");\n            });\n          } catch (e) {\n            setState(() {\n              _importMessage = tr(\"screen.download_import.import_failed\") + \" $e\";\n            });\n          } finally {\n            setState(() {\n              _importing = false;\n            });\n          }\n        }\n      },\n      child: LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return Container(\n            width: constraints.maxWidth,\n            padding: const EdgeInsets.only(top: 15, bottom: 15),\n            color:\n                (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                    .withOpacity(.05),\n            child: Text(\n              tr(\"screen.download_import.import_from_other_device\"),\n              textAlign: TextAlign.center,\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _importDirFilesZipButton() {\n    return MaterialButton(\n      height: 80,\n      onPressed: () async {\n        late String? path;\n        try {\n          path = await chooseFolder(context);\n        } catch (e) {\n          defaultToast(context, \"$e\");\n          return;\n        }\n        if (path != null) {\n          try {\n            setState(() {\n              _importing = true;\n            });\n            await method.importComicDownloadDir(path);\n            setState(() {\n              _importMessage = tr(\"screen.download_import.import_success\");\n            });\n          } catch (e) {\n            setState(() {\n              _importMessage = tr(\"screen.download_import.import_failed\") + \" $e\";\n            });\n          } finally {\n            setState(() {\n              _importing = false;\n            });\n          }\n        }\n      },\n      child: LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return Container(\n            width: constraints.maxWidth,\n            padding: const EdgeInsets.only(top: 15, bottom: 15),\n            color:\n                (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                    .withOpacity(.05),\n            child: Text(\n              tr(\"screen.download_import.select_folder_desc\") + (!isPro ? \"\\n(${tr('app.pro')})\" : \"\"),\n              textAlign: TextAlign.center,\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadInfoScreen.dart",
    "content": "import 'dart:convert';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Navigator.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/ShowCommentAtDownload.dart';\nimport 'ComicInfoScreen.dart';\nimport 'DownloadExportToFileScreen.dart';\nimport 'DownloadReaderScreen.dart';\nimport 'components/ComicDescriptionCard.dart';\nimport 'components/ComicTagsCard.dart';\nimport 'components/CommentList.dart';\nimport 'components/CommentMainType.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ContinueReadButton.dart';\nimport 'components/DownloadInfoCard.dart';\nimport 'components/ListView.dart';\nimport 'components/Recommendation.dart';\n\n// 下载详情\nclass DownloadInfoScreen extends StatefulWidget {\n  final String comicId;\n  final String comicTitle;\n\n  const DownloadInfoScreen({\n    required this.comicId,\n    required this.comicTitle,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadInfoScreenState();\n}\n\nclass _DownloadInfoScreenState extends State<DownloadInfoScreen>\n    with RouteAware {\n  late Future<ViewLog?> _viewFuture = _loadViewLog();\n  late DownloadComic _task;\n  late List<DownloadEp> _epList = [];\n  late Future _future = _load();\n\n  Future _load() async {\n    _task = (await method.loadDownloadComic(widget.comicId))!;\n    _epList = await method.downloadEpList(widget.comicId);\n  }\n\n  Future<ViewLog?> _loadViewLog() {\n    return method.loadView(widget.comicId);\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    routeObserver.subscribe(this, ModalRoute.of(context)!);\n  }\n\n  @override\n  void didPopNext() {\n    setState(() {\n      _viewFuture = _loadViewLog();\n    });\n  }\n\n  @override\n  void dispose() {\n    routeObserver.unsubscribe(this);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(widget.comicTitle),\n        actions: [\n          IconButton(\n            onPressed: () async {\n              Navigator.push(\n                context,\n                mixRoute(\n                  builder: (context) => DownloadExportToFileScreen(\n                    comicId: widget.comicId,\n                    comicTitle: widget.comicTitle,\n                  ),\n                ),\n              );\n            },\n            icon: const Icon(Icons.add_to_home_screen),\n          ),\n          IconButton(\n            onPressed: () {\n              Navigator.push(\n                context,\n                mixRoute(\n                  builder: (context) => ComicInfoScreen(\n                    comicId: widget.comicId,\n                  ),\n                ),\n              );\n            },\n            icon: const Icon(Icons.settings_ethernet_outlined),\n          ),\n        ],\n      ),\n      body: FutureBuilder(\n        future: _future,\n        builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n          if (snapshot.hasError) {\n            return ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  setState(() {\n                    _future = _load();\n                  });\n                });\n          }\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr(\"screen.download_info.loading\"));\n          }\n          List<dynamic> tagsDynamic = json.decode(_task.tags);\n          List<String> tags = tagsDynamic.map((e) => \"$e\").toList();\n          var list = PikaListView(\n            children: [\n              DownloadInfoCard(task: _task, linkItem: true),\n              ComicTagsCard(tags),\n              ComicDescriptionCard(description: _task.description),\n              Container(height: 5),\n              _bottom(),\n            ],\n          );\n          // todo only pika task\n          if (showCommentAtDownload()) {\n            return DefaultTabController(\n              length: 3,\n              child: list,\n            );\n          }\n          return list;\n        },\n      ),\n    );\n  }\n\n  var _tabIndex = 0;\n\n  Widget _bottom() {\n    // todo only pika task\n    if (showCommentAtDownload()) {\n      final theme = Theme.of(context);\n      var _tabs = <Widget>[\n        Tab(text: tr(\"screen.download_info.chapter\") + \" (${_epList.length})\"),\n        Tab(text: tr(\"screen.download_info.comment\")),\n        Tab(text: tr(\"screen.download_info.recommend\")),\n      ];\n      var _views = <Widget>[\n        _chapters(),\n        CommentList(CommentMainType.COMIC, widget.comicId),\n        Recommendation(comicId: widget.comicId),\n      ];\n      return Column(children: [\n        Container(\n          height: 40,\n          color: theme.colorScheme.secondary.withOpacity(.025),\n          child: TabBar(\n            tabs: _tabs,\n            indicatorColor: theme.colorScheme.secondary,\n            labelColor: theme.colorScheme.secondary,\n            onTap: (val) async {\n              setState(() {\n                _tabIndex = val;\n              });\n            },\n          ),\n        ),\n        Container(height: 15),\n        _views[_tabIndex],\n        Container(height: 5),\n      ]);\n    }\n    return _chapters();\n  }\n\n  Widget _chapters() {\n    return Wrap(\n      spacing: 10,\n      runSpacing: 10,\n      alignment: WrapAlignment.spaceAround,\n      children: [\n        ContinueReadButton(\n          viewFuture: _viewFuture,\n          onChoose: (int? epOrder, int? pictureRank) {\n            if (epOrder != null && pictureRank != null) {\n              for (var i in _epList) {\n                if (i.epOrder == epOrder) {\n                  _push(_task, _epList, epOrder, pictureRank);\n                  return;\n                }\n              }\n            } else {\n              _push(_task, _epList, _epList.first.epOrder, null);\n            }\n          },\n        ),\n        ..._epList.reversed.map((e) {\n          return MaterialButton(\n            onPressed: () {\n              _push(_task, _epList, e.epOrder, null);\n            },\n            color: Colors.white,\n            child: Text(e.title, style: const TextStyle(color: Colors.black)),\n          );\n        }),\n      ],\n    );\n  }\n\n  void _push(\n    DownloadComic task,\n    List<DownloadEp> epList,\n    int epOrder,\n    int? rank,\n  ) {\n    Navigator.push(\n      context,\n      mixRoute(\n        builder: (context) => DownloadReaderScreen(\n          comicInfo: _task,\n          epList: _epList,\n          currentEpOrder: epOrder,\n          initPicturePosition: rank,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadListScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:math';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'components/flutter_search_bar.dart' as fsb;\nimport 'package:pikapika/basic/Channels.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/DownloadExportGroupScreen.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'DownloadImportScreen.dart';\nimport 'DownloadInfoScreen.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/DownloadInfoCard.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 下载列表\nclass DownloadListScreen extends StatefulWidget {\n  const DownloadListScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadListScreenState();\n}\n\nclass _DownloadListScreenState extends State<DownloadListScreen> {\n  String _search = \"\";\n  bool _selecting = false;\n  List<String> _selectingList = [];\n  List<String> _selectable = [];\n  String _filterCustomFolder = \"\";\n  List<String> _folderList = [];\n\n  late final fsb.SearchBar _searchBar = fsb.SearchBar(\n    hintText: tr('screen.download_list.search_download'),\n    inBar: false,\n    setState: setState,\n    onSubmitted: (value) {\n      _search = value;\n      _reloadList();\n      setState(() {});\n      _searchBar.controller.text = value;\n    },\n    buildDefaultAppBar: (BuildContext context) {\n      final theme = Theme.of(context);\n      final appBarIconColor = theme.appBarTheme.foregroundColor ?? Colors.white;\n      if (_selecting) {\n        return AppBar(\n          leading: IconButton(\n            icon: const Icon(Icons.arrow_back_ios),\n            onPressed: () {\n              if (_selecting) {\n                setState(() {\n                  _selecting = false;\n                  _selectingList = [];\n                });\n              } else {\n                Navigator.pop(context);\n              }\n            },\n          ),\n          title: Text(tr('screen.download_list.multi_select_operation')),\n          actions: [\n            _selectingCancelButton(appBarIconColor),\n            _selectingMoveButton(appBarIconColor),\n            _selectingDeleteButton(appBarIconColor),\n            _selectAllButton(appBarIconColor),\n          ],\n        );\n      }\n\n      return AppBar(\n        title: Text(_search == \"\"\n            ? tr('screen.download_list.download_list')\n            : (tr('screen.download_list.search_download') + ' - $_search')),\n        actions: [\n          //_searchBar.getSearchAction(context),\n          GestureDetector(\n            behavior: HitTestBehavior.opaque,\n            onTap: () {\n              _searchBar.beginSearch(context);\n            },\n            child: Column(\n              children: [\n                Expanded(child: Container()),\n                Icon(\n                  Icons.search,\n                  size: 18,\n                  color: appBarIconColor,\n                ),\n                Text(\n                  tr('screen.download_list.search'),\n                  style: TextStyle(fontSize: 14, color: appBarIconColor),\n                ),\n                Expanded(child: Container()),\n              ],\n            ),\n          ),\n          _customFolderButton(appBarIconColor),\n          _toSelectingButton(appBarIconColor),\n          _fileButton(appBarIconColor),\n          Container(width: 10),\n          pauseButton(appBarIconColor),\n        ],\n      );\n    },\n  );\n\n  DownloadComic? _downloading;\n  late bool _downloadRunning = false;\n  late Future<List<DownloadComic>> _f = method\n      .allDownloads(_search, customFolder: _filterCustomFolder)\n      .then((value) {\n    setState(() {\n      _selecting = false;\n      _selectingList = [];\n      _selectable = value.map((e) => e.id).toList();\n    });\n    return value;\n  });\n\n  List<DownloadComic> _data = [];\n\n  void _onMessageChange(String event) {\n    print(\"EVENT\");\n    print(event);\n    try {\n      setState(() {\n        _downloading = DownloadComic.fromJson(json.decode(event));\n      });\n    } catch (e, s) {\n      print(e);\n      print(s);\n    }\n  }\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"DOWNLOAD\");\n    method\n        .downloadRunning()\n        .then((val) => setState(() => _downloadRunning = val));\n    method.allCustomFolders().then((value) {\n      setState(() {\n        _folderList = value.where((e) => e.isNotEmpty).toList();\n      });\n    });\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  Widget _selectAllButton(Color appBarIconColor) {\n    return IconButton(\n      icon: Icon(\n        Icons.select_all,\n        size: 18,\n        color: appBarIconColor,\n      ),\n      onPressed: () async {\n        setState(() {\n          if (_selectingList.length >= _selectable.length) {\n            _selectingList.clear();\n          } else {\n            _selectingList.clear();\n            _selectingList.addAll(_selectable);\n          }\n        });\n      },\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final screen = Scaffold(\n      appBar: _searchBar.build(context),\n      body: FutureBuilder(\n        future: _f,\n        builder: (BuildContext context,\n            AsyncSnapshot<List<DownloadComic>> snapshot) {\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr('app.loading'));\n          }\n\n          if (snapshot.hasError) {\n            print(\"${snapshot.error}\");\n            print(\"${snapshot.stackTrace}\");\n            return Center(child: Text(tr('app.loading_failed')));\n          }\n\n          var data = snapshot.data!;\n          _data = data;\n          if (_downloading != null) {\n            try {\n              for (var i = 0; i < data.length; i++) {\n                if (_downloading!.id == data[i].id) {\n                  data[i].copy(_downloading!);\n                }\n              }\n            } catch (e, s) {\n              print(e);\n              print(s);\n            }\n          }\n\n          if (_selecting) {\n            return ListView(\n              children: [\n                ...data.map(selectingWidget),\n              ],\n            );\n          }\n\n          return RefreshIndicator(\n            onRefresh: () async {\n              _reloadList();\n              setState(() {});\n            },\n            child: PikaListView(\n              children: [\n                ...data.map(downloadWidget),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n    var w = rightClickPop(\n      child: screen,\n      context: context,\n      canPop: true,\n    );\n    return WillPopScope(\n      onWillPop: () async {\n        if (_selecting) {\n          setState(() {\n            _selecting = false;\n            _selectingList = [];\n          });\n          return false;\n        }\n        return true;\n      },\n      child: w,\n    );\n  }\n\n  Widget _customFolderButton(Color appBarIconColor) {\n    return IconButton(\n        onPressed: () async {\n          String? choose = await chooseListDialog(\n              context, tr('screen.download_list.select_folder'), [\n            tr('app.all'),\n            ..._folderList,\n          ]);\n          if (choose != null) {\n            if (choose == tr('app.all')) {\n              choose = \"\";\n            }\n            _filterCustomFolder = choose;\n            _reloadList();\n            setState(() {});\n          }\n        },\n        icon: Column(\n          children: [\n            Expanded(child: Container()),\n            Icon(\n              Icons.folder,\n              size: 18,\n              color: appBarIconColor,\n            ),\n            Text(\n              _customFolderName(),\n              style: TextStyle(fontSize: 14, color: appBarIconColor),\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n            ),\n            Expanded(child: Container()),\n          ],\n        ));\n  }\n\n  String _customFolderName() {\n    if (_filterCustomFolder == \"\") {\n      return tr('app.all');\n    }\n    return _filterCustomFolder;\n  }\n\n  Widget downloadWidget(DownloadComic e) {\n    return InkWell(\n      onTap: () {\n        if (e.deleting) {\n          return;\n        }\n        Navigator.push(\n          context,\n          mixRoute(\n            builder: (context) => DownloadInfoScreen(\n              comicId: e.id,\n              comicTitle: e.title,\n            ),\n          ),\n        );\n      },\n      onLongPress: () async {\n        String? action =\n            await chooseListDialog(context, e.title, [tr('app.delete')]);\n        if (action == tr('app.delete')) {\n          await method.deleteDownloadComic(e.id);\n          setState(() => e.deleting = true);\n        }\n      },\n      child: DownloadInfoCard(\n        task: e,\n        downloading: _downloading != null && _downloading!.id == e.id,\n      ),\n    );\n  }\n\n  Widget selectingWidget(DownloadComic e) {\n    return InkWell(\n      onTap: () {\n        if (e.deleting) {\n          defaultToast(context,\n              tr('screen.download_list.download_already_in_delete_queue'));\n          return;\n        } else {\n          if (_selectingList.contains(e.id)) {\n            setState(() {\n              _selectingList.remove(e.id);\n            });\n          } else {\n            setState(() {\n              _selectingList.add(e.id);\n            });\n          }\n        }\n      },\n      child: Stack(\n        children: [\n          DownloadInfoCard(\n            task: e,\n            downloading: _downloading != null && _downloading!.id == e.id,\n          ),\n          SizedBox(\n            height: imageHeight,\n            child: Align(\n              alignment: Alignment.bottomRight,\n              child: Container(\n                margin: const EdgeInsets.only(top: 5),\n                padding: const EdgeInsets.only(right: 10, left: 5),\n                decoration: BoxDecoration(\n                  color: Colors.grey.shade500.withOpacity(.1),\n                  borderRadius: const BorderRadius.only(\n                    topLeft: Radius.circular(5),\n                    bottomLeft: Radius.circular(5),\n                  ),\n                ),\n                child: Padding(\n                  padding: const EdgeInsets.all(5),\n                  child: Icon(\n                    _selectingList.contains(e.id)\n                        ? Icons.check_circle_sharp\n                        : Icons.circle_outlined,\n                    color: Theme.of(context).colorScheme.secondary,\n                  ),\n                ),\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _fileButton(Color appBarIconColor) {\n    return PopupMenuButton<int>(\n      itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n        PopupMenuItem<int>(\n          value: 0,\n          child: ListTile(\n            leading: const Icon(Icons.read_more),\n            title: Text(tr('screen.download_list.import')),\n          ),\n        ),\n        PopupMenuItem<int>(\n          value: 1,\n          child: ListTile(\n            leading: const Icon(Icons.save_alt),\n            title: Text(tr('screen.download_list.export')),\n          ),\n        ),\n      ],\n      onSelected: (a) async {\n        if (a == 0) {\n          await Navigator.push(\n            context,\n            mixRoute(\n              builder: (context) => const DownloadImportScreen(),\n            ),\n          );\n          _reloadList();\n          setState(() {});\n        } else if (a == 1) {\n          await Navigator.push(\n            context,\n            mixRoute(\n              builder: (context) => const DownloadExportGroupScreen(),\n            ),\n          );\n        }\n      },\n      child: Column(\n        children: [\n          Expanded(child: Container()),\n          Icon(\n            Icons.drive_file_move,\n            size: 18,\n            color: appBarIconColor,\n          ),\n          Text(\n            tr('screen.download_list.file'),\n            style: TextStyle(fontSize: 14, color: appBarIconColor),\n          ),\n          Expanded(child: Container()),\n        ],\n      ),\n    );\n  }\n\n  Future<void> _onPauseChangeClick() async {\n    await showDialog(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(tr('screen.download_list.download_task')),\n          content: Text(\n            _downloadRunning\n                ? tr('screen.download_list.pause_download')\n                : tr('screen.download_list.start_download'),\n          ),\n          actions: [\n            MaterialButton(\n              onPressed: () async {\n                Navigator.pop(context);\n              },\n              child: Text(tr('app.cancel')),\n            ),\n            MaterialButton(\n              onPressed: () async {\n                Navigator.pop(context);\n                var to = !_downloadRunning;\n                await method.setDownloadRunning(to);\n                setState(() {\n                  _downloadRunning = to;\n                });\n              },\n              child: Text(tr('app.confirm')),\n            ),\n          ],\n        );\n      },\n    );\n  }\n\n  Widget pauseButton(Color appBarIconColor) {\n    return PopupMenuButton<int>(\n        itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n              PopupMenuItem<int>(\n                value: 0,\n                child: ListTile(\n                  leading: const Icon(Icons.compare_arrows_sharp),\n                  title: Text(_downloadRunning\n                      ? tr('screen.download_list.pause_download')\n                      : tr('screen.download_list.start_download')),\n                ),\n              ),\n              PopupMenuItem<int>(\n                value: 1,\n                child: ListTile(\n                  leading: const Icon(Icons.sync_problem),\n                  title: Text(tr('screen.download_list.resume_failed')),\n                ),\n              ),\n            ],\n        onSelected: (a) async {\n          if (a == 0) {\n            await _onPauseChangeClick();\n          } else if (a == 1) {\n            await method.resetFailed();\n            _reloadList();\n            setState(() {});\n            defaultToast(\n                context, tr('screen.download_list.resume_failed_desc'));\n          }\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            Icon(\n              _downloadRunning\n                  ? Icons.compare_arrows_sharp\n                  : Icons.schedule_send,\n              size: 18,\n              color: appBarIconColor,\n            ),\n            Text(\n              _downloadRunning\n                  ? tr('screen.download_list.downloading')\n                  : tr('screen.download_list.paused'),\n              style: TextStyle(fontSize: 14, color: appBarIconColor),\n            ),\n            Expanded(child: Container()),\n          ],\n        ));\n  }\n\n  void _reloadList() {\n    _f = method\n        .allDownloads(_search, customFolder: _filterCustomFolder)\n        .then((value) {\n      setState(() {\n        _selecting = false;\n        _selectingList = [];\n      });\n      return value;\n    });\n    method.allCustomFolders().then((value) {\n      setState(() {\n        _folderList = value.where((e) => e.isNotEmpty).toList();\n      });\n    });\n  }\n\n  Widget _selectingCancelButton(Color appBarIconColor) {\n    return IconButton(\n      onPressed: () {\n        setState(() {\n          _selecting = false;\n          _selectingList = [];\n        });\n      },\n      icon: Icon(Icons.cancel, color: appBarIconColor),\n    );\n  }\n\n  Widget _selectingMoveButton(Color appBarIconColor) {\n    return IconButton(\n      onPressed: () async {\n        var tmp = _selectingList;\n        _selecting = false;\n        _selectingList = [];\n        setState(() {});\n        if (tmp.isEmpty) {\n          defaultToast(\n              context, tr('screen.download_list.select_download_to_move'));\n        } else {\n          var moveToChoose = await chooseListDialog(\n            context,\n            tr('screen.download_list.move_download'),\n            [\n              tr('app.all'),\n              ..._folderList,\n              tr('screen.download_list.input_name')\n            ],\n            tips: tr('screen.download_list.empty_folder_will_be_deleted'),\n          );\n          if (moveToChoose == null) {\n            return;\n          }\n          if (moveToChoose == tr('screen.download_list.input_name')) {\n            String? name = await displayTextInputDialog(context,\n                title: tr('screen.download_list.folder_name'),\n                hint: tr('screen.download_list.please_input_folder_name'));\n            if (name != null) {\n              if (tr('app.all') != name &&\n                  tr('screen.download_list.input_name') != name) {\n                await method.moveDownloadComic(tmp, name);\n                _reloadList();\n                setState(() {});\n              }\n            }\n          } else if (moveToChoose == tr('app.all')) {\n            await method.moveDownloadComic(tmp, \"\");\n            _reloadList();\n            setState(() {});\n          } else {\n            await method.moveDownloadComic(tmp, moveToChoose);\n            _reloadList();\n            setState(() {});\n          }\n        }\n      },\n      icon: Icon(Icons.move_down, color: appBarIconColor),\n    );\n  }\n\n  Widget _selectingDeleteButton(Color appBarIconColor) {\n    return IconButton(\n      onPressed: () async {\n        var tmp = _selectingList;\n        _selecting = false;\n        _selectingList = [];\n        setState(() {});\n        if (tmp.isEmpty) {\n          defaultToast(\n              context, tr('screen.download_list.select_download_to_delete'));\n        } else {\n          if (await confirmDialog(\n              context,\n              tr('screen.download_list.delete_download'),\n              tr('screen.download_list.delete_selected_download'))) {\n            for (var id in tmp) {\n              await method.deleteDownloadComic(id);\n            }\n            for (var i = 0; i < _data.length; i++) {\n              if (tmp.contains(_data[i].id)) {\n                _data[i].deleting = true;\n              }\n            }\n            _selecting = false;\n            _selectingList = [];\n            //_reloadList();\n            setState(() {});\n          }\n        }\n      },\n      icon: Icon(Icons.delete, color: appBarIconColor),\n    );\n  }\n\n  Widget _toSelectingButton(Color appBarIconColor) {\n    return IconButton(\n      onPressed: () {\n        setState(() {\n          _selecting = true;\n          _selectingList = [];\n        });\n      },\n      icon: Column(\n        children: [\n          Expanded(child: Container()),\n          Icon(\n            Icons.rule,\n            size: 18,\n            color: appBarIconColor,\n          ),\n          Text(\n            tr('screen.download_list.multi_select'),\n            style: TextStyle(fontSize: 14, color: appBarIconColor),\n          ),\n          Expanded(child: Container()),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadOnlyImportScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:pikapika/basic/Common.dart';\n\nimport '../basic/Channels.dart';\nimport '../basic/Method.dart';\nimport 'components/ContentLoading.dart';\n\nclass DownloadOnlyImportScreen extends StatefulWidget {\n  final bool holdPkz;\n  final String path;\n\n  const DownloadOnlyImportScreen({\n    Key? key,\n    required this.path,\n    this.holdPkz = false,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadOnlyImportScreenState();\n}\n\nclass _DownloadOnlyImportScreenState extends State<DownloadOnlyImportScreen> {\n  bool importing = false;\n  bool imported = false;\n  bool importFail = false;\n  dynamic e;\n  String importMessage = tr('screen.download_import.importing');\n  StreamSubscription<String?>? _linkSubscription;\n\n  @override\n  void initState() {\n    if (widget.holdPkz) {\n      _linkSubscription = linkSubscript(context);\n    }\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _linkSubscription?.cancel();\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    setState(() {\n      importMessage = event;\n    });\n  }\n\n  Widget _body() {\n    if (importing) {\n      return ContentLoading(label: importMessage);\n    }\n    if (importFail) {\n      return Center(child: Text(tr('screen.download_import.import_failed') + \"\\n$e\"));\n    }\n    if (imported) {\n      return Center(child: Text(tr('screen.download_import.import_success')));\n    }\n    return Center(\n      child: MaterialButton(\n        onPressed: _import,\n        child: Text(tr('screen.download_import.click_import_file') + \"\\n${p.basename(widget.path)}\"),\n      ),\n    );\n  }\n\n  _import() async {\n    try {\n      setState(() {\n        importing = true;\n      });\n      if (widget.path.endsWith(\".zip\")) {\n        await method.importComicDownload(widget.path);\n      } else if (widget.path.endsWith(\".pki\")) {\n        await method.importComicDownloadPki(widget.path);\n      }\n      imported = true;\n    } catch (err) {\n      e = err;\n      importFail = true;\n    } finally {\n      setState(() {\n        importing = false;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return WillPopScope(\n      child: Scaffold(\n        appBar: AppBar(\n          title: Text(tr('screen.download_import.import')),\n        ),\n        body: _body(),\n      ),\n      onWillPop: () async {\n        if (importing) {\n          defaultToast(context, tr('screen.download_import.importing_please_wait'));\n          return false;\n        }\n        return true;\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/DownloadReaderScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/FullScreenUI.dart';\nimport 'package:pikapika/basic/config/ReaderDirection.dart';\nimport 'package:pikapika/basic/config/ReaderType.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ImageReader.dart';\nimport 'components/RightClickPop.dart';\n\n// 阅读下载的内容\nclass DownloadReaderScreen extends StatefulWidget {\n  final DownloadComic comicInfo;\n  final List<DownloadEp> epList;\n  final int currentEpOrder;\n  final int? initPicturePosition;\n  final ReaderType pagerType = currentReaderType();\n  final ReaderDirection pagerDirection = gReaderDirection;\n  late final bool autoFullScreen;\n\n  DownloadReaderScreen({\n    Key? key,\n    required this.comicInfo,\n    required this.epList,\n    required this.currentEpOrder,\n    this.initPicturePosition,\n    bool? autoFullScreen,\n  }) : super(key: key) {\n    this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen();\n  }\n\n  @override\n  State<StatefulWidget> createState() => _DownloadReaderScreenState();\n}\n\nclass _DownloadReaderScreenState extends State<DownloadReaderScreen> {\n  late DownloadEp _ep;\n  late bool _fullScreen = false;\n  late List<DownloadPicture> pictures = [];\n  late Future _future = _load();\n  int? _lastChangeRank;\n  bool _replacement = false;\n\n  Future _load() async {\n    if (widget.initPicturePosition == null) {\n      await method.storeViewEp(widget.comicInfo.id, _ep.epOrder, _ep.title, 0);\n    }\n    pictures.clear();\n    for (var ep in widget.epList) {\n      if (ep.epOrder == widget.currentEpOrder) {\n        pictures.addAll((await method.downloadPicturesByEpId(ep.id)));\n      }\n    }\n    if (widget.autoFullScreen) {\n      setState(() {\n        SystemChrome.setEnabledSystemUIMode(\n          SystemUiMode.manual,\n          overlays: [],\n        );\n        _fullScreen = true;\n      });\n    }\n  }\n\n  Future _onPositionChange(int position) async {\n    _lastChangeRank = position;\n    return method.storeViewEp(\n        widget.comicInfo.id, _ep.epOrder, _ep.title, position);\n  }\n\n  FutureOr<dynamic> _onDownload() async {\n    defaultToast(context, \"您阅读的是下载漫画\");\n  }\n\n  FutureOr<dynamic> _onChangeEp(int epOrder) {\n    var orderMap = <int, DownloadEp>{};\n    for (var element in widget.epList) {\n      orderMap[element.epOrder] = element;\n    }\n    if (orderMap.containsKey(epOrder)) {\n      _replacement = true;\n      Navigator.of(context).pushReplacement(\n        mixRoute(\n          builder: (context) => DownloadReaderScreen(\n            comicInfo: widget.comicInfo,\n            epList: widget.epList,\n            currentEpOrder: epOrder,\n            autoFullScreen: _fullScreen,\n          ),\n        ),\n      );\n    }\n  }\n\n  FutureOr<dynamic> _onReloadEp() {\n    _replacement = true;\n    Navigator.of(context).pushReplacement(\n      mixRoute(\n        builder: (context) => DownloadReaderScreen(\n          comicInfo: widget.comicInfo,\n          epList: widget.epList,\n          currentEpOrder: widget.currentEpOrder,\n          initPicturePosition: _lastChangeRank ?? widget.initPicturePosition,\n          // maybe null\n          autoFullScreen: _fullScreen,\n        ),\n      ),\n    );\n  }\n\n  @override\n  void initState() {\n    // EP\n    for (var element in widget.epList) {\n      if (element.epOrder == widget.currentEpOrder) {\n        _ep = element;\n      }\n    }\n    // INIT\n    _future = _load();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (!_replacement) {\n      switchFullScreenUI();\n    }\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return readerKeyboardHolder(_build(context));\n  }\n\n  Widget _build(BuildContext context) {\n    return FutureBuilder(\n      future: _future,\n      builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: ContentError(\n              error: snapshot.error,\n              stackTrace: snapshot.stackTrace,\n              onRefresh: () async {\n                setState(() {\n                  _future = _load();\n                });\n              },\n            ),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: const ContentLoading(label: '加载中'),\n          );\n        }\n        var epNameMap = <int, String>{};\n        for (var element in widget.epList) {\n          epNameMap[element.epOrder] = element.title;\n        }\n        return Scaffold(\n          body: ImageReader(\n            ImageReaderStruct(\n              images: pictures\n                  .map((e) => ReaderImageInfo(e.fileServer, e.path, e.localPath,\n                      e.width, e.height, e.format, e.fileSize))\n                  .toList(),\n              fullScreen: _fullScreen,\n              onFullScreenChange: _onFullScreenChange,\n              onPositionChange: _onPositionChange,\n              initPosition: widget.initPicturePosition,\n              epOrder: _ep.epOrder,\n              epNameMap: epNameMap,\n              comicTitle: widget.comicInfo.title,\n              onReloadEp: _onReloadEp,\n              onChangeEp: _onChangeEp,\n              onDownload: _onDownload,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Future _onFullScreenChange(bool fullScreen) async {\n    setState(() {\n      if (fullScreen) {\n        if (Platform.isAndroid || Platform.isIOS) {\n          SystemChrome.setEnabledSystemUIMode(\n            SystemUiMode.manual,\n            overlays: [],\n          );\n        }\n      } else {\n        switchFullScreenUI();\n      }\n      _fullScreen = fullScreen;\n    });\n  }\n}\n"
  },
  {
    "path": "lib/screens/FavouritePaperScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../basic/Entities.dart';\nimport 'components/ComicPager.dart';\nimport 'components/RightClickPop.dart';\n\n// 收藏的漫画\nclass FavouritePaperScreen extends StatefulWidget {\n  const FavouritePaperScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _FavouritePaperScreen();\n}\n\nclass _FavouritePaperScreen extends State<FavouritePaperScreen> {\n  Future<ComicsPage> _fetch(String _currentSort, int _currentPage) {\n    return method.favouriteComics(_currentSort, _currentPage);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.favourite_paper.favourite')),\n      ),\n      body: ComicPager(\n        fetchPage: _fetch,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/FilePhotoViewScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:photo_view/photo_view.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/screens/components/Images.dart';\n\nimport 'components/RightClickPop.dart';\n\n// 预览图片\nclass FilePhotoViewScreen extends StatelessWidget {\n  final String filePath;\n\n  const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) => Scaffold(\n        body: Stack(\n          children: [\n            GestureDetector(\n              onLongPress: () async {\n                String? choose = await chooseListDialog(\n                    context, tr('app.please_select'), [tr('app.save_image')]);\n                if (choose == null) {\n                  return;\n                }\n                if (choose == tr('app.save_image')) {\n                  saveImage(filePath, context);\n                }\n              },\n              child: PhotoView(\n                imageProvider: ResourceFileImageProvider(filePath),\n              ),\n            ),\n            InkWell(\n              onTap: () => Navigator.of(context).pop(),\n              child: Container(\n                margin: const EdgeInsets.only(top: 30),\n                padding: const EdgeInsets.only(left: 4, right: 4),\n                decoration: BoxDecoration(\n                  color: Colors.black.withOpacity(.75),\n                  borderRadius: const BorderRadius.only(\n                    topRight: Radius.circular(8),\n                    bottomRight: Radius.circular(8),\n                  ),\n                ),\n                child:\n                    const Icon(Icons.keyboard_backspace, color: Colors.white),\n              ),\n            ),\n          ],\n        ),\n      );\n}\n"
  },
  {
    "path": "lib/screens/ForgotPasswordScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../basic/Common.dart';\nimport '../basic/Cross.dart';\nimport '../basic/Method.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\n\nclass ForgotPasswordScreen extends StatefulWidget {\n  const ForgotPasswordScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ForgotPasswordScreenState();\n}\n\nclass _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {\n  bool _loading = false;\n  int _state = 0; // 0 输入账号，1 回答问题，2 密码已经找回\n  String _email = \"\";\n  String _question1 = \"\";\n  String _question2 = \"\";\n  String _question3 = \"\";\n  String _answer1 = \"\";\n  String _answer2 = \"\";\n  String _answer3 = \"\";\n  String _password = \"\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.forgot_password.title\")),\n      ),\n      body: _stateScreen(),\n    );\n  }\n\n  Widget _stateScreen() {\n    if (_loading) {\n      return ContentLoading(label: tr('app.loading'));\n    }\n    switch (_state) {\n      case 0:\n        return _inputEmailScreen();\n      case 1:\n        return _inputAnswerScreen();\n      case 2:\n        return _showNewPasswordScreen();\n    }\n    throw '';\n  }\n\n  Widget _inputEmailScreen() {\n    return PikaListView(children: [\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.username\")),\n        subtitle: Text(_email == \"\" ? tr(\"screen.forgot_password.not_set\") : _email),\n        onTap: () async {\n          String? input = await displayTextInputDialog(\n            context,\n            src: _email,\n            title: tr('screen.forgot_password.username'),\n            hint: tr('screen.forgot_password.please_enter_username'),\n          );\n          if (input != null) {\n            setState(() {\n              _email = input;\n            });\n          }\n        },\n      ),\n      Container(\n        margin: const EdgeInsets.all(10),\n        color: Colors.grey.shade500.withAlpha(18),\n        child: MaterialButton(\n          onPressed: _confirmEmail,\n          child: Text(tr(\"screen.forgot_password.confirm\")),\n        ),\n      ),\n    ]);\n  }\n\n  void _confirmEmail() async {\n    if (_email.isEmpty) {\n      defaultToast(context, tr(\"screen.forgot_password.please_enter_username\"));\n      return;\n    }\n    try {\n      setState(() {\n        _loading = true;\n      });\n      var result = await method.forgotPassword(_email);\n      _question1 = result.question1;\n      _question2 = result.question2;\n      _question3 = result.question3;\n      _state = 1;\n    } catch (e, s) {\n      print(\"$e\\n$s\");\n      defaultToast(context, '$e');\n    } finally {\n      setState(() {\n        _loading = false;\n      });\n    }\n  }\n\n  Widget _inputAnswerScreen() {\n    return ListView(children: [\n      Container(height: 10),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.username\")),\n        subtitle: Text(_email.isEmpty ? tr(\"screen.forgot_password.not_set\") : _email),\n      ),\n      Container(height: 10),\n      const Divider(),\n      Container(height: 10),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.question_1\")),\n        subtitle: Text(_question1),\n      ),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.answer_1\")),\n        subtitle: Text(_answer1.isEmpty ? tr(\"screen.forgot_password.not_set\") : _answer1),\n        onTap: () async {\n          String? input = await displayTextInputDialog(\n            context,\n            src: _answer1,\n            title: tr('screen.forgot_password.answer_1'),\n            hint: tr('screen.forgot_password.please_enter_answer_1'),\n          );\n          if (input != null) {\n            setState(() {\n              _answer1 = input;\n            });\n          }\n        },\n      ),\n      Container(\n        margin: const EdgeInsets.all(10),\n        color: Colors.grey.shade500.withAlpha(18),\n        child: MaterialButton(\n          onPressed: () {\n            _confirmAnswer(1, _answer1);\n          },\n          child: Text(tr(\"screen.forgot_password.use_answer_1_recover\")),\n        ),\n      ),\n      Container(height: 10),\n      const Divider(),\n      Container(height: 10),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.question_2\")),\n        subtitle: Text(_question2),\n      ),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.answer_2\")),\n        subtitle: Text(_answer2.isEmpty ? tr(\"screen.forgot_password.not_set\") : _answer2),\n        onTap: () async {\n          String? input = await displayTextInputDialog(\n            context,\n            src: _answer2,\n            title: tr('screen.forgot_password.answer_2'),\n            hint: tr('screen.forgot_password.please_enter_answer_2'),\n          );\n          if (input != null) {\n            setState(() {\n              _answer2 = input;\n            });\n          }\n        },\n      ),\n      Container(\n        margin: const EdgeInsets.all(10),\n        color: Colors.grey.shade500.withAlpha(18),\n        child: MaterialButton(\n          onPressed: () {\n            _confirmAnswer(2, _answer2);\n          },\n          child: Text(tr(\"screen.forgot_password.use_answer_2_recover\")),\n        ),\n      ),\n      Container(height: 10),\n      const Divider(),\n      Container(height: 10),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.question_3\")),\n        subtitle: Text(_question3),\n      ),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.answer_3\")),\n        subtitle: Text(_answer3.isEmpty ? tr(\"screen.forgot_password.not_set\") : _answer3),\n        onTap: () async {\n          String? input = await displayTextInputDialog(\n            context,\n            src: _answer3,\n            title: tr('screen.forgot_password.answer_3'),\n            hint: tr('screen.forgot_password.please_enter_answer_3'),\n          );\n          if (input != null) {\n            setState(() {\n              _answer3 = input;\n            });\n          }\n        },\n      ),\n      Container(\n        margin: const EdgeInsets.all(10),\n        color: Colors.grey.shade500.withAlpha(18),\n        child: MaterialButton(\n          onPressed: () {\n            _confirmAnswer(3, _answer3);\n          },\n          child: Text(tr(\"screen.forgot_password.use_answer_3_recover\")),\n        ),\n      ),\n      /////////\n      Container(height: 20),\n    ]);\n  }\n\n  _confirmAnswer(int answerNo, String answer) async {\n    if (answer.isEmpty) {\n      defaultToast(context, tr(\"screen.forgot_password.please_enter_answer\"));\n      return;\n    }\n    try {\n      setState(() {\n        _loading = true;\n      });\n      var result = await method.resetPassword(_email, answerNo, answer);\n      _password = result.password;\n      _state = 2;\n      defaultToast(context, tr(\"screen.forgot_password.new_password_copied\"));\n      copyToClipBoard(context, _password);\n    } catch (e, s) {\n      print(\"$e\\n$s\");\n      if (\"$e\".contains(\"invalid request\")) {\n        defaultToast(context, tr('screen.forgot_password.answer_incorrect'));\n      } else {\n        defaultToast(context, '$e');\n      }\n    } finally {\n      setState(() {\n        _loading = false;\n      });\n    }\n  }\n\n  Widget _showNewPasswordScreen() {\n    return ListView(children: [\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.username\")),\n        subtitle: Text(_email.isEmpty ? tr(\"screen.forgot_password.not_set\") : _email),\n      ),\n      ListTile(\n        title: Text(tr(\"screen.forgot_password.password\")),\n        subtitle: Text(_password.isEmpty ? tr(\"screen.forgot_password.not_set\") : _password),\n        onTap: () {\n          defaultToast(context, tr(\"screen.forgot_password.new_password_copied\"));\n          copyToClipBoard(context, _password);\n        },\n      ),\n    ]);\n  }\n}\n"
  },
  {
    "path": "lib/screens/GameDownloadScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ItemBuilder.dart';\n\nimport 'components/GameTitleCard.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 游戏下载地址列表页\nclass GameDownloadScreen extends StatefulWidget {\n  final GameInfo info;\n\n  const GameDownloadScreen(this.info, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _GameDownloadScreenState();\n}\n\nclass _GameDownloadScreenState extends State<GameDownloadScreen> {\n  late Future<List<String>> _future =\n      method.downloadGame(widget.info.androidLinks[0]);\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(\"${tr('screen.game_download.title')} - ${widget.info.title}\"),\n      ),\n      body: PikaListView(\n        children: [\n          GameTitleCard(widget.info),\n          ItemBuilder(\n            future: _future,\n            onRefresh: () async  {\n              setState(() {\n                _future =\n                    method.downloadGame(widget.info.androidLinks[0]);\n              });\n            },\n            successBuilder:\n                (BuildContext context, AsyncSnapshot<List<String>> snapshot) {\n              return Column(\n                children: [\n                  Container(\n                    padding: const EdgeInsets.all(30),\n                    child: Text(tr('screen.game_download.download_links_obtained')),\n                  ),\n                  ...snapshot.data!.map((e) => _copyCard(e)),\n                ],\n              );\n            },\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _copyCard(String string) {\n    return InkWell(\n      onTap: () {\n        copyToClipBoard(context, string);\n      },\n      child: Row(\n        children: [\n          Expanded(\n            child: Container(\n              margin: const EdgeInsets.all(10),\n              padding: const EdgeInsets.all(10),\n              decoration: BoxDecoration(\n                border: Border.all(\n                  color: Colors.grey.shade500,\n                  width: .5,\n                  style: BorderStyle.solid,\n                ),\n              ),\n              child: Text(string),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/GameInfoScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/CommentMainType.dart';\nimport 'package:pikapika/screens/components/ContentError.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\nimport 'package:pikapika/screens/components/Images.dart';\n\nimport '../basic/config/IconLoading.dart';\nimport 'GameDownloadScreen.dart';\nimport 'components/CommentList.dart';\nimport 'components/GameTitleCard.dart';\nimport 'components/RightClickPop.dart';\n\n// 游戏详情\nclass GameInfoScreen extends StatefulWidget {\n  final String gameId;\n\n  const GameInfoScreen(this.gameId, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _GameInfoScreenState();\n}\n\nclass _GameInfoScreenState extends State<GameInfoScreen> {\n  late var _future = method.game(widget.gameId);\n  late var _key = UniqueKey();\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return FutureBuilder(\n      key: _key,\n      future: _future,\n      builder: (BuildContext context, AsyncSnapshot<GameInfo> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr('app.error')),\n            ),\n            body: ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  setState(() {\n                    _future = method.game(widget.gameId);\n                    _key = UniqueKey();\n                  });\n                }),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: AppBar(\n              title: Text(tr('app.loading')),\n            ),\n            body: ContentLoading(label: tr('app.loading')),\n          );\n        }\n\n        BorderRadius iconRadius = const BorderRadius.all(Radius.circular(6));\n        double screenShootMargin = 10;\n        double screenShootHeight = 200;\n        TextStyle descriptionStyle = const TextStyle();\n\n        return LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints constraints) {\n            var info = snapshot.data!;\n            return DefaultTabController(\n              length: 2,\n              child: Scaffold(\n                appBar: AppBar(\n                  title: Text(info.title),\n                ),\n                body: ListView(\n                  children: [\n                    GameTitleCard(info),\n                    Container(\n                      padding: const EdgeInsets.only(\n                        left: 20,\n                        right: 20,\n                        top: 5,\n                        bottom: 10,\n                      ),\n                      child: ClipRRect(\n                        borderRadius:\n                            const BorderRadius.all(Radius.circular(5)),\n                        child: MaterialButton(\n                          color: Theme.of(context).colorScheme.secondary,\n                          textColor: Colors.white,\n                          onPressed: () {\n                            Navigator.push(\n                              context,\n                              mixRoute(\n                                builder: (context) => GameDownloadScreen(info),\n                              ),\n                            );\n                          },\n                          child: Container(\n                            padding: const EdgeInsets.all(5),\n                            child: Text(tr('screen.game_info.download')),\n                          ),\n                        ),\n                      ),\n                    ),\n                    Container(\n                      margin: EdgeInsets.only(\n                        top: screenShootMargin,\n                        bottom: screenShootMargin,\n                      ),\n                      height: screenShootHeight,\n                      child: ListView(\n                        padding: EdgeInsets.only(\n                          left: screenShootMargin,\n                          right: screenShootMargin,\n                        ),\n                        scrollDirection: Axis.horizontal,\n                        children: info.screenshots\n                            .map((e) => Container(\n                                  margin: EdgeInsets.only(\n                                    left: screenShootMargin,\n                                    right: screenShootMargin,\n                                  ),\n                                  child: ClipRRect(\n                                    borderRadius: iconRadius,\n                                    child: RemoteImage(\n                                      height: screenShootHeight,\n                                      fileServer: e.fileServer,\n                                      path: e.path,\n                                    ),\n                                  ),\n                                ))\n                            .toList(),\n                      ),\n                    ),\n                    Container(height: 20),\n                    Column(\n                      children: [\n                        Container(\n                          height: 40,\n                          color: Theme.of(context)\n                              .colorScheme\n                              .secondary\n                              .withOpacity(.025),\n                          child: TabBar(\n                            tabs: <Widget>[\n                              Tab(text: tr('screen.game_info.details')),\n                              Tab(text: '${tr('screen.game_info.comments')} (${info.commentsCount})'),\n                            ],\n                            indicatorColor:\n                                Theme.of(context).colorScheme.secondary,\n                            labelColor: Theme.of(context).colorScheme.secondary,\n                            onTap: (val) async {\n                              setState(() {\n                                _tabIndex = val;\n                              });\n                            },\n                          ),\n                        ),\n                      ],\n                    ),\n                    _tabIndex == 0\n                        ? Container(\n                            padding: const EdgeInsets.all(20),\n                            child:\n                                Text(info.description, style: descriptionStyle),\n                          )\n                        : CommentList(CommentMainType.GAME, info.id),\n                  ],\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n\n  var _tabIndex = 0;\n}\n"
  },
  {
    "path": "lib/screens/GamesScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\n\nimport '../basic/config/IconLoading.dart';\nimport 'GameInfoScreen.dart';\nimport 'components/Images.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 游戏列表\nclass GamesScreen extends StatefulWidget {\n  const GamesScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _GamesScreenState();\n}\n\nclass _GamesScreenState extends State<GamesScreen> {\n  int _currentPage = 1;\n  late Future<GamePage> _future = _loadPage();\n  late Key _key = UniqueKey();\n\n  Future<GamePage> _loadPage() {\n    return method.games(_currentPage);\n  }\n\n  void _onPageChange(int number) {\n    setState(() {\n      _currentPage = number;\n      _future = _loadPage();\n      _key = UniqueKey();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.games.title')),\n      ),\n      body: ContentBuilder(\n        key: _key,\n        future: _future,\n        onRefresh: _loadPage,\n        successBuilder:\n            (BuildContext context, AsyncSnapshot<GamePage> snapshot) {\n          var page = snapshot.data!;\n\n          List<Wrap> wraps = [];\n          GameCard? gameCard;\n          for (var element in page.docs) {\n            if (gameCard == null) {\n              gameCard = GameCard(element);\n            } else {\n              wraps.add(Wrap(\n                children: [GameCard(element), gameCard],\n                alignment: WrapAlignment.center,\n              ));\n              gameCard = null;\n            }\n          }\n          if (gameCard != null) {\n            wraps.add(Wrap(\n              children: [gameCard],\n              alignment: WrapAlignment.center,\n            ));\n          }\n          return Scaffold(\n            appBar: PreferredSize(\n              preferredSize: const Size.fromHeight(40),\n              child: Container(\n                padding: const EdgeInsets.only(left: 10, right: 10),\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: BorderSide(\n                      width: .5,\n                      style: BorderStyle.solid,\n                      color: Colors.grey[200]!,\n                    ),\n                  ),\n                ),\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.spaceBetween,\n                                        children: [\n                        InkWell(\n                          onTap: () {\n                            _textEditController.clear();\n                            showDialog(\n                              context: context,\n                              builder: (context) {\n                                return AlertDialog(\n                                  content: Card(\n                                    child: TextField(\n                                      controller: _textEditController,\n                                      decoration: InputDecoration(\n                                        labelText: tr(\"app.please_enter_page_number\"),\n                                      ),\n                                  keyboardType: TextInputType.number,\n                                  inputFormatters: <TextInputFormatter>[\n                                    FilteringTextInputFormatter.allow(\n                                        RegExp(r'\\d+')),\n                                  ],\n                                ),\n                              ),\n                                                                actions: <Widget>[\n                                    MaterialButton(\n                                      onPressed: () {\n                                        Navigator.pop(context);\n                                      },\n                                      child: Text(tr('app.cancel')),\n                                    ),\n                                    MaterialButton(\n                                      onPressed: () {\n                                        Navigator.pop(context);\n                                        var text = _textEditController.text;\n                                        if (text.isEmpty || text.length > 5) {\n                                          return;\n                                        }\n                                        var num = int.parse(text);\n                                        if (num == 0 || num > page.pages) {\n                                          return;\n                                        }\n                                        _onPageChange(num);\n                                      },\n                                      child: Text(tr('app.confirm')),\n                                    ),\n                                  ],\n                            );\n                          },\n                        );\n                      },\n                                                child: Row(\n                            children: [\n                              Text(\"${tr('app.page')} ${page.page} / ${page.pages}\"),\n                            ],\n                          ),\n                    ),\n                    Row(\n                      children: [\n                        MaterialButton(\n                          minWidth: 0,\n                          onPressed: () {\n                            if (page.page > 1) {\n                              _onPageChange(page.page - 1);\n                            }\n                          },\n                          child: Text(tr('app.previous_page')),\n                        ),\n                        MaterialButton(\n                          minWidth: 0,\n                          onPressed: () {\n                            if (page.page < page.pages) {\n                              _onPageChange(page.page + 1);\n                            }\n                          },\n                          child: Text(tr('app.next_page')),\n                        )\n                      ],\n                    ),\n                  ],\n                ),\n              ),\n            ),\n            body: PikaListView(\n              children: [\n                ...wraps,\n                ...page.page < page.pages\n                    ? [\n                        MaterialButton(\n                          onPressed: () {\n                            _onPageChange(page.page + 1);\n                          },\n                          child: Container(\n                            padding: const EdgeInsets.only(top: 30, bottom: 30),\n                            child: Text(tr('screen.games.next_page')),\n                          ),\n                        ),\n                      ]\n                    : [],\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass GameCard extends StatelessWidget {\n  final GameSimple info;\n\n  const GameCard(this.info, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    var textColor = theme.textTheme.bodyText1!.color!;\n    var categoriesStyle = TextStyle(\n      fontSize: 13,\n      color: textColor.withAlpha(0xCC),\n    );\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        // data.width/data.height = width/ ?\n        //  data.width * ? = width * data.height\n        // ? = width * data.height / data.width\n        var size = MediaQuery.of(context).size;\n        var min = size.width < size.height ? size.width : size.height;\n        var imageWidth = (min - 45 - 40) / 2;\n        var imageHeight = imageWidth * 280 / 500;\n        return Card(\n          child: InkWell(\n            onTap: () {\n              Navigator.push(\n                context,\n                mixRoute(\n                    builder: (context) => GameInfoScreen(info.id)),\n              );\n            },\n            child: Container(\n              padding: const EdgeInsets.all(10),\n              child: SizedBox(\n                width: imageWidth,\n                child: Column(\n                  children: [\n                    RemoteImage(\n                      width: imageWidth,\n                      height: imageHeight,\n                      fileServer: info.icon.fileServer,\n                      path: info.icon.path,\n                    ),\n                    Text(\n                      info.title + '\\n',\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                      style: const TextStyle(height: 1.4),\n                      strutStyle: const StrutStyle(height: 1.4),\n                    ),\n                    Text(\n                      info.publisher,\n                      style: categoriesStyle,\n                      maxLines: 1,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ],\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n\nfinal TextEditingController _textEditController =\n    TextEditingController(text: '');\n"
  },
  {
    "path": "lib/screens/HiddenWordsScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport '../basic/config/HiddenWords.dart';\nimport 'components/RightClickPop.dart';\nimport 'components/ListView.dart';\nimport 'components/ContentBuilder.dart';\n\nclass HiddenWordsScreen extends StatefulWidget {\n  const HiddenWordsScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _HiddenWordsScreenState();\n}\n\nclass _HiddenWordsScreenState extends State<HiddenWordsScreen> {\n  late Future<String> _future = initHiddenWords();\n  late Key _key = UniqueKey();\n  final TextEditingController _textController = TextEditingController();\n\n  Future<void> _addWord() async {\n    if (_textController.text.trim().isEmpty) return;\n    await addHiddenWord(_textController.text.trim());\n    _textController.clear();\n    setState(() {\n      _future = initHiddenWords();\n      _key = UniqueKey();\n    });\n  }\n\n  Future<void> _removeWord(String word) async {\n    await removeHiddenWord(word);\n    setState(() {\n      _future = initHiddenWords();\n      _key = UniqueKey();\n    });\n  }\n\n  Future<void> _clearAll() async {\n    await clearHiddenWords();\n    setState(() {\n      _future = initHiddenWords();\n      _key = UniqueKey();\n    });\n  }\n\n  @override\n  void dispose() {\n    _textController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"settings.hidden_words.title\")),\n        actions: [\n          IconButton(\n            icon: const Icon(Icons.delete_sweep),\n            onPressed: () async {\n              bool? confirm = await showDialog<bool>(\n                context: context,\n                builder: (context) => AlertDialog(\n                  title: Text(tr(\"settings.hidden_words.clear_all\")),\n                  content: Text(tr(\"settings.hidden_words.clear_all_desc\")),\n                  actions: [\n                    TextButton(\n                      onPressed: () => Navigator.of(context).pop(false),\n                      child: Text(tr(\"settings.hidden_words.cancel\")),\n                    ),\n                    TextButton(\n                      onPressed: () => Navigator.of(context).pop(true),\n                      child: Text(tr(\"settings.hidden_words.confirm\")),\n                    ),\n                  ],\n                ),\n              );\n              if (confirm == true) {\n                await _clearAll();\n              }\n            },\n          ),\n        ],\n      ),\n      body: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.all(16.0),\n            child: Row(\n              children: [\n                Expanded(\n                  child: TextField(\n                    controller: _textController,\n                    decoration: InputDecoration(\n                      hintText: tr(\"settings.hidden_words.input_hint\"),\n                      border: const OutlineInputBorder(),\n                    ),\n                    onSubmitted: (_) => _addWord(),\n                  ),\n                ),\n                const SizedBox(width: 8),\n                IconButton(\n                  icon: const Icon(Icons.add),\n                  onPressed: _addWord,\n                ),\n              ],\n            ),\n          ),\n          Expanded(\n            child: ContentBuilder(\n              key: _key,\n              future: _future,\n              onRefresh: () async {\n                setState(() {\n                  _future = initHiddenWords();\n                  _key = UniqueKey();\n                });\n              },\n              successBuilder: (BuildContext context, AsyncSnapshot<String> snapshot) {\n                if (hiddenWords.isEmpty) {\n                  return Center(\n                    child: Text(tr(\"settings.hidden_words.no_words\")),\n                  );\n                }\n                return PikaListView(\n                  children: hiddenWords.map((word) => ListTile(\n                    title: Text(word),\n                    trailing: IconButton(\n                      icon: const Icon(Icons.delete),\n                      onPressed: () => _removeWord(word),\n                    ),\n                  )).toList(),\n                );\n              },\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n} "
  },
  {
    "path": "lib/screens/ImportFromOffScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport '../basic/Channels.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass ImportFromOffScreen extends StatefulWidget {\n  final String dbPath;\n\n  const ImportFromOffScreen({Key? key, required this.dbPath}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ImportFromOffScreenState();\n}\n\nclass _ImportFromOffScreenState extends State<ImportFromOffScreen> {\n  bool _importing = false;\n  String _importMessage = \"\";\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    _process();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    if (event is String) {\n      setState(() {\n        _importMessage = event;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: !_importing,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    if (_importing) {\n      return Scaffold(\n        body: ContentLoading(label: _importMessage),\n      );\n    }\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.import_from_off.title')),\n      ),\n      body: PikaListView(\n        children: [\n          Container(\n            padding: const EdgeInsets.all(10),\n            child: Text(_importMessage),\n          ),\n        ],\n      ),\n    );\n  }\n\n  _process() async {\n    try {\n      setState(() {\n        _importing = true;\n      });\n      await method.importComicViewFormOff(widget.dbPath);\n      setState(() {\n        _importMessage = tr(\"screen.import_from_off.import_success\");\n      });\n    } catch (e) {\n      setState(() {\n        _importMessage = \"${tr('screen.import_from_off.import_failed')} $e\";\n      });\n    } finally {\n      setState(() {\n        _importing = false;\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/InitScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:pikapika/basic/config/Address.dart';\nimport 'package:pikapika/basic/config/AndroidDisplayMode.dart';\nimport 'package:pikapika/basic/config/AndroidSecureFlag.dart';\nimport 'package:pikapika/basic/config/AppOrientation.dart';\nimport 'package:pikapika/basic/config/Authentication.dart';\nimport 'package:pikapika/basic/config/AutoClean.dart';\nimport 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart';\nimport 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart';\nimport 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/AutoFullScreenOnForward.dart';\nimport 'package:pikapika/basic/config/CategoriesColumnCount.dart';\nimport 'package:pikapika/basic/config/ChooserRoot.dart';\nimport 'package:pikapika/basic/config/ContentFailedReloadAction.dart';\nimport 'package:pikapika/basic/config/CopySkipConfirm.dart';\nimport 'package:pikapika/basic/config/DragRegionLock.dart';\nimport 'package:pikapika/basic/config/GestureSpeed.dart';\nimport 'package:pikapika/basic/config/DownloadAndExportPath.dart';\nimport 'package:pikapika/basic/config/DownloadThreadCount.dart';\nimport 'package:pikapika/basic/config/EBookScrolling.dart';\nimport 'package:pikapika/basic/config/EBookScrollingRange.dart';\nimport 'package:pikapika/basic/config/EBookScrollingTrigger.dart';\nimport 'package:pikapika/basic/config/FullScreenAction.dart';\nimport 'package:pikapika/basic/config/FullScreenUI.dart';\nimport 'package:pikapika/basic/config/HiddenSearchPersion.dart';\nimport 'package:pikapika/basic/config/HiddenSubIcon.dart';\nimport 'package:pikapika/basic/config/IgnoreInfoHistory.dart';\nimport 'package:pikapika/basic/config/ImageAddress.dart';\nimport 'package:pikapika/basic/config/ImageFilter.dart';\nimport 'package:pikapika/basic/config/KeyboardController.dart';\nimport 'package:pikapika/basic/config/LocalHistorySync.dart';\nimport 'package:pikapika/basic/config/NoAnimation.dart';\nimport 'package:pikapika/basic/config/PagerAction.dart';\nimport 'package:pikapika/basic/config/Platform.dart';\nimport 'package:pikapika/basic/config/Proxy.dart';\nimport 'package:pikapika/basic/config/Quality.dart';\nimport 'package:pikapika/basic/config/ReaderDirection.dart';\nimport 'package:pikapika/basic/config/ReaderSliderPosition.dart';\nimport 'package:pikapika/basic/config/ReaderType.dart';\nimport 'package:pikapika/basic/config/RecommendLinks.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/config/ShowCommentAtDownload.dart';\nimport 'package:pikapika/basic/config/Themes.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/ListLayout.dart';\nimport 'package:pikapika/basic/config/TimeOffsetHour.dart';\nimport 'package:pikapika/basic/config/TimeoutLock.dart';\nimport 'package:pikapika/basic/config/UseApiLoadImage.dart';\nimport 'package:pikapika/basic/config/UsingRightClickPop.dart';\nimport 'package:pikapika/basic/config/Version.dart';\nimport 'package:pikapika/basic/config/VolumeController.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesMode.dart';\nimport 'package:pikapika/basic/config/WillPopNotice.dart';\nimport 'package:pikapika/basic/config/passed.dart';\nimport 'package:pikapika/screens/AccessKeyReplaceScreen.dart';\nimport 'package:pikapika/screens/ComicInfoScreen.dart';\nimport 'package:pikapika/screens/PkzArchiveScreen.dart';\nimport 'package:pikapika/screens/calculator_screen.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\nimport 'package:uni_links/uni_links.dart';\nimport 'package:uri_to_file/uri_to_file.dart';\nimport '../basic/config/CategoriesSort.dart';\nimport '../basic/config/CopyFullName.dart';\nimport '../basic/config/CopyFullNameTemplate.dart';\nimport '../basic/config/DownloadCachePath.dart';\nimport '../basic/config/ExportPath.dart';\nimport '../basic/config/ExportRename.dart';\nimport '../basic/config/HiddenFdIcon.dart';\nimport '../basic/config/HiddenViewed.dart';\nimport '../basic/config/HiddenWords.dart';\nimport '../basic/config/HideOnlineFavorite.dart';\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/IgnoreUpgradeConfirm.dart';\nimport '../basic/config/IsPro.dart';\nimport '../basic/config/ReaderBackgroundColor.dart';\nimport '../basic/config/ReaderScrollByScreenPercentage.dart';\nimport '../basic/config/WebToonScrollMode.dart';\nimport '../basic/config/ReaderZoomScale.dart';\nimport '../basic/config/ReaderTwoPageDirection.dart';\nimport '../basic/config/ThreeKeepRight.dart';\nimport '../basic/config/VolumeNextChapter.dart';\nimport '../basic/config/WebDav.dart';\nimport 'AccountScreen.dart';\nimport 'AppScreen.dart';\nimport 'DownloadOnlyImportScreen.dart';\n\n// 初始化界面\nclass InitScreen extends StatefulWidget {\n  const InitScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _InitScreenState();\n}\n\nclass _InitScreenState extends State<InitScreen> {\n  var _authenticating = false;\n  Widget? _loadPic;\n\n  Widget _defaultLoadingPic() {\n    return const ContentLoading(label: \"加载中\");\n  }\n\n  @override\n  initState() {\n    _init();\n    super.initState();\n  }\n\n  Future<dynamic> _init() async {\n    var dataLocal = await method.dataLocal();\n    print(\"dataLocal: $dataLocal\");\n    if (await File(p.join(dataLocal, \"startup_pic\")).exists()) {\n      _loadPic = Image.file(\n        File(p.join(dataLocal, \"startup_pic\")),\n        fit: BoxFit.contain,\n      );\n    } else {\n      _loadPic = _defaultLoadingPic();\n    }\n    setState(() {});\n    // 初始化配置文件\n    await initPlatform(); // 必须第一个初始化, 加载设备信息\n    await initAutoClean();\n    await initAppOrientation();\n    await initAddress();\n    await initImageAddress();\n    await initProxy();\n    await initQuality();\n    await initFont();\n    await initTheme();\n    await initFullScreenUI();\n    await initListLayout();\n    await initReaderType();\n    await initReaderDirection();\n    await initReaderSliderPosition();\n    await initAutoFullScreen();\n    await initAutoFullScreenOnForward();\n    await initFullScreenAction();\n    await initPagerAction();\n    await initShadowCategoriesMode();\n    await initShadowCategories();\n    await initIconLoading();\n    await initCategoriesColumnCount();\n    await initContentFailedReloadAction();\n    await initVolumeController();\n    await initKeyboardController();\n    await initAndroidDisplayMode();\n    await initChooserRoot();\n    await initExportPath();\n    await initTimeZone();\n    await initDownloadAndExportPath();\n    await initAndroidSecureFlag();\n    await initDownloadThreadCount();\n    await initNoAnimation();\n    await initDragRegionLock();\n    await initGestureSpeed();\n    await initExportRename();\n    await initVersion();\n    await initUsingRightClickPop();\n    await initAuthentication();\n    await reloadIsPro();\n    await initIgnoreUpgradeConfirm();\n    await initWillPopNotice();\n    await initHiddenFdIcon();\n    await initShowCommentAtDownload();\n    await initDownloadCachePath();\n    await initUseApiLoadImage();\n    await initWebDav();\n    await initImageFilter();\n    await initReaderBackgroundColor();\n    await initEBookScrolling();\n    await initEBookScrollingRange();\n    await initEBookScrollingTrigger();\n    await initVolumeNextChapter();\n    await initCopySkipConfirm();\n    await initCopyFullName();\n    await initCategoriesSort();\n    await initLocalHistorySync();\n    await initHideOnlineFavorite();\n    await initHiddenViewed();\n    await initHiddenSubIcon();\n    await initHiddenSearchPersion();\n    await initLockTimeOut();\n    await initReaderTwoPageDirection();\n    await initHiddenWords();\n    await initReaderScrollByScreenPercentage();\n    await initWebToonScrollMode();\n    await initReaderZoomScale();\n    await initIgnoreInfoHistory();\n    await initThreeKeepRight();\n    await initCopyFullNameTemplate();\n    await initAutoDownloadOnFavorite();\n    await initDisableAutoDownloadOnMobile();\n    await initAutoDeleteDownloadOnUnfavorite();\n    await initPassed();\n    await initRecommendLinks();\n    if (!currentPassed()) {\n      Navigator.pushReplacement(context, MaterialPageRoute(\n        builder: (BuildContext context) {\n          return const CalculatorScreen();\n        },\n      ));\n      return;\n    }\n    autoCheckNewVersion();\n    String? initUrl;\n    if (Platform.isAndroid || Platform.isIOS) {\n      try {\n        initUrl = (await getInitialUri())?.toString();\n        // Use the uri and warn the user, if it is not correct,\n        // but keep in mind it could be `null`.\n      } on FormatException {\n        // Handle exception by warning the user their action did not succeed\n        // return?\n      }\n    }\n    if (initUrl != null) {\n      var parsed = Uri.parse(initUrl!);\n      if (RegExp(r\"^pika://access_key/([0-9A-z:\\-]+)/$\")\n          .allMatches(initUrl!)\n          .isNotEmpty) {\n        String accessKey = RegExp(r\"^pika://access_key/([0-9A-z:\\-]+)/$\")\n            .allMatches(initUrl!)\n            .first\n            .group(1)!;\n        Navigator.of(context).pushReplacement(mixRoute(\n          builder: (BuildContext context) =>\n              AccessKeyReplaceScreen(accessKey: accessKey),\n        ));\n        return;\n      } else if (RegExp(r\"^pika://comic/([0-9A-z]+)/$\")\n          .allMatches(initUrl!)\n          .isNotEmpty) {\n        String comicId = RegExp(r\"^pika://comic/([0-9A-z]+)/$\")\n            .allMatches(initUrl!)\n            .first\n            .group(1)!;\n        Navigator.of(context).pushReplacement(mixRoute(\n          builder: (BuildContext context) =>\n              ComicInfoScreen(comicId: comicId, holdPkz: true),\n        ));\n        return;\n      } else if (RegExp(r\"^https?://pika/comic/([0-9A-z]+)/$\")\n          .allMatches(initUrl!)\n          .isNotEmpty) {\n        String comicId = RegExp(r\"^https?://pika/comic/([0-9A-z]+)/$\")\n            .allMatches(initUrl!)\n            .first\n            .group(1)!;\n        Navigator.of(context).pushReplacement(mixRoute(\n          builder: (BuildContext context) =>\n              ComicInfoScreen(comicId: comicId, holdPkz: true),\n        ));\n        return;\n      } else if (RegExp(r\"^.*\\.pkz$\").allMatches(parsed.path).isNotEmpty) {\n        File file = await toFile(initUrl!);\n        Navigator.of(context).pushReplacement(mixRoute(\n          builder: (BuildContext context) =>\n              PkzArchiveScreen(pkzPath: file.path, holdPkz: true),\n        ));\n        return;\n      } else if (RegExp(r\"^.*\\.((pki)|(zip))$\")\n          .allMatches(parsed.path)\n          .isNotEmpty) {\n        File file = await toFile(initUrl!);\n        Navigator.of(context).pushReplacement(\n          mixRoute(\n            builder: (BuildContext context) =>\n                DownloadOnlyImportScreen(path: file.path, holdPkz: true),\n          ),\n        );\n        return;\n      }\n    }\n\n    setState(() {\n      _authenticating = currentAuthentication();\n    });\n    if (_authenticating) {\n      _goAuthentication();\n    } else {\n      syncWebDavIfAuto(context);\n      _goApplication();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_authenticating) {\n      return Scaffold(\n        appBar: AppBar(\n          title: const Text(\"身份验证\"),\n        ),\n        body: Center(\n          child: Container(\n            padding: const EdgeInsets.all(20),\n            child: MaterialButton(\n              onPressed: () {\n                _goAuthentication();\n              },\n              child:\n                  const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'),\n            ),\n          ),\n        ),\n      );\n    }\n    return Scaffold(\n      body: LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return Center(\n            child: ConstrainedBox(\n              constraints: BoxConstraints(\n                maxWidth: constraints.maxWidth * 4 / 5,\n                maxHeight: constraints.maxHeight * 4 / 5,\n              ),\n              child: _loadPic ?? Container(),\n            ),\n          );\n        },\n      ),\n    );\n  }\n\n  Future _goApplication() async {\n    // 登录, 如果token失效重新登录, 网络不好的时候可能需要1分钟\n    if (await method.preLogin()) {\n      // 如果token或username+password有效则直接进入登录好的界面\n      Navigator.pushReplacement(\n        context,\n        mixRoute(builder: (context) => const AppScreen()),\n      );\n    } else {\n      // 否则跳转到登录页\n      Navigator.pushReplacement(\n        context,\n        mixRoute(builder: (context) => const AccountScreen()),\n      );\n    }\n  }\n\n  Future _goAuthentication() async {\n    if (await verifyAuthentication(context)) {\n      _goApplication();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/LocalFavoriteScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'dart:convert';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/IsPro.dart';\nimport 'package:pikapika/screens/ComicInfoScreen.dart';\nimport 'package:pikapika/screens/components/ComicInfoCard.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\nimport 'package:pikapika/screens/components/ListView.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\n\nclass LocalFavoriteScreen extends StatefulWidget {\n  const LocalFavoriteScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _LocalFavoriteScreenState();\n}\n\n// 将ViewLog转换为ComicSimple\nComicSimple _viewLogToComicSimple(ViewLog view) {\n  List<dynamic> categories = [];\n  try {\n    categories = jsonDecode(view.categories);\n  } catch (_) {\n    categories = [];\n  }\n  return ComicSimple.fromJson({\n    \"_id\": view.id,\n    \"title\": view.title,\n    \"author\": view.author,\n    \"pagesCount\": view.pagesCount,\n    \"epsCount\": view.epsCount,\n    \"finished\": view.finished,\n    \"categories\": categories,\n    \"likesCount\": 0,\n    \"thumb\": {\n      \"originalName\": view.thumbOriginalName,\n      \"fileServer\": view.thumbFileServer,\n      \"path\": view.thumbPath,\n    },\n  });\n}\n\nclass _LocalFavoriteScreenState extends State<LocalFavoriteScreen>\n    {\n  List<LocalFavoriteFolder> _folders = [];\n  List<ComicSimple> _comics = [];\n  String _currentFolderId = 'all';\n  bool _loading = true;\n  bool _selecting = false;\n  final Set<String> _selectedComicIds = {};\n\n  @override\n  void initState() {\n    super.initState();\n    _loadFolders();\n  }\n\n  Future<void> _loadFolders() async {\n    setState(() {\n      _loading = true;\n    });\n\n    try {\n      final folders = await method.listLocalFavoriteFolders();\n      if (!mounted) {\n        return;\n      }\n\n      bool currentExists = _currentFolderId == 'all';\n      if (_currentFolderId != 'all') {\n        currentExists = folders.any((f) => f.id == _currentFolderId);\n      }\n\n      setState(() {\n        _folders = folders;\n        if (!currentExists) {\n          _currentFolderId = 'all';\n        }\n      });\n\n      await _loadComics();\n    } catch (e) {\n      print(\"Load folders error: $e\");\n    } finally {\n      if (mounted) {\n        setState(() {\n          _loading = false;\n        });\n      }\n    }\n  }\n\n  String _currentFolderTitle() {\n    if (_currentFolderId == 'all') {\n      return tr('local_favorite.all_folders');\n    }\n    for (final f in _folders) {\n      if (f.id == _currentFolderId) {\n        return f.name;\n      }\n    }\n    return tr('local_favorite.all_folders');\n  }\n\n  Future<void> _loadComics() async {\n    setState(() {\n      _loading = true;\n    });\n\n    try {\n      List<LocalFavoriteComic> localFavorites;\n      if (_currentFolderId == 'all') {\n        localFavorites = await method.listAllLocalFavoriteComics();\n      } else {\n        localFavorites = await method.listLocalFavoriteComics(_currentFolderId);\n      }\n\n      // 获取漫画详情\n      List<ComicSimple> comics = [];\n      for (var fav in localFavorites) {\n        if (fav.info != null && fav.info!.isNotEmpty) {\n          try {\n            comics.add(ComicSimple.fromJson(\n              Map<String, dynamic>.from(jsonDecode(fav.info!)),\n            ));\n            continue;\n          } catch (e) {\n            // Fallback to view log\n          }\n        }\n        try {\n          var view = await method.loadView(fav.comicId);\n          if (view != null) {\n            comics.add(_viewLogToComicSimple(view));\n          }\n        } catch (e) {\n          print(\"Load comic ${fav.comicId} error: $e\");\n        }\n      }\n\n      if (mounted) {\n        setState(() {\n          _comics = comics;\n        });\n      }\n    } catch (e) {\n      print(\"Load comics error: $e\");\n    } finally {\n      if (mounted) {\n        setState(() {\n          _loading = false;\n        });\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: Scaffold(\n        appBar: AppBar(\n          title: Text(_selecting\n              ? \"${tr('local_favorite.title')} · ${tr('local_favorite.select_mode')} (${_selectedComicIds.length})\"\n              : \"${tr('local_favorite.title')} · ${_currentFolderTitle()}\"),\n          actions: [\n            IconButton(\n              onPressed: _openFolderPicker,\n              icon: const Icon(Icons.folder_open),\n              tooltip: tr('local_favorite.select_folder'),\n            ),\n            _buildMenuButton(),\n          ],\n        ),\n        body: _buildBody(),\n      ),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Future<void> _openFolderPicker() async {\n    String? selected = await showDialog<String>(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(tr('local_favorite.select_folder')),\n          content: SizedBox(\n            width: double.maxFinite,\n            child: ListView(\n              shrinkWrap: true,\n              children: [\n                ListTile(\n                  leading: const Icon(Icons.folder_special),\n                  title: Text(tr('local_favorite.all_folders')),\n                  onTap: () => Navigator.of(context).pop('all'),\n                ),\n                const Divider(),\n                ..._folders.map(\n                  (f) => ListTile(\n                    leading: const Icon(Icons.folder),\n                    title: Text(f.name),\n                    onTap: () => Navigator.of(context).pop(f.id),\n                  ),\n                ),\n                const Divider(),\n                ListTile(\n                  leading: const Icon(Icons.create_new_folder),\n                  title: Text(tr('local_favorite.new_folder')),\n                  onTap: () => Navigator.of(context).pop('__CREATE__'),\n                ),\n              ],\n            ),\n          ),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () => Navigator.of(context).pop(),\n              child: Text(tr('app.cancel')),\n            ),\n          ],\n        );\n      },\n    );\n\n    if (!mounted || selected == null) return;\n    if (selected == '__CREATE__') {\n      await _createFolder();\n      return;\n    }\n    if (selected != _currentFolderId) {\n      setState(() {\n        _currentFolderId = selected;\n        _selecting = false;\n        _selectedComicIds.clear();\n      });\n      await _loadComics();\n    }\n  }\n\n  Widget _buildBody() {\n    if (_loading) {\n      return const ContentLoading(label: '加载中');\n    }\n\n    if (_folders.isEmpty && _comics.isEmpty) {\n      return Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Icon(Icons.folder_open, size: 64, color: Colors.grey),\n            const SizedBox(height: 16),\n            Text(\n              tr('local_favorite.no_folders'),\n              style: const TextStyle(color: Colors.grey),\n            ),\n            const SizedBox(height: 24),\n            ElevatedButton.icon(\n              onPressed: _createFolder,\n              icon: const Icon(Icons.create_new_folder),\n              label: Text(tr('local_favorite.new_folder')),\n            ),\n          ],\n        ),\n      );\n    }\n\n    if (_comics.isEmpty) {\n      return Center(\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            const Icon(Icons.book, size: 64, color: Colors.grey),\n            const SizedBox(height: 16),\n            Text(\n              tr('local_favorite.empty_folder'),\n              style: const TextStyle(color: Colors.grey),\n            ),\n          ],\n        ),\n      );\n    }\n\n    return ComicList(\n      _comics,\n    );\n  }\n\n  Widget ComicList(List<ComicSimple> comics) {\n    final entries = comics.map((e) {\n      Widget card = GestureDetector(\n        onTap: () {\n          if (_selecting) {\n            setState(() {\n              if (_selectedComicIds.contains(e.id)) {\n                _selectedComicIds.remove(e.id);\n              } else {\n                _selectedComicIds.add(e.id);\n              }\n            });\n          } else {\n            Navigator.push(\n              context,\n              MaterialPageRoute(\n                builder: (context) => ComicInfoScreen(comicId: e.id),\n              ),\n            );\n          }\n        },\n        child: ComicInfoCard(e, linkItem: true),\n      );\n\n      if (_selecting) {\n        card = Stack(\n          children: [\n            card,\n            Positioned(\n              top: 10,\n              right: 10,\n              child: Icon(\n                _selectedComicIds.contains(e.id)\n                    ? Icons.check_box\n                    : Icons.check_box_outline_blank,\n                color: _selectedComicIds.contains(e.id)\n                    ? Theme.of(context).colorScheme.primary\n                    : null,\n              ),\n            ),\n          ],\n        );\n      }\n\n      return card;\n    });\n\n    return PikaListView(\n      children: entries.toList(),\n    );\n  }\n\n  Widget _buildMenuButton() {\n    return PopupMenuButton<int>(\n      itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n        PopupMenuItem<int>(\n          value: 10,\n          child: ListTile(\n            leading: const Icon(Icons.create_new_folder),\n            title: Text(tr('local_favorite.new_folder')),\n          ),\n        ),\n        if (_currentFolderId != 'all')\n          PopupMenuItem<int>(\n            value: 11,\n            child: ListTile(\n              leading: const Icon(Icons.delete),\n              title: Text(tr('local_favorite.delete_folder')),\n            ),\n          ),\n        if (!_selecting && _comics.isNotEmpty)\n          PopupMenuItem<int>(\n            value: 20,\n            child: ListTile(\n              leading: Icon(\n                Icons.checklist,\n              ),\n              title: Text(\n                tr('local_favorite.select_mode'),\n              ),\n            ),\n          ),\n        if (_selecting)\n          PopupMenuItem<int>(\n            value: 21,\n            child: ListTile(\n              leading: const Icon(Icons.close),\n              title: Text(tr('local_favorite.cancel_select_mode')),\n            ),\n          ),\n        if (_selecting && _comics.isNotEmpty)\n          PopupMenuItem<int>(\n            value: 22,\n            child: ListTile(\n              leading: const Icon(Icons.select_all),\n              title: Text(tr('local_favorite.select_all')),\n            ),\n          ),\n        if (_selecting && _selectedComicIds.isNotEmpty)\n          PopupMenuItem<int>(\n            value: 1,\n            child: ListTile(\n              leading: const Icon(Icons.drive_file_move),\n              title: Text(tr('local_favorite.move_to_folder')),\n            ),\n          ),\n        if (_selecting && _selectedComicIds.isNotEmpty)\n          PopupMenuItem<int>(\n            value: 0,\n            child: ListTile(\n              leading: Icon(\n                Icons.download,\n                color: isPro ? null : Colors.grey,\n              ),\n              title: Text(\n                tr('local_favorite.batch_download') +\n                    (isPro ? \"\" : \" (${tr('app.pro')})\"),\n                style: TextStyle(\n                  color: isPro ? null : Colors.grey,\n                ),\n              ),\n            ),\n          ),\n        if (_selecting && _selectedComicIds.isNotEmpty)\n          PopupMenuItem<int>(\n            value: 23,\n            child: ListTile(\n              leading: const Icon(Icons.delete_outline),\n              title: Text(tr('local_favorite.remove_selected')),\n            ),\n          ),\n        PopupMenuItem<int>(\n          value: 3,\n          child: ListTile(\n            leading: const Icon(Icons.refresh),\n            title: Text(tr('app.refresh')),\n          ),\n        ),\n      ],\n      onSelected: (int value) {\n        switch (value) {\n          case 10:\n            _createFolder();\n            break;\n          case 11:\n            _deleteFolder();\n            break;\n          case 20:\n            setState(() {\n              _selecting = true;\n              _selectedComicIds.clear();\n            });\n            break;\n          case 21:\n            setState(() {\n              _selecting = false;\n              _selectedComicIds.clear();\n            });\n            break;\n          case 22:\n            setState(() {\n              _selectedComicIds.clear();\n              for (final c in _comics) {\n                _selectedComicIds.add(c.id);\n              }\n            });\n            break;\n          case 0:\n            _batchDownload();\n            break;\n          case 1:\n            _moveToFolder();\n            break;\n          case 23:\n            _removeSelected();\n            break;\n          case 3:\n            _loadComics();\n            break;\n        }\n      },\n    );\n  }\n\n  Future<void> _createFolder() async {\n    int folderCount = await method.countLocalFavoriteFolders();\n\n    if (!isPro && folderCount >= 3) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(\n            content: Text(tr('local_favorite.folder_limit_reached')),\n          ),\n        );\n      }\n      return;\n    }\n\n    String? folderName = await showDialog<String>(\n      context: context,\n      builder: (BuildContext context) {\n        final controller = TextEditingController();\n        return AlertDialog(\n          title: Text(tr('local_favorite.new_folder')),\n          content: TextField(\n            controller: controller,\n            decoration: InputDecoration(\n              hintText: tr('local_favorite.folder_name'),\n            ),\n            autofocus: true,\n          ),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n              child: Text(tr('app.cancel')),\n            ),\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop(controller.text.trim());\n              },\n              child: Text(tr('app.confirm')),\n            ),\n          ],\n        );\n      },\n    );\n\n    if (folderName != null && folderName.isNotEmpty) {\n      try {\n        await method.createLocalFavoriteFolder(folderName);\n        await _loadFolders();\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.create_success'))),\n          );\n        }\n      } catch (e) {\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.create_folder_failed'))),\n          );\n        }\n      }\n    }\n  }\n\n  Future<void> _deleteFolder() async {\n    if (_currentFolderId == 'all') return;\n\n    bool? confirm = await showDialog<bool>(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(tr('local_favorite.delete_folder')),\n          content: Text(tr('local_favorite.delete_confirm')),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop(false);\n              },\n              child: Text(tr('app.cancel')),\n            ),\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop(true);\n              },\n              child: Text(tr('app.confirm')),\n            ),\n          ],\n        );\n      },\n    );\n\n    if (confirm == true) {\n      try {\n        await method.deleteLocalFavoriteFolder(_currentFolderId);\n        _currentFolderId = 'all';\n        await _loadFolders();\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.delete_success'))),\n          );\n        }\n      } catch (e) {\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.delete_failed'))),\n          );\n        }\n      }\n    }\n  }\n\n  Future<void> _batchDownload() async {\n    if (!_selecting || _selectedComicIds.isEmpty) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.select_comics'))),\n        );\n      }\n      return;\n    }\n    if (!isPro) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(content: Text(tr('app.pro_required'))),\n      );\n      return;\n    }\n\n    try {\n      await method.downloadAll(_selectedComicIds.toList());\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.download_started'))),\n        );\n      }\n    } catch (e) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.download_failed'))),\n        );\n      }\n    }\n  }\n\n  Future<void> _moveToFolder() async {\n    if (_selectedComicIds.isEmpty) return;\n\n    List<LocalFavoriteFolder> targetFolders =\n        _folders.where((f) => f.id != _currentFolderId).toList();\n\n    String? targetFolderId = await showDialog<String>(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(tr('local_favorite.move_to_folder')),\n          content: SizedBox(\n            width: double.maxFinite,\n            child: ListView.builder(\n              shrinkWrap: true,\n              itemCount: targetFolders.length + 1,\n              itemBuilder: (context, index) {\n                if (index == 0) {\n                  return ListTile(\n                    leading: const Icon(Icons.folder_special),\n                    title: Text(tr('local_favorite.all_folders')),\n                    onTap: () {\n                      Navigator.of(context).pop('');\n                    },\n                  );\n                }\n                final folder = targetFolders[index - 1];\n                return ListTile(\n                  leading: const Icon(Icons.folder),\n                  title: Text(folder.name),\n                  onTap: () {\n                    Navigator.of(context).pop(folder.id);\n                  },\n                );\n              },\n            ),\n          ),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n              child: Text(tr('app.cancel')),\n            ),\n          ],\n        );\n      },\n    );\n\n    if (targetFolderId != null) {\n      try {\n        await method.moveLocalFavoriteComics(\n          _selectedComicIds.toList(),\n          targetFolderId,\n        );\n        setState(() {\n          _selecting = false;\n          _selectedComicIds.clear();\n        });\n        await _loadComics();\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.move_success'))),\n          );\n        }\n      } catch (e) {\n        if (mounted) {\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.move_failed'))),\n          );\n        }\n      }\n    }\n  }\n\n  Future<void> _removeSelected() async {\n    if (_selectedComicIds.isEmpty) return;\n    bool? confirm = await showDialog<bool>(\n      context: context,\n      builder: (BuildContext context) {\n        return AlertDialog(\n          title: Text(tr('local_favorite.remove_selected')),\n          content: Text(tr('local_favorite.remove_selected_confirm')),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () => Navigator.of(context).pop(false),\n              child: Text(tr('app.cancel')),\n            ),\n            TextButton(\n              onPressed: () => Navigator.of(context).pop(true),\n              child: Text(tr('app.confirm')),\n            ),\n          ],\n        );\n      },\n    );\n    if (confirm != true) return;\n\n    try {\n      final ids = _selectedComicIds.toList();\n      for (final id in ids) {\n        await method.removeLocalFavoriteComic(id);\n      }\n      if (!mounted) return;\n      setState(() {\n        _selectedComicIds.clear();\n        _selecting = false;\n      });\n      await _loadComics();\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.remove_selected_success'))),\n        );\n      }\n    } catch (e) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.remove_selected_failed'))),\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/MigrateScreen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\n\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\n// 数据迁移页面\nclass MigrateScreen extends StatefulWidget {\n  const MigrateScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _MigrateScreenState();\n}\n\nclass _MigrateScreenState extends State<MigrateScreen> {\n  late final Key _key = UniqueKey();\n  late final Future _future = _load();\n  late String _current;\n  late List<String> paths;\n  String _message = \"\";\n\n  int _migrate = 0; // 0 没有开始迁移，1 正在迁移，2 迁移成功，3 迁移失败\n\n  Future _load() async {\n    await method.setDownloadRunning(false);\n    _current = await method.dataLocal();\n    if (Platform.isAndroid) {\n      paths = await method.androidGetExtendDirs();\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text('数据迁移'),\n      ),\n      body: ContentBuilder(\n        key: _key,\n        future: _future,\n        onRefresh: () async {},\n        successBuilder:\n            (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n          switch (_migrate) {\n            case 0:\n              return PikaListView(\n                children: [\n                  Container(\n                    padding: const EdgeInsets.all(10),\n                    child: const Text(\n                      \"1. 为了手机数据存储空间不足, 且具有内存卡的安卓手机设计, 可将数据迁移到内存卡上。\\n\\n\"\n                      \"2. 您在迁移之前, 请确保您的下载处于暂停状态, 或下载均已完成, 以保证您的数据完整性。\\n\\n\"\n                      \"3. 如果迁移中断, 迁移失败, 或其他原因导致程序无法启动, 图片失效等问题, 您可在程序管理中清除本应用程序的数据, 以回复正常使用。\\n\\n\"\n                      \"4. 如果您将数据迁移后将内存卡取出, 将会使用默认本地存储, 再次插入同一张内存卡会继续使用该储存卡, 不支持更换内存卡, 途中您若再次迁移会发生数据覆盖, 这必然会丢失一部分数据.\\n\\n\"\n                      \"5. 您不能更改, 删除, 移动这些数据, 否则程序可能不能正常执行\\n\\n\"\n                      \"6. 迁移成功之前一定不要退出应用程序, 也不要按返回键\\n\\n\"\n                      \"7. 如果您已经了解此功能, 悉知文件迁移的风险, 可以在下面的按钮中选择一项执行\\n\\n\",\n                    ),\n                  ),\n                  Container(\n                    padding: const EdgeInsets.all(10),\n                    child: Text(\"当前文件储存路径 : $_current\"),\n                  ),\n                  ...paths.map((e) => Container(\n                        padding: const EdgeInsets.all(10),\n                        child: MaterialButton(\n                          color: Theme.of(context).colorScheme.secondary,\n                          textColor:\n                              Theme.of(context).textTheme.bodyText1?.color,\n                          padding: const EdgeInsets.all(10),\n                          onPressed: () async {\n                            if (!await confirmDialog(context, \"文件迁移\",\n                                \"您将要迁移到$e, 迁移过程中一定《 不 要 关 闭 程 序 》\")) {\n                              return;\n                            }\n                            setState(() {\n                              _migrate = 1;\n                            });\n                            try {\n                              await method.migrate(e);\n                              setState(() {\n                                _migrate = 2;\n                              });\n                            } catch (ex, tr) {\n                              _message = \"$ex\\n$tr\\n\";\n                              setState(() {\n                                _migrate = 3;\n                              });\n                            }\n                          },\n                          child: Text(\"迁移到 $e\"),\n                        ),\n                      )),\n                ],\n              );\n            case 1:\n              return const ContentLoading(label: \"迁移中\");\n            case 2:\n              return const Center(child: Text(\"迁移成功 您需要关闭应用程序重新启动\"));\n            case 3:\n              return Center(child: Text(\"迁移失败\\n$_message\"));\n            default:\n              throw \"\";\n          }\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/ModifyPasswordScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentLoading.dart';\n\nimport '../basic/Common.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass ModifyPasswordScreen extends StatefulWidget {\n  const ModifyPasswordScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ModifyPasswordScreenState();\n}\n\nclass _ModifyPasswordScreenState extends State<ModifyPasswordScreen> {\n  late bool _loading = false;\n  late String _oldPassword = \"\";\n  late String _newPassword = \"\";\n  late String _newPasswordRep = \"\";\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr(\"screen.modify_password.title\")),\n      ),\n      body: _loading\n          ? Stack(\n              children: [\n                ContentLoading(label: tr(\"screen.modify_password.please_wait\")),\n                WillPopScope(\n                  child: Container(),\n                  onWillPop: () async {\n                    return false;\n                  },\n                ),\n              ],\n            )\n          : _buildForm(),\n    );\n  }\n\n  Widget _buildForm() {\n    return PikaListView(\n      children: [\n        const Divider(),\n        ListTile(\n          title: Text(tr(\"screen.modify_password.old_password\")),\n          subtitle: Text(_oldPassword == \"\" ? tr(\"screen.modify_password.not_filled\") : '\\u2022' * 10),\n          onTap: () async {\n            String? input = await displayTextInputDialog(\n              context,\n              src: _oldPassword,\n              title: tr('screen.modify_password.old_password'),\n              hint: tr('screen.modify_password.please_enter_old_password'),\n              isPasswd: true,\n            );\n            if (input != null) {\n              setState(() {\n                _oldPassword = input;\n              });\n            }\n          },\n        ),\n        const Divider(),\n        ListTile(\n          title: Text(tr(\"screen.modify_password.new_password\")),\n          subtitle: Text(_newPassword == \"\" ? tr(\"screen.modify_password.not_filled\") : '\\u2022' * 10),\n          onTap: () async {\n            String? input = await displayTextInputDialog(\n              context,\n              src: _newPassword,\n              title: tr('screen.modify_password.new_password'),\n              hint: tr('screen.modify_password.please_enter_new_password'),\n              isPasswd: true,\n            );\n            if (input != null) {\n              setState(() {\n                _newPassword = input;\n              });\n            }\n          },\n        ),\n        const Divider(),\n        ListTile(\n          title: Text(tr(\"screen.modify_password.repeat_new_password\")),\n          subtitle: Text(_newPasswordRep == \"\" ? tr(\"screen.modify_password.not_filled\") : '\\u2022' * 10),\n          onTap: () async {\n            String? input = await displayTextInputDialog(\n              context,\n              src: _newPasswordRep,\n              title: tr('screen.modify_password.repeat_new_password'),\n              hint: tr('screen.modify_password.please_repeat_new_password'),\n              isPasswd: true,\n            );\n            if (input != null) {\n              setState(() {\n                _newPasswordRep = input;\n              });\n            }\n          },\n        ),\n        const Divider(),\n        Container(\n          margin: const EdgeInsets.all(10),\n          child: MaterialButton(\n            textColor: Colors.white,\n            color: Theme.of(context).appBarTheme.backgroundColor,\n            onPressed: () async {\n              if (_newPasswordRep != _newPassword) {\n                defaultToast(context, tr(\"screen.modify_password.new_password_mismatch\"));\n                return;\n              }\n              setState(() {\n                _loading = true;\n              });\n              try {\n                await method.updatePassword(_oldPassword, _newPassword);\n                defaultToast(context, tr(\"screen.modify_password.modify_success\"));\n                Navigator.of(context).pop();\n              } catch (e) {\n                defaultToast(context, \"${tr('screen.modify_password.failed')} : $e\");\n                setState(() {\n                  _loading = false;\n                });\n              }\n            },\n            child: Text(tr(\"screen.modify_password.confirm\")),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/NetworkSettingsScreen.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/screens/components/NetworkSetting.dart';\n\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass NetworkSettingsScreen extends StatelessWidget {\n  const NetworkSettingsScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) => Scaffold(\n        appBar: AppBar(title: Text(tr('screen.network_settings.title'))),\n        body: PikaListView(\n          children: const [\n            NetworkSetting(),\n          ],\n        ),\n      );\n}\n"
  },
  {
    "path": "lib/screens/PkzArchiveScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\nimport 'package:pikapika/screens/components/PkzComicInfoCard.dart';\nimport 'package:uni_links/uni_links.dart';\nimport 'package:uri_to_file/uri_to_file.dart';\n\nimport '../basic/Common.dart';\nimport '../basic/Navigator.dart';\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/Platform.dart';\nimport 'PkzComicInfoScreen.dart';\nimport 'components/ListView.dart';\n\nclass PkzArchiveScreen extends StatefulWidget {\n  final bool holdPkz;\n  final String pkzPath;\n\n  const PkzArchiveScreen({\n    Key? key,\n    required this.pkzPath,\n    this.holdPkz = false,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PkzArchiveScreenState();\n}\n\nclass _PkzArchiveScreenState extends State<PkzArchiveScreen> with RouteAware {\n  Map<String, PkzComicViewLog> _logMap = {};\n  late String _fileName;\n  late Future _future;\n  late Key _key;\n  late PkzArchive _info;\n  StreamSubscription<String?>? _linkSubscription;\n\n  @override\n  void initState() {\n    if (widget.holdPkz) {\n      _linkSubscription = linkSubscript(context);\n    }\n    _fileName = p.basename(widget.pkzPath);\n    _future = _load();\n    _key = UniqueKey();\n    super.initState();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    routeObserver.subscribe(this, ModalRoute.of(context)!);\n  }\n\n  @override\n  void dispose() {\n    _linkSubscription?.cancel();\n    routeObserver.unsubscribe(this);\n    super.dispose();\n  }\n\n  @override\n  void didPopNext() {\n    () async {\n      var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath);\n      for (var value in a) {\n        _logMap[value.lastViewComicId] = value;\n      }\n      setState(() {});\n    }();\n  }\n\n  Future _load() async {\n    await method.viewPkz(_fileName, widget.pkzPath);\n    if (Platform.isAndroid) {\n      late bool g;\n      if (androidVersion < 30) {\n        g = await Permission.storage.request().isGranted;\n      }else{\n        g = await Permission.manageExternalStorage.request().isGranted;\n      }\n      if (!g) {\n        throw 'error permission';\n      }\n    }\n    _info = await method.pkzInfo(widget.pkzPath);\n    if (_info.comics.length == 1) {\n      Navigator.of(context).pushReplacement(mixRoute(\n        builder: (BuildContext context) => PkzComicInfoScreen(\n          pkzPath: widget.pkzPath,\n          pkzComic: _info.comics.first,\n          holdPkz: widget.holdPkz,\n        ),\n      ));\n    }\n    var a = await method.pkzComicViewLogs(_fileName, widget.pkzPath);\n    for (var value in a) {\n      _logMap[value.lastViewComicId] = value;\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(_fileName),\n      ),\n      body: ContentBuilder(\n        key: _key,\n        future: _future,\n        onRefresh: () async {\n          setState(() {\n            _future = _load();\n            _key = UniqueKey();\n          });\n        },\n        successBuilder: (\n          BuildContext context,\n          AsyncSnapshot snapshot,\n        ) {\n          return PikaListView(children: [\n            ..._info.comics\n                .map((e) => GestureDetector(\n                      behavior: HitTestBehavior.opaque,\n                      onTap: () {\n                        Navigator.of(context).push(mixRoute(\n                          builder: (BuildContext context) {\n                            return PkzComicInfoScreen(\n                              pkzComic: e,\n                              pkzPath: widget.pkzPath,\n                            );\n                          },\n                        ));\n                      },\n                      child: PkzComicInfoCard(\n                        info: e,\n                        pkzPath: widget.pkzPath,\n                        displayViewLog: _logMap[e.id],\n                      ),\n                    ))\n                .toList(),\n          ]);\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/PkzComicInfoScreen.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/PkzReaderScreen.dart';\n\nimport '../basic/Common.dart';\nimport '../basic/Navigator.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'components/ListView.dart';\nimport 'components/PkzComicInfoCard.dart';\n\nclass PkzComicInfoScreen extends StatefulWidget {\n  final bool holdPkz;\n  final String pkzPath;\n  final PkzComic pkzComic;\n\n  const PkzComicInfoScreen({\n    Key? key,\n    required this.pkzPath,\n    required this.pkzComic,\n    this.holdPkz = false,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PkzComicInfoScreenState();\n}\n\nclass _PkzComicInfoScreenState extends State<PkzComicInfoScreen>\n    with RouteAware {\n  PkzComicViewLog? _log;\n  StreamSubscription<String?>? _linkSubscription;\n\n  @override\n  void initState() {\n    if (widget.holdPkz) {\n      _linkSubscription = linkSubscript(context);\n    }\n    _load();\n    super.initState();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    routeObserver.subscribe(this, ModalRoute.of(context)!);\n  }\n\n  @override\n  void dispose() {\n    _linkSubscription?.cancel();\n    routeObserver.unsubscribe(this);\n    super.dispose();\n  }\n\n  @override\n  void didPopNext() {\n    () async {\n      _log = await method.pkzComicViewLogByPkzNameAndId(\n        p.basename(widget.pkzPath),\n        widget.pkzComic.id,\n      );\n      setState(() {});\n    }();\n  }\n\n  _load() async {\n    await method.viewPkzComic(\n      p.basename(widget.pkzPath),\n      widget.pkzPath,\n      widget.pkzComic.id,\n      widget.pkzComic.title,\n    );\n    _log = await method.pkzComicViewLogByPkzNameAndId(\n      p.basename(widget.pkzPath),\n      widget.pkzComic.id,\n    );\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    List<Widget> chapterButtons = [];\n    for (var volume in widget.pkzComic.volumes) {\n      for (var chapter in volume.chapters) {\n        chapterButtons.add(MaterialButton(\n          onPressed: () {\n            Navigator.of(context).push(mixRoute(\n              builder: (BuildContext context) {\n                return PkzReaderScreen(\n                  comicInfo: widget.pkzComic,\n                  currentEpId: chapter.id,\n                  pkzPath: widget.pkzPath,\n                );\n              },\n            ));\n          },\n          color: Colors.white,\n          child: Text(\n            chapter.title,\n            style: const TextStyle(color: Colors.black),\n          ),\n        ));\n      }\n    }\n\n    final theme = Theme.of(context);\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(\n          widget.pkzComic.title,\n        ),\n      ),\n      body: PikaListView(children: [\n        PkzComicInfoCard(info: widget.pkzComic, pkzPath: widget.pkzPath),\n        Container(\n          padding: const EdgeInsets.only(top: 5, bottom: 5),\n          decoration: BoxDecoration(\n            border: Border(\n              bottom: BorderSide(\n                color: theme.dividerColor,\n              ),\n            ),\n          ),\n          child: Wrap(\n            children: widget.pkzComic.tags.map((e) {\n              return Container(\n                padding: const EdgeInsets.only(\n                  left: 10,\n                  right: 10,\n                  top: 3,\n                  bottom: 3,\n                ),\n                margin: const EdgeInsets.only(\n                  left: 5,\n                  right: 5,\n                  top: 3,\n                  bottom: 3,\n                ),\n                decoration: BoxDecoration(\n                  color: Colors.pink.shade100,\n                  border: Border.all(\n                    style: BorderStyle.solid,\n                    color: Colors.pink.shade400,\n                  ),\n                  borderRadius: const BorderRadius.all(Radius.circular(30)),\n                ),\n                child: Text(\n                  e,\n                  style: TextStyle(\n                    color: Colors.pink.shade500,\n                    height: 1.4,\n                  ),\n                  strutStyle: const StrutStyle(\n                    height: 1.4,\n                  ),\n                ),\n              );\n            }).toList(),\n          ),\n        ),\n        Container(\n          padding: const EdgeInsets.only(\n            top: 5,\n            bottom: 5,\n            left: 10,\n            right: 10,\n          ),\n          decoration: BoxDecoration(\n            border: Border(\n              bottom: BorderSide(\n                color: theme.dividerColor,\n              ),\n            ),\n          ),\n          child: SelectableText(\n            widget.pkzComic.description,\n            style: const TextStyle(\n              fontSize: 13,\n              color: Colors.grey,\n            ),\n          ),\n        ),\n        LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints constraints) {\n            PkzChapter? first;\n            Map<String, PkzChapter> chapters = {};\n            for (var vol in widget.pkzComic.volumes) {\n              for (var c in vol.chapters) {\n                first ??= c;\n                chapters[c.id] = (c);\n              }\n            }\n            if (chapters.isEmpty) {\n              return Container();\n            }\n            final width = constraints.maxWidth;\n            return Container(\n              padding: const EdgeInsets.only(left: 10, right: 10),\n              margin: const EdgeInsets.only(top: 10, bottom: 10),\n              width: width,\n              child: MaterialButton(\n                onPressed: () {\n                  if (chapters.containsKey(_log?.lastViewEpId)) {\n                    Navigator.of(context).push(mixRoute(\n                      builder: (BuildContext context) {\n                        return PkzReaderScreen(\n                          comicInfo: widget.pkzComic,\n                          currentEpId: _log!.lastViewEpId,\n                          pkzPath: widget.pkzPath,\n                          initPicturePosition: _log!.lastViewPictureRank,\n                        );\n                      },\n                    ));\n                    return;\n                  }\n                  Navigator.of(context).push(mixRoute(\n                    builder: (BuildContext context) {\n                      return PkzReaderScreen(\n                        comicInfo: widget.pkzComic,\n                        currentEpId: first!.id,\n                        pkzPath: widget.pkzPath,\n                      );\n                    },\n                  ));\n                },\n                child: Row(\n                  children: [\n                    Expanded(\n                      child: Container(\n                        color: Theme.of(context)\n                            .textTheme\n                            .bodyText1!\n                            .color!\n                            .withOpacity(.05),\n                        padding: const EdgeInsets.all(10),\n                        child: Text(\n                          chapters.containsKey(_log?.lastViewEpId)\n                              ? \"继续阅读 ${chapters[_log?.lastViewEpId]!.title}\"\n                              : \"开始阅读\",\n                          textAlign: TextAlign.center,\n                        ),\n                      ),\n                    )\n                  ],\n                ),\n              ),\n            );\n          },\n        ),\n        Wrap(\n          spacing: 10,\n          runSpacing: 10,\n          alignment: WrapAlignment.spaceAround,\n          children: chapterButtons,\n        ),\n      ]),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/PkzReaderScreen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:path/path.dart' as p;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/FullScreenUI.dart';\nimport 'package:pikapika/basic/config/ReaderDirection.dart';\nimport 'package:pikapika/basic/config/ReaderType.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ImageReader.dart';\nimport 'components/RightClickPop.dart';\n\n// 阅读下载的内容\nclass PkzReaderScreen extends StatefulWidget {\n  final String pkzPath;\n  final PkzComic comicInfo;\n  late final List<PkzChapter> epList;\n  final String currentEpId;\n  final int? initPicturePosition;\n  final ReaderType pagerType = currentReaderType();\n  final ReaderDirection pagerDirection = gReaderDirection;\n  late final bool autoFullScreen;\n\n  PkzReaderScreen({\n    Key? key,\n    required this.comicInfo,\n    required this.currentEpId,\n    this.initPicturePosition,\n    bool? autoFullScreen,\n    required this.pkzPath,\n  }) : super(key: key) {\n    epList = [];\n    for (var volume in comicInfo.volumes) {\n      for (var chapter in volume.chapters) {\n        epList.add(chapter);\n      }\n    }\n    this.autoFullScreen = autoFullScreen ?? currentAutoFullScreen();\n  }\n\n  @override\n  State<StatefulWidget> createState() => _PkzReaderScreenState();\n}\n\nclass _PkzReaderScreenState extends State<PkzReaderScreen> {\n  late PkzChapter _ep;\n  late int _epOrder;\n  late bool _fullScreen = false;\n  late List<PkzPicture> pictures = [];\n  late Future _future = _load();\n  int? _lastChangeRank;\n  bool _replacement = false;\n\n  @override\n  void initState() {\n    // EP\n    pictures.clear();\n    for (var ep in widget.epList) {\n      if (ep.id == widget.currentEpId) {\n        _ep = ep;\n        _epOrder = widget.epList.indexOf(ep);\n        pictures.addAll(ep.pictures);\n        break;\n      }\n    }\n    if (widget.autoFullScreen) {\n      setState(() {\n        SystemChrome.setEnabledSystemUIMode(\n          SystemUiMode.manual,\n          overlays: [],\n        );\n        _fullScreen = true;\n      });\n    }\n    // INIT\n    _future = _load();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (!_replacement) {\n      switchFullScreenUI();\n    }\n    super.dispose();\n  }\n\n  Future _load() async {\n    if (widget.initPicturePosition == null) {\n      await method.viewPkzEpAndPicture(\n        p.basename(widget.pkzPath),\n        widget.pkzPath,\n        widget.comicInfo.id,\n        widget.comicInfo.title,\n        _ep.id,\n        _ep.title,\n        0,\n      );\n    }\n  }\n\n  Future _onPositionChange(int position) async {\n    _lastChangeRank = position;\n    await method.viewPkzEpAndPicture(\n      p.basename(widget.pkzPath),\n      widget.pkzPath,\n      widget.comicInfo.id,\n      widget.comicInfo.title,\n      _ep.id,\n      _ep.title,\n      position,\n    );\n    return;\n  }\n\n  FutureOr<dynamic> _onDownload() async {\n    defaultToast(context, tr(\"screen.pkz_reader.reading_downloaded_comic\"));\n  }\n\n  FutureOr<dynamic> _onChangeEp(int epOrder) {\n    final ep = widget.epList[epOrder];\n    _replacement = true;\n    Navigator.of(context).pushReplacement(\n      mixRoute(\n        builder: (context) => PkzReaderScreen(\n          comicInfo: widget.comicInfo,\n          pkzPath: widget.pkzPath,\n          currentEpId: ep.id,\n          autoFullScreen: _fullScreen,\n        ),\n      ),\n    );\n  }\n\n  FutureOr<dynamic> _onReloadEp() {\n    _replacement = true;\n    Navigator.of(context).pushReplacement(\n      mixRoute(\n        builder: (context) => PkzReaderScreen(\n          comicInfo: widget.comicInfo,\n          currentEpId: widget.currentEpId,\n          initPicturePosition: _lastChangeRank ?? widget.initPicturePosition,\n          // maybe null\n          autoFullScreen: _fullScreen,\n          pkzPath: widget.pkzPath,\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return readerKeyboardHolder(_build(context));\n  }\n\n  Widget _build(BuildContext context) {\n    return FutureBuilder(\n      future: _future,\n      builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n        if (snapshot.hasError) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: ContentError(\n              error: snapshot.error,\n              stackTrace: snapshot.stackTrace,\n              onRefresh: () async {\n                setState(() {\n                  _future = _load();\n                });\n              },\n            ),\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            appBar: _fullScreen\n                ? null\n                : AppBar(\n                    title: Text(\"${_ep.title} - ${widget.comicInfo.title}\"),\n                  ),\n            body: ContentLoading(label: tr('app.loading')),\n          );\n        }\n        var epNameMap = <int, String>{};\n        for (var i = 0; i < widget.epList.length; i++) {\n          epNameMap[i] = widget.epList[i].title;\n        }\n        return Scaffold(\n          body: ImageReader(\n            ImageReaderStruct(\n              images: pictures\n                  .map((e) => ReaderImageInfo(\n                        \"\",\n                        \"\",\n                        \"\",\n                        e.width,\n                        e.height,\n                        e.format,\n                        0,\n                        pkzFile: PkzFile(widget.pkzPath, e.picturePath),\n                      ))\n                  .toList(),\n              fullScreen: _fullScreen,\n              onFullScreenChange: _onFullScreenChange,\n              onPositionChange: _onPositionChange,\n              initPosition: widget.initPicturePosition,\n              epOrder: _epOrder,\n              epNameMap: epNameMap,\n              comicTitle: widget.comicInfo.title,\n              onReloadEp: _onReloadEp,\n              onChangeEp: _onChangeEp,\n              onDownload: _onDownload,\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Future _onFullScreenChange(bool fullScreen) async {\n    setState(() {\n      if (fullScreen) {\n        if (Platform.isAndroid || Platform.isIOS) {\n          SystemChrome.setEnabledSystemUIMode(\n            SystemUiMode.manual,\n            overlays: [],\n          );\n        }\n      } else {\n        switchFullScreenUI();\n      }\n      _fullScreen = fullScreen;\n    });\n  }\n}\n"
  },
  {
    "path": "lib/screens/ProScreen.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/AccessKeyReplaceScreen.dart';\n\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/IsPro.dart';\nimport 'components/ListView.dart';\n\nclass ProScreen extends StatefulWidget {\n  const ProScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ProScreenState();\n}\n\nclass _ProScreenState extends State<ProScreen> {\n  String _username = \"\";\n\n  @override\n  void initState() {\n    method.getUsername().then((value) {\n      setState(() {\n        _username = value;\n      });\n    });\n    proEvent.subscribe(_setState);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    proEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  _setState(_) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var size = MediaQuery.of(context).size;\n    var min = size.width < size.height ? size.width : size.height;\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.pro.title')),\n      ),\n      body: PikaListView(\n        children: [\n          SizedBox(\n            width: min / 2,\n            height: min / 2,\n            child: Center(\n              child: Icon(\n                isPro ? Icons.offline_bolt : Icons.offline_bolt_outlined,\n                size: min / 3,\n                color: Colors.grey.shade500,\n              ),\n            ),\n          ),\n          Center(child: Text(_username)),\n          Container(height: 20),\n          const Divider(),\n          Padding(\n            padding: const EdgeInsets.all(20),\n            child: Text(tr('screen.pro.power_guide')),\n          ),\n          const Divider(),\n          Row(\n            children: [\n              Expanded(\n                child: ListTile(\n                  title: Text(tr('screen.pro.sign_in_exchange')),\n                  subtitle: Text(\n                    proInfoAf.isPro\n                        ? \"${tr('screen.pro.powered')} (${DateTime.fromMillisecondsSinceEpoch(1000 * proInfoAf.expire).toString()})\"\n                        : tr('screen.pro.not_powered'),\n                  ),\n                ),\n              ),\n              Expanded(\n                child: ListTile(\n                  title: Text(tr('screen.pro.pat_membership')),\n                  subtitle: Text(\n                    proInfoPat.isPro ? tr('screen.pro.powered') : tr('screen.pro.not_powered'),\n                  ),\n                  onTap: () {\n                    defaultToast(context, tr('screen.pro.click_pat_to_change'));\n                  },\n                ),\n              ),\n            ],\n          ),\n          const Divider(),\n          ListTile(\n            title: Text(tr('screen.pro.i_have_powered')),\n            onTap: () async {\n              try {\n                await method.reloadPro();\n                defaultToast(context, \"SUCCESS\");\n              } catch (e, s) {\n                defaultToast(context, \"FAIL\");\n              }\n              await reloadIsPro();\n              setState(() {});\n            },\n          ),\n          const Divider(),\n          ListTile(\n            title: Text(tr('screen.pro.i_just_powered')),\n            onTap: () async {\n              var code = await inputString(context, tr('screen.pro.enter_code'));\n              if (code != null) {\n                code = code.trim();\n                if (code.isNotEmpty) {\n                  try {\n                    await method.inputCdKey(code);\n                    defaultToast(context, \"SUCCESS\");\n                  } catch (e, s) {\n                    defaultToast(context, \"FAIL\");\n                  }\n                }\n              }\n              await reloadIsPro();\n              setState(() {});\n            },\n          ),\n          const Divider(),\n          const ProServerNameWidget(),\n          const Divider(),\n          ...patPro(),\n          const Divider(),\n          const Divider(),\n        ],\n      ),\n    );\n  }\n\n  List<Widget> patPro() {\n    List<Widget> widgets = [];\n    if (proInfoPat.accessKey.isNotEmpty) {\n      var text = tr('screen.pro.key_recorded');\n      if (proInfoPat.patId.isNotEmpty) {\n        text += \"\\n${tr('screen.pro.pat_account')} : ${proInfoPat.patId}\";\n      }\n      if (proInfoPat.bindUid.isNotEmpty) {\n        text += \"\\n${tr('screen.pro.bind_pika_account')} : ${proInfoPat.bindUid}\";\n      }\n      if (proInfoPat.requestDelete > 0) {\n        DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(\n          proInfoPat.requestDelete * 1000,\n          isUtc: true,\n        );\n        String formattedDate =\n            DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal());\n        text += \"\\n${tr('screen.pro.bind_account_time')} : $formattedDate\";\n      }\n      if (proInfoPat.reBind > 0) {\n        DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(\n          proInfoPat.reBind * 1000,\n          isUtc: true,\n        );\n        String formattedDate =\n            DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal());\n        text += \"\\n${tr('screen.pro.rebind_time')} : $formattedDate\";\n      }\n      List<TextSpan> append = [];\n      if (proInfoPat.bindUid == \"\") {\n        append.add(TextSpan(\n          text: \"\\n(${tr('screen.pro.pat_bind_hint')})\",\n          style: const TextStyle(color: Colors.blue),\n        ));\n      } else if (proInfoPat.bindUid != _username) {\n        append.add(TextSpan(\n          text: \"\\n(${tr('screen.pro.pat_rebind_hint')})\",\n          style: const TextStyle(color: Colors.red),\n        ));\n      } else if (proInfoPat.isPro == false) {\n        append.add(TextSpan(\n          text: \"\\n(${tr('screen.pro.pat_not_detected')})\",\n          style: const TextStyle(color: Colors.orange),\n        ));\n      } else {\n        append.add(TextSpan(\n          text: \"\\n(${tr('screen.pro.pat_normal')})\",\n          style: const TextStyle(color: Colors.green),\n        ));\n      }\n      widgets.add(ListTile(\n        onTap: () async {\n          print(jsonEncode(proInfoPat));\n          var choose = await chooseMapDialog<int>(\n            context,\n            {\n              tr('screen.pro.update_pat_status'): 2,\n              tr('screen.pro.bind_to_account'): 3,\n              tr('screen.pro.change_pat_key'): 1,\n              tr('screen.pro.clear_pat_info'): 4,\n            },\n            tr('app.please_select'),\n          );\n          switch (choose) {\n            case 1:\n              addPatAccount();\n              break;\n            case 2:\n              reloadPatAccount();\n              break;\n            case 3:\n              bindThisAccount();\n              break;\n            case 4:\n              clearPat();\n              break;\n          }\n        },\n        title: Text(tr('screen.pro.pat_membership')),\n        subtitle: Text.rich(TextSpan(children: [\n          TextSpan(text: text),\n          ...append,\n        ])),\n      ));\n    } else {\n      widgets.add(ListTile(\n        onTap: () {\n          addPatAccount();\n        },\n        title: Text(tr('screen.pro.pat_membership')),\n        subtitle: Text(tr('screen.pro.click_to_bind')),\n      ));\n    }\n    return widgets;\n  }\n\n  void addPatAccount() async {\n    print(jsonEncode(proInfoPat));\n    String? key = await inputString(context, tr('screen.pro.enter_auth_code'));\n    if (key != null) {\n      await Navigator.of(context)\n          .push(mixRoute(builder: (BuildContext context) {\n        return AccessKeyReplaceScreen(accessKey: key);\n      }));\n    }\n  }\n\n  reloadPatAccount() async {\n    defaultToast(context, tr('screen.pro.please_wait'));\n    try {\n      await method.reloadPatAccount();\n      await reloadIsPro();\n      defaultToast(context, \"SUCCESS\");\n    } catch (e) {\n      defaultToast(context, \"FAIL : $e\");\n    } finally {}\n  }\n\n  bindThisAccount() async {\n    defaultToast(context, tr('screen.pro.please_wait'));\n    try {\n      await method.bindThisAccount();\n      await method.reloadPatAccount();\n      await reloadIsPro();\n      defaultToast(context, \"SUCCESS\");\n    } catch (e) {\n      defaultToast(context, \"FAIL : $e\");\n    } finally {}\n  }\n\n  clearPat() async {\n    await method.clearPat();\n    await reloadIsPro();\n    defaultToast(context, \"Success\");\n  }\n}\n\nclass ProServerNameWidget extends StatefulWidget {\n  const ProServerNameWidget({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ProServerNameWidgetState();\n}\n\nclass _ProServerNameWidgetState extends State<ProServerNameWidget> {\n  String _serverName = \"\";\n\n  @override\n  void initState() {\n    method.getProServerName().then((value) {\n      setState(() {\n        _serverName = value;\n      });\n    });\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ListTile(\n      title: Text(tr('screen.pro.power_method')),\n      subtitle: Text(_loadServerName()),\n      onTap: () async {\n        final serverName = await chooseMapDialog(\n          context,\n          {\n            tr('screen.pro.wind_power'): \"HK\",\n            tr('screen.pro.hydro_power'): \"US\",\n            tr('screen.pro.solar_power'): \"SIG\",\n            tr('screen.pro.nuclear_power'): \"JPOS\",\n          },\n          tr('screen.pro.choose_power_method'),\n        );\n        if (serverName != null && serverName.isNotEmpty) {\n          await method.setProServerName(serverName);\n          setState(() {\n            _serverName = serverName;\n          });\n        }\n      },\n    );\n  }\n\n  String _loadServerName() {\n    switch (_serverName) {\n      case \"HK\":\n        return tr('screen.pro.wind_power');\n      case \"US\":\n        return tr('screen.pro.hydro_power');\n      case \"SIG\":\n        return tr('screen.pro.solar_power');\n      case \"JPOS\":\n        return tr('screen.pro.nuclear_power');\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/RandomComicsScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/ListLayout.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\n\nimport '../basic/config/Address.dart';\nimport 'components/ComicListBuilder.dart';\nimport 'components/Common.dart';\nimport 'components/RightClickPop.dart';\n\n// 随机漫画页面\nclass RandomComicsScreen extends StatefulWidget {\n  const RandomComicsScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _RandomComicsScreenState();\n}\n\nclass _RandomComicsScreenState extends State<RandomComicsScreen> {\n\n  @override\n  Widget build(BuildContext context){\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.random_comics.title')),\n        actions: [\n          commonPopMenu(context),\n          addressPopMenu(context),\n        ],\n      ),\n      body: ComicListBuilder(method.randomComics),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/RankingsScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/Avatar.dart';\nimport 'package:pikapika/screens/components/ContentBuilder.dart';\n\nimport '../basic/Cross.dart';\nimport '../basic/Navigator.dart';\nimport '../basic/config/Address.dart';\nimport 'ComicsScreen.dart';\nimport 'components/ComicListBuilder.dart';\nimport 'components/Common.dart';\nimport 'components/FitButton.dart';\nimport 'components/RightClickPop.dart';\n\n// 排行榜\nclass RankingsScreen extends StatelessWidget {\n  const RankingsScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    var theme = Theme.of(context);\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.rankings.title')),\n        actions: [\n          commonPopMenu(context),\n          addressPopMenu(context),\n        ],\n      ),\n      body: DefaultTabController(\n        length: 4,\n        child: Column(\n          children: [\n            Container(\n              height: 40,\n              color: theme.colorScheme.secondary.withOpacity(.025),\n              child: TabBar(\n                indicatorColor: theme.colorScheme.secondary,\n                labelColor: theme.colorScheme.secondary,\n                tabs: [\n                  Tab(text: tr('screen.rankings.day')),\n                  Tab(text: tr('screen.rankings.week')),\n                  Tab(text: tr('screen.rankings.month')),\n                  Tab(text: tr('screen.rankings.knight')),\n                ],\n              ),\n            ),\n            const Expanded(\n              child: TabBarView(\n                children: [\n                  _Leaderboard(\"H24\"),\n                  _Leaderboard(\"D7\"),\n                  _Leaderboard(\"D30\"),\n                  _KnightLeaderBoard(),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _Leaderboard extends StatefulWidget {\n  final String type;\n\n  const _Leaderboard(this.type);\n\n  @override\n  State<StatefulWidget> createState() => _LeaderboardState();\n}\n\nclass _LeaderboardState extends State<_Leaderboard> {\n  @override\n  Widget build(BuildContext context) {\n    return ComicListBuilder(() => method.leaderboard(widget.type));\n  }\n}\n\nclass _KnightLeaderBoard extends StatefulWidget {\n  const _KnightLeaderBoard();\n\n  @override\n  State<StatefulWidget> createState() => _KnightLeaderBoardState();\n}\n\nclass _KnightLeaderBoardState extends State<_KnightLeaderBoard> {\n  Future<List<Knight>> _future = method.leaderboardOfKnight();\n  Key _key = UniqueKey();\n\n  @override\n  Widget build(BuildContext context) {\n    return ContentBuilder(\n      key: _key,\n      future: _future,\n      onRefresh: () async {\n        setState(() {\n          _future = method.leaderboardOfKnight();\n          _key = UniqueKey();\n        });\n      },\n      successBuilder: (\n        BuildContext context,\n        AsyncSnapshot<List<Knight>> snapshot,\n      ) {\n        return RefreshIndicator(\n          onRefresh: () async {\n            setState(() {\n              _future = method.leaderboardOfKnight();\n            });\n          },\n          child: ListView(children: [\n            ...snapshot.requireData.map(_knightCard).toList(),\n            SizedBox(\n              height: 80,\n              child: FitButton(\n                text: tr('screen.rankings.refresh'),\n                onPressed: () async {\n                  setState(() {\n                    _future = method.leaderboardOfKnight();\n                  });\n                },\n              ),\n            ),\n          ]),\n        );\n      },\n    );\n  }\n\n  Widget _knightCard(Knight e) {\n    final theme = Theme.of(context);\n    var nameStyle = const TextStyle(fontWeight: FontWeight.bold);\n    var levelStyle = TextStyle(\n        fontSize: 12, color: theme.colorScheme.secondary.withOpacity(.8));\n    var connectStyle =\n        TextStyle(color: theme.textTheme.bodyText1?.color?.withOpacity(.8));\n    var datetimeStyle = TextStyle(\n        color: theme.textTheme.bodyText1?.color?.withOpacity(.6), fontSize: 12);\n\n    final card = Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          top: BorderSide(\n            width: .25,\n            style: BorderStyle.solid,\n            color: Colors.grey.shade500.withOpacity(.5),\n          ),\n          bottom: BorderSide(\n            width: .25,\n            style: BorderStyle.solid,\n            color: Colors.grey.shade500.withOpacity(.5),\n          ),\n        ),\n      ),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Avatar(e.avatar),\n          Container(width: 5),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Row(\n                  crossAxisAlignment: CrossAxisAlignment.center,\n                  children: [\n                    Text(e.name, style: nameStyle),\n                    Expanded(child: Container()),\n                    Text(\n                      \"${e.comicsUploaded} ${tr('screen.rankings.comics_count')}\",\n                      style: datetimeStyle,\n                    ),\n                  ],\n                ),\n                Text(\"Lv. ${e.level} (${e.title})\", style: levelStyle),\n                Text(e.slogan ?? \"\", style: connectStyle),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n\n    return InkWell(\n      onTap: () {\n        navPushOrReplace(\n          context,\n          (context) => ComicsScreen(\n            creatorId: e.id,\n            creatorName: e.name,\n          ),\n        );\n      },\n      onLongPress: () {\n        confirmCopy(\n          context,\n          e.name,\n        );\n      },\n      child: card,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/RegisterScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n// import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/NetworkSetting.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\n\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\n\n/// 注册页面\nclass RegisterScreen extends StatefulWidget {\n  const RegisterScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _RegisterScreenState();\n}\n\nclass _RegisterScreenState extends State<RegisterScreen> {\n  late bool _registering = false;\n  late bool _registerOver = false;\n\n  late String _email = \"\";\n  late String _name = \"\";\n  late String _password = \"\";\n  late String _gender = \"bot\";\n  late String _birthday = \"2000-01-01\";\n  late String _question1 = \"问题1\";\n  late String _answer1 = \"回答1\";\n  late String _question2 = \"问题2\";\n  late String _answer2 = \"回答2\";\n  late String _question3 = \"问题3\";\n  late String _answer3 = \"回答3\";\n\n  Future _register() async {\n    setState(() {\n      _registering = true;\n    });\n    try {\n      var mustList = <String>[\n        _email,\n        _name,\n        _password,\n        _gender,\n        _birthday,\n        _question1,\n        _answer1,\n        _question2,\n        _answer2,\n        _question3,\n        _answer3,\n      ];\n      for (var a in mustList) {\n        if (a.isEmpty) {\n          throw tr('screen.register.check_form');\n        }\n      }\n      await method.register(\n        _email,\n        _name,\n        _password,\n        _gender,\n        _birthday,\n        _question1,\n        _answer1,\n        _question2,\n        _answer2,\n        _question3,\n        _answer3,\n      );\n      await method.setUsername(_email);\n      await method.setPassword(_password);\n      await method.clearToken();\n      setState(() {\n        _registerOver = true;\n      });\n    } catch (e) {\n      String message = \"$e\";\n      if (message.contains(\"email is already exist\")) {\n        message = tr('screen.register.account_exists');\n      } else if (message.contains(\"name is already exist\")) {\n        message = tr('screen.register.name_exists');\n      }\n      alertDialog(context, tr('screen.register.register_failed'), message);\n    } finally {\n      setState(() {\n        _registering = false;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    if (_registerOver) {\n      return Scaffold(\n        appBar: AppBar(\n          title: Text(tr('screen.register.register_success')),\n        ),\n        body: Center(\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              Text(tr('screen.register.register_success_desc')),\n              Text('${tr('screen.register.account_label')} : $_email'),\n              Text('${tr('screen.register.nickname_label')} : $_name'),\n              Expanded(child: Container()),\n              Expanded(child: Container()),\n            ],\n          ),\n        ),\n      );\n    }\n    if (_registering) {\n      return Scaffold(\n        appBar: AppBar(),\n        body: ContentLoading(label: tr('screen.register.registering')),\n      );\n    }\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.register.title')),\n        actions: [\n          IconButton(\n            onPressed: () => _register(),\n            icon: const Icon(Icons.check),\n          ),\n        ],\n      ),\n      body: PikaListView(\n        children: [\n          const Divider(),\n          ListTile(\n            title: Text(tr('screen.register.account')),\n            subtitle: Text(_email == \"\" ? tr('screen.register.not_set') : _email),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _email,\n                title: tr('screen.register.account'),\n                hint: tr('screen.register.please_enter_account'),\n                desc: tr('screen.register.account_desc'),\n              );\n              if (input != null) {\n                setState(() {\n                  _email = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.password')),\n            subtitle: Text(_password == \"\" ? tr('screen.register.not_set') : '\\u2022' * 10),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _password,\n                title: tr('screen.register.password'),\n                hint: tr('screen.register.please_enter_password'),\n                desc: tr('screen.register.password_desc'),\n                isPasswd: true,\n              );\n              if (input != null) {\n                setState(() {\n                  _password = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.nickname')),\n            subtitle: Text(_name == \"\" ? tr('screen.register.not_set') : _name),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _name,\n                title: tr('screen.register.nickname'),\n                hint: tr('screen.register.please_enter_nickname'),\n                desc: tr('screen.register.nickname_desc'),\n              );\n              if (input != null) {\n                setState(() {\n                  _name = input;\n                });\n              }\n            },\n          ),\n          const Divider(),\n          ListTile(\n            title: Text(tr('screen.register.gender')),\n            subtitle: Text(_genderText(_gender)),\n            onTap: () async {\n              String? result = await showDialog<String>(\n                context: context,\n                builder: (BuildContext context) {\n                  return SimpleDialog(\n                    title: Text(tr('screen.register.choose_gender')),\n                    children: [\n                      SimpleDialogOption(\n                        child: Text(tr('screen.register.futa')),\n                        onPressed: () {\n                          Navigator.pop(context, 'bot');\n                        },\n                      ),\n                      SimpleDialogOption(\n                        child: Text(tr('screen.register.male')),\n                        onPressed: () {\n                          Navigator.pop(context, 'm');\n                        },\n                      ),\n                      SimpleDialogOption(\n                        child: Text(tr('screen.register.female')),\n                        onPressed: () {\n                          Navigator.pop(context, 'f');\n                        },\n                      ),\n                    ],\n                  );\n                },\n              );\n              if (result != null) {\n                setState(() {\n                  _gender = result;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.birthday')),\n            subtitle: Text(_birthday),\n          ),\n          const Divider(),\n          ListTile(\n            title: Text(tr('screen.register.question_1')),\n            subtitle: Text(_question1 == \"\" ? tr('screen.register.not_set') : _question1),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _question1,\n                title: tr('screen.register.question_1'),\n                hint: tr('screen.register.please_enter_question_1'),\n              );\n              if (input != null) {\n                setState(() {\n                  _question1 = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.answer_1')),\n            subtitle: Text(_answer1 == \"\" ? tr('screen.register.not_set') : _answer1),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _answer1,\n                title: tr('screen.register.answer_1'),\n                hint: tr('screen.register.please_enter_answer_1'),\n              );\n              if (input != null) {\n                setState(() {\n                  _answer1 = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.question_2')),\n            subtitle: Text(_question2 == \"\" ? tr('screen.register.not_set') : _question2),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _question2,\n                title: tr('screen.register.question_2'),\n                hint: tr('screen.register.please_enter_question_2'),\n              );\n              if (input != null) {\n                setState(() {\n                  _question2 = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.answer_2')),\n            subtitle: Text(_answer2 == \"\" ? tr('screen.register.not_set') : _answer2),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _answer2,\n                title: tr('screen.register.answer_2'),\n                hint: tr('screen.register.please_enter_answer_2'),\n              );\n              if (input != null) {\n                setState(() {\n                  _answer2 = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.question_3')),\n            subtitle: Text(_question3 == \"\" ? tr('screen.register.not_set') : _question3),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _question3,\n                title: tr('screen.register.question_3'),\n                hint: tr('screen.register.please_enter_question_3'),\n              );\n              if (input != null) {\n                setState(() {\n                  _question3 = input;\n                });\n              }\n            },\n          ),\n          ListTile(\n            title: Text(tr('screen.register.answer_3')),\n            subtitle: Text(_answer3 == \"\" ? tr('screen.register.not_set') : _answer3),\n            onTap: () async {\n              String? input = await displayTextInputDialog(\n                context,\n                src: _answer3,\n                title: tr('screen.register.answer_3'),\n                hint: tr('screen.register.please_enter_answer_3'),\n              );\n              if (input != null) {\n                setState(() {\n                  _answer3 = input;\n                });\n              }\n            },\n          ),\n          const Divider(),\n          const NetworkSetting(),\n          const Divider(),\n        ],\n      ),\n    );\n  }\n\n  String _genderText(String gender) {\n    switch (gender) {\n      case 'bot':\n        return tr('screen.register.futa');\n      case \"m\":\n        return tr('screen.register.male');\n      case \"f\":\n        return tr('screen.register.female');\n      default:\n        return \"\";\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/SearchAuthorScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport '../basic/config/PagerAction.dart';\nimport 'components/flutter_search_bar.dart' as fsb;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/store/Categories.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\nimport '../basic/Entities.dart';\nimport '../basic/config/Address.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'components/ComicList.dart';\nimport 'components/ComicPager.dart';\nimport 'components/Common.dart';\nimport 'components/GoDownloadSelect.dart';\n\nclass ComicSearchAuthorScreenButton extends StatelessWidget {\n  const ComicSearchAuthorScreenButton({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) => IconButton(\n        icon: const Icon(Icons.person_search),\n        onPressed: () async {\n          Navigator.push(\n            context,\n            MaterialPageRoute(\n              builder: (context) => const SearchAuthorScreen(\n                author: '',\n              ),\n            ),\n          );\n        },\n      );\n}\n\n// 搜索页面\nclass SearchAuthorScreen extends StatefulWidget {\n  final String author;\n  final String? category;\n\n  const SearchAuthorScreen({\n    Key? key,\n    required this.author,\n    this.category,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _SearchAuthorScreenState();\n}\n\nclass _SearchAuthorScreenState extends State<SearchAuthorScreen> {\n  late final _comicListController = ComicListController();\n  late final TextEditingController _textEditController =\n      TextEditingController(text: widget.author);\n  late final fsb.SearchBar _searchBar = fsb.SearchBar(\n    hintText: '${tr('screen.search_author.search_hint')}${categoryTitle(widget.category)}',\n    controller: _textEditController,\n    inBar: false,\n    setState: setState,\n    onSubmitted: (value) {\n      if (value.isNotEmpty) {\n        Navigator.pushReplacement(\n          context,\n          mixRoute(\n            builder: (context) => SearchAuthorScreen(\n              author: value,\n              category: widget.category,\n            ),\n          ),\n        );\n      }\n    },\n    buildDefaultAppBar: (BuildContext context) {\n      return AppBar(\n        title:\n            Text(\"${tr('screen.search_author.by_author')}${widget.author} + ${categoryTitle(widget.category)}\"),\n        actions: [\n          commonPopMenu(\n            context,\n            setState: setState,\n            comicListController: _comicListController,\n          ),\n          addressPopMenu(context),\n          _chooseCategoryAction(),\n          _searchBar.getSearchAction(context),\n        ],\n      );\n    },\n  );\n\n  Widget _chooseCategoryAction() => IconButton(\n        onPressed: () async {\n          String? category = await chooseListDialog(context, tr('screen.search.choose_category'), [\n            categoryTitle(null),\n            ...filteredList(\n              storedCategories,\n              (c) => !shadowCategories.contains(c),\n            ),\n          ]);\n          if (category != null) {\n            if (category == categoryTitle(null)) {\n              category = null;\n            }\n            Navigator.of(context).pushReplacement(mixRoute(\n              builder: (context) {\n                return SearchAuthorScreen(\n                  category: category,\n                  author: widget.author,\n                );\n              },\n            ));\n          }\n        },\n        icon: const Icon(Icons.category),\n      );\n\n  Future<ComicsPage> _fetch(String _currentSort, int _currentPage) {\n    if (currentPagerAction() == PagerAction.CONTROLLER &&\n        _comicListController.selecting) {\n      setState(() {\n        _comicListController.selecting = false;\n      });\n    }\n    if (widget.category == null) {\n      return method.comics(\n        _currentSort,\n        _currentPage,\n        author: widget.author,\n      );\n    } else {\n      return method.comics(\n        _currentSort,\n        _currentPage,\n        author: widget.author,\n        category: widget.category!,\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    var a = Scaffold(\n      appBar: _comicListController.selecting\n          ? downAppBar(context, _comicListController, setState)\n          : _searchBar.build(context),\n      body: ComicPager(\n        fetchPage: _fetch,\n        comicListController: _comicListController,\n      ),\n    );\n    return WillPopScope(\n      onWillPop: () async {\n        if (_comicListController.selecting) {\n          setState(() {\n            _comicListController.selecting = false;\n          });\n          return false;\n        }\n        return true;\n      },\n      child: a,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/SearchScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport '../basic/config/PagerAction.dart';\nimport 'components/flutter_search_bar.dart' as fsb;\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/store/Categories.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\nimport '../basic/Entities.dart';\nimport '../basic/config/Address.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'components/ComicList.dart';\nimport 'components/ComicPager.dart';\nimport 'components/Common.dart';\nimport 'components/GoDownloadSelect.dart';\n\n// 搜索页面\nclass SearchScreen extends StatefulWidget {\n  final String keyword;\n  final String? category;\n\n  const SearchScreen({\n    Key? key,\n    required this.keyword,\n    this.category,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _SearchScreenState();\n}\n\nclass _SearchScreenState extends State<SearchScreen> {\n  late final _comicListController = ComicListController();\n  late final TextEditingController _textEditController =\n      TextEditingController(text: widget.keyword);\n  late final fsb.SearchBar _searchBar = fsb.SearchBar(\n    hintText: '${tr('screen.search.search_hint')} ${categoryTitle(widget.category)}',\n    controller: _textEditController,\n    inBar: false,\n    setState: setState,\n    onSubmitted: (value) {\n      if (value.isNotEmpty) {\n        Navigator.pushReplacement(\n          context,\n          mixRoute(\n            builder: (context) => SearchScreen(\n              keyword: value,\n              category: widget.category,\n            ),\n          ),\n        );\n      }\n    },\n    buildDefaultAppBar: (BuildContext context) {\n      return AppBar(\n        title: Text(\"${categoryTitle(widget.category)} ${widget.keyword}\"),\n        actions: [\n          commonPopMenu(\n            context,\n            setState: setState,\n            comicListController: _comicListController,\n          ),\n          addressPopMenu(context),\n          _chooseCategoryAction(),\n          _searchBar.getSearchAction(context),\n        ],\n      );\n    },\n  );\n\n  Widget _chooseCategoryAction() => IconButton(\n        onPressed: () async {\n          String? category = await chooseListDialog(context, tr('screen.search.choose_category'), [\n            categoryTitle(null),\n            ...filteredList(\n              storedCategories,\n              (c) => !shadowCategories.contains(c),\n            ),\n          ]);\n          if (category != null) {\n            if (category == categoryTitle(null)) {\n              category = null;\n            }\n            Navigator.of(context).pushReplacement(mixRoute(\n              builder: (context) {\n                return SearchScreen(\n                  category: category,\n                  keyword: widget.keyword,\n                );\n              },\n            ));\n          }\n        },\n        icon: const Icon(Icons.category),\n      );\n\n  Future<ComicsPage> _fetch(String _currentSort, int _currentPage) {\n    if (currentPagerAction() == PagerAction.CONTROLLER &&\n        _comicListController.selecting) {\n      setState(() {\n        _comicListController.selecting = false;\n      });\n    }\n    if (widget.category == null) {\n      return method.searchComics(widget.keyword, _currentSort, _currentPage);\n    } else {\n      return method.searchComicsInCategories(\n        widget.keyword,\n        _currentSort,\n        _currentPage,\n        [widget.category!],\n      );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    var a = Scaffold(\n      appBar: _comicListController.selecting\n          ? downAppBar(context, _comicListController, setState)\n          : _searchBar.build(context),\n      body: ComicPager(\n        fetchPage: _fetch,\n        comicListController: _comicListController,\n      ),\n    );\n    return WillPopScope(\n      onWillPop: () async {\n        if (_comicListController.selecting) {\n          setState(() {\n            _comicListController.selecting = false;\n          });\n          return false;\n        }\n        return true;\n      },\n      child: a,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/SettingsScreen.dart",
    "content": "import 'dart:io';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/AndroidDisplayMode.dart';\nimport 'package:pikapika/basic/config/AndroidSecureFlag.dart';\nimport 'package:pikapika/basic/config/AutoClean.dart';\nimport 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart';\nimport 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart';\nimport 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/AutoFullScreenOnForward.dart';\nimport 'package:pikapika/basic/config/ChooserRoot.dart';\nimport 'package:pikapika/basic/config/ContentFailedReloadAction.dart';\nimport 'package:pikapika/basic/config/CopySkipConfirm.dart';\nimport 'package:pikapika/basic/config/DownloadAndExportPath.dart';\nimport 'package:pikapika/basic/config/DownloadThreadCount.dart';\nimport 'package:pikapika/basic/config/EBookScrollingRange.dart';\nimport 'package:pikapika/basic/config/EBookScrollingTrigger.dart';\nimport 'package:pikapika/basic/config/ExportRename.dart';\nimport 'package:pikapika/basic/config/FullScreenAction.dart';\nimport 'package:pikapika/basic/config/FullScreenUI.dart';\nimport 'package:pikapika/basic/config/HiddenSearchPersion.dart';\nimport 'package:pikapika/basic/config/IconLoading.dart';\nimport 'package:pikapika/basic/config/IgnoreUpgradeConfirm.dart';\nimport 'package:pikapika/basic/config/IsPro.dart';\nimport 'package:pikapika/basic/config/KeyboardController.dart';\nimport 'package:pikapika/basic/config/NoAnimation.dart';\nimport 'package:pikapika/basic/config/DragRegionLock.dart';\nimport 'package:pikapika/basic/config/GestureSpeed.dart';\nimport 'package:pikapika/basic/config/PagerAction.dart';\nimport 'package:pikapika/basic/config/Quality.dart';\nimport 'package:pikapika/basic/config/ReaderBackgroundColor.dart';\nimport 'package:pikapika/basic/config/ReaderDirection.dart';\nimport 'package:pikapika/basic/config/ReaderSliderPosition.dart';\nimport 'package:pikapika/basic/config/ReaderType.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesMode.dart';\nimport 'package:pikapika/basic/config/ShowCommentAtDownload.dart';\nimport 'package:pikapika/basic/config/Themes.dart';\nimport 'package:pikapika/basic/config/TimeOffsetHour.dart';\nimport 'package:pikapika/basic/config/VolumeController.dart';\nimport 'package:pikapika/basic/config/VolumeNextChapter.dart';\nimport 'package:pikapika/screens/components/NetworkSetting.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\n\nimport '../basic/config/AppOrientation.dart';\nimport '../basic/config/Authentication.dart';\nimport '../basic/config/CategoriesColumnCount.dart';\nimport '../basic/config/CategoriesSort.dart';\nimport '../basic/config/CopyFullName.dart';\nimport '../basic/config/CopyFullNameTemplate.dart';\nimport '../basic/config/DownloadCachePath.dart';\nimport '../basic/config/EBookScrolling.dart';\nimport '../basic/config/HiddenFdIcon.dart';\nimport '../basic/config/HiddenSubIcon.dart';\nimport '../basic/config/HiddenViewed.dart';\nimport '../basic/config/HiddenWords.dart';\nimport '../basic/config/HideOnlineFavorite.dart';\nimport '../basic/config/IgnoreInfoHistory.dart';\nimport '../basic/config/ImageFilter.dart';\nimport '../basic/config/ReaderScrollByScreenPercentage.dart';\nimport '../basic/config/WebToonScrollMode.dart';\nimport '../basic/config/ReaderZoomScale.dart';\nimport '../basic/config/ReaderTwoPageDirection.dart';\nimport '../basic/config/StartupPic.dart';\nimport '../basic/config/ThreeKeepRight.dart';\nimport '../basic/config/TimeoutLock.dart';\nimport '../basic/config/UsingRightClickPop.dart';\nimport '../basic/config/WebDav.dart';\nimport '../basic/config/WillPopNotice.dart';\nimport '../basic/config/i18n.dart';\nimport 'CleanScreen.dart';\nimport 'MigrateScreen.dart';\nimport 'ModifyPasswordScreen.dart';\nimport 'ThemeScreen.dart';\nimport 'WebServerScreen.dart';\n\nclass SettingsScreen extends StatefulWidget {\n  final bool hiddenAccountInfo;\n\n  const SettingsScreen({Key? key, this.hiddenAccountInfo = false})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _SettingsScreenState();\n}\n\nclass _SettingsScreenState extends State<SettingsScreen> {\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('settings.settings')),\n      ),\n      body: SingleChildScrollView(\n        padding: const EdgeInsets.all(16),\n        child: Column(\n          children: [\n            ExpansionTile(\n              leading: const Icon(Icons.ad_units),\n              title: Text(tr('settings.interface')),\n              children: [\n                const Divider(),\n                ...themeWidgets(context, setState),\n                appOrientationWidget(),\n                const Divider(),\n                pagerActionSetting(),\n                contentFailedReloadActionSetting(),\n                willPopNoticeSetting(),\n                categoriesColumnCountSetting(),\n                categoriesSortSetting(),\n                const Divider(),\n                setStartupPicTile(context),\n                clearStartupPicTile(context),\n                const Divider(),\n                languageListTile(),\n                timeZoneSetting(),\n                fontSetting(),\n                fullScreenUISetting(),\n                usingRightClickPopSetting(),\n                hiddenFdIconSetting(),\n                hiddenSubIconSetting(),\n                hiddenSearchPersionSetting(),\n                const Divider(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.lan),\n              title: Text(tr('settings.network')),\n              children: const [\n                Divider(),\n                NetworkSetting(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.dangerous),\n              title: Text(tr('settings.seal')),\n              children: [\n                const Divider(),\n                shadowCategoriesModeSetting(),\n                shadowCategoriesSetting(),\n                hiddenWordsSetting(),\n                hiddenViewedSetting(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.open_in_browser),\n              title: Text(tr('settings.interaction')),\n              children: [\n                const Divider(),\n                copySkipConfirmSetting(),\n                copyFullNameSetting(),\n                copyFullNameTemplateSetting(),\n                const Divider(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.menu_book_outlined),\n              title: Text(tr('settings.reading')),\n              children: [\n                const Divider(),\n                qualitySetting(),\n                readerTypeSettings(),\n                readerTwoPageDirectionSetting(),\n                readerDirectionSetting(),\n                readerSliderPositionSetting(),\n                autoFullScreenSetting(),\n                autoFullScreenOnForwardSetting(),\n                fullScreenActionSetting(),\n                webToonScrollModeSetting(),\n                readerScrollByScreenPercentageSetting(),\n                readerZoomMinScaleSetting(),\n                readerZoomMaxScaleSetting(),\n                readerZoomDoubleTapScaleSetting(),\n                dragRegionLockSetting(),\n                gestureSpeedSetting(),\n                const Divider(),\n                volumeControllerSetting(),\n                volumeNextChapterSetting(),\n                keyboardControllerSetting(),\n                const Divider(),\n                noAnimationSetting(),\n                readerBackgroundColorSetting(),\n                const Divider(),\n                threeKeepRightSetting(),\n                ignoreInfoHistorySetting(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.download),\n              title: Text(tr('settings.download')),\n              children: [\n                const Divider(),\n                autoDownloadOnFavoriteSetting(),\n                disableAutoDownloadOnMobileSetting(),\n                autoDeleteDownloadOnUnfavoriteSetting(),\n                const Divider(),\n                ListTile(\n                  title: Text(tr('settings.web_server')),\n                  subtitle: Text(tr('settings.web_server_subtitle')),\n                  onTap: () {\n                    Navigator.of(context).push(\n                      mixRoute(\n                        builder: (BuildContext context) =>\n                            const WebServerScreen(),\n                      ),\n                    );\n                  },\n                ),\n                const Divider(),\n                chooserRootSetting(),\n                downloadThreadCountSetting(),\n                downloadAndExportPathSetting(),\n                showCommentAtDownloadSetting(),\n                exportRenameSetting(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.backup),\n              title: Text(tr('settings.sync')),\n              children: [\n                const Divider(),\n                ...webDavSettings(context),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.manage_accounts),\n              title: Text(tr('settings.account')),\n              children: [\n                const Divider(),\n                widget.hiddenAccountInfo\n                    ? Container()\n                    : ListTile(\n                        onTap: () async {\n                          Navigator.push(\n                            context,\n                            mixRoute(\n                              builder: (context) =>\n                                  const ModifyPasswordScreen(),\n                            ),\n                          );\n                        },\n                        title: Text(tr('settings.modify_password')),\n                      ),\n                const Divider(),\n                useLocalFavoriteSetting(),\n                const Divider(),\n                hideOnlineFavoriteSetting(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.chrome_reader_mode),\n              title: Text(tr('settings.ebook')),\n              children: [\n                const Divider(),\n                iconLoadingSetting(),\n                eBookScrollingSetting(),\n                eBookScrollingRangeSetting(),\n                eBookScrollingTriggerSetting(),\n                imageFilterSetting(),\n                const Divider(),\n              ],\n            ),\n            ExpansionTile(\n              leading: const Icon(Icons.ad_units),\n              title: Text(tr('settings.system')),\n              children: [\n                const Divider(),\n                androidDisplayModeSetting(),\n                androidSecureFlagSetting(),\n                authenticationSetting(),\n                lockTimeOutSecSetting(),\n                lockTimeOutSecNotice(),\n                const Divider(),\n                autoCleanSecSetting(),\n                ListTile(\n                  onTap: () {\n                    Navigator.push(\n                      context,\n                      mixRoute(builder: (context) => const CleanScreen()),\n                    );\n                  },\n                  title: Text(tr('settings.clear_cache')),\n                ),\n                const Divider(),\n                migrate(context),\n                const Divider(),\n                downloadCachePathSetting(),\n                importViewLogFromOff(),\n                const Divider(),\n                ignoreUpgradeConfirmSetting(),\n              ],\n            ),\n            SafeArea(\n              top: false,\n              child: Container(),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget migrate(BuildContext context) {\n    if (Platform.isAndroid) {\n      return ListTile(\n        title: Text(\n          tr('settings.migrate') + (!isPro ? \"(${tr('settings.app.pro')})\" : \"\"),\n          style: TextStyle(\n            color: !isPro ? Colors.grey : null,\n          ),\n        ),\n        subtitle: Text(tr('settings.migrate_subtitle')),\n        onTap: () async {\n          if (!isPro) {\n            defaultToast(context, tr('app.pro_required'));\n            return;\n          }\n          var f =\n              await confirmDialog(context, tr('settings.migrate'), tr('settings.migrate_confirm'));\n          if (f) {\n            Navigator.of(context).pushAndRemoveUntil(\n              mixRoute(builder: (BuildContext context) {\n                return const MigrateScreen();\n              }),\n              (route) => false,\n            );\n          }\n        },\n      );\n    }\n    return Container();\n  }\n}\n\nclass _IconAndWidgets {\n  final IconData icon;\n  final List<Widget> widgets;\n\n  _IconAndWidgets(this.icon, this.widgets);\n}\n"
  },
  {
    "path": "lib/screens/SpaceScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/config/HideOnlineFavorite.dart';\nimport 'package:pikapika/basic/config/HiddenFdIcon.dart';\nimport 'package:pikapika/basic/config/Version.dart';\nimport 'package:pikapika/screens/AboutScreen.dart';\nimport 'package:pikapika/screens/AccountScreen.dart';\nimport 'package:pikapika/screens/DownloadListScreen.dart';\nimport 'package:pikapika/screens/FavouritePaperScreen.dart';\nimport 'package:pikapika/screens/LocalFavoriteScreen.dart';\nimport 'package:pikapika/screens/ProScreen.dart';\nimport 'package:pikapika/screens/ViewLogsScreen.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/WebDav.dart';\nimport 'package:pikapika/screens/components/RecommendLinksPanel.dart';\n\nimport '../basic/config/IconLoading.dart';\nimport '../basic/config/IsPro.dart';\nimport 'SettingsScreen.dart';\nimport 'components/Badge.dart';\nimport 'components/UserProfileCard.dart';\n\n// 个人空间页面\nclass SpaceScreen extends StatefulWidget {\n  const SpaceScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _SpaceScreenState();\n}\n\nclass _SpaceScreenState extends State<SpaceScreen> {\n  @override\n  void initState() {\n    versionEvent.subscribe(_onEvent);\n    proEvent.subscribe(_onEvent);\n    hiddenFdIconEvent.subscribe(_onEvent);\n    useLocalFavoriteEvent.subscribe(_onEvent);\n    hideOnlineFavoriteEvent.subscribe(_onEvent);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    versionEvent.unsubscribe(_onEvent);\n    proEvent.unsubscribe(_onEvent);\n    hiddenFdIconEvent.unsubscribe(_onEvent);\n    useLocalFavoriteEvent.unsubscribe(_onEvent);\n    hideOnlineFavoriteEvent.unsubscribe(_onEvent);\n    super.dispose();\n  }\n\n  void _onEvent(dynamic a) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.space.title')),\n        actions: [\n          IconButton(\n            onPressed: () async {\n              bool result = await confirmDialog(context,\n                  tr('screen.space.logout'), tr('screen.space.logout_confirm'));\n              if (result) {\n                await method.clearToken();\n                await method.setPassword(\"\");\n                Navigator.pushReplacement(\n                  context,\n                  mixRoute(builder: (context) => const AccountScreen()),\n                );\n              }\n            },\n            icon: const Icon(Icons.exit_to_app),\n          ),\n          IconButton(\n            onPressed: () {\n              Navigator.push(\n                context,\n                mixRoute(builder: (context) => const AboutScreen()),\n              );\n            },\n            icon: Badged(\n              child: const Icon(Icons.info_outline),\n              badge: latestVersion() == null ? null : \"1\",\n            ),\n          ),\n          ...hiddenFdIcon\n              ? []\n              : [\n                  IconButton(\n                    onPressed: () {\n                      Navigator.of(context)\n                          .push(mixRoute(builder: (BuildContext context) {\n                        return const ProScreen();\n                      }));\n                    },\n                    icon: Icon(\n                      isPro ? Icons.offline_bolt : Icons.offline_bolt_outlined,\n                    ),\n                  ),\n                ],\n          IconButton(\n            onPressed: () {\n              Navigator.push(\n                context,\n                mixRoute(builder: (context) => const SettingsScreen()),\n              );\n            },\n            icon: const Icon(Icons.settings),\n          ),\n        ],\n      ),\n      body: ListView(\n        children: [\n          const Divider(),\n          const UserProfileCard(),\n          const Divider(),\n          if (!hideOnlineFavorite) ...[\n            ListTile(\n              onTap: () {\n                Navigator.push(\n                  context,\n                  mixRoute(builder: (context) => const FavouritePaperScreen()),\n                );\n              },\n              title: Text(tr('screen.space.my_favourites')),\n            ),\n            const Divider(),\n          ],\n          if (useLocalFavorite) ...[\n            ListTile(\n              onTap: () {\n                Navigator.push(\n                  context,\n                  mixRoute(builder: (context) => const LocalFavoriteScreen()),\n                );\n              },\n              title: Text(tr('local_favorite.title')),\n            ),\n            const Divider(),\n          ],\n          ListTile(\n            onTap: () {\n              Navigator.push(\n                context,\n                mixRoute(builder: (context) => const ViewLogsScreen()),\n              );\n            },\n            title: Text(tr('screen.space.view_history')),\n          ),\n          const Divider(),\n          ListTile(\n            onTap: () {\n              Navigator.push(\n                context,\n                mixRoute(builder: (context) => const DownloadListScreen()),\n              );\n            },\n            title: Text(tr('screen.space.my_downloads')),\n          ),\n          const Divider(),\n          const RecommendLinksPanel(\n            padding: EdgeInsets.fromLTRB(16, 0, 16, 16),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/ThemeScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/Platform.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../basic/config/Themes.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass ThemeScreen extends StatefulWidget {\n  const ThemeScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ThemeScreenState();\n}\n\nclass _ThemeScreenState extends State<ThemeScreen> {\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(tr('screen.theme.title'))),\n      body: PikaListView(\n        children: themeWidgets(context, setState),\n      ),\n    );\n  }\n}\n\nList<Widget> themeWidgets(\n    BuildContext context, void Function(VoidCallback fn) setState) {\n  return [\n    ListTile(\n      onTap: () async {\n        await chooseLightTheme(context);\n        setState(() {});\n      },\n      title: Text(tr('screen.theme.theme')),\n      subtitle: Text(currentLightThemeName()),\n    ),\n    ...androidNightModeDisplay\n        ? [\n            SwitchListTile(\n                title: Text(tr('screen.theme.dark_mode_different_theme')),\n                value: androidNightMode,\n                onChanged: (value) async {\n                  await setAndroidNightMode(value);\n                  setState(() {});\n                }),\n          ]\n        : [],\n    ...androidNightModeDisplay && androidNightMode\n        ? [\n            ListTile(\n              onTap: () async {\n                await chooseDarkTheme(context);\n                setState(() {});\n              },\n              title: Text(tr('screen.theme.dark_mode_theme')),\n              subtitle: Text(currentDarkThemeName()),\n            ),\n          ]\n        : [],\n    // ...androidVersion > 0 ? [enableStatusBarColorSetting()] : [],\n  ];\n}\n"
  },
  {
    "path": "lib/screens/ViewLogsScreen.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/ComicInfoCard.dart';\nimport 'package:pikapika/screens/components/RightClickPop.dart';\n\nimport '../basic/Entities.dart';\nimport '../basic/config/IconLoading.dart';\nimport 'ComicInfoScreen.dart';\nimport 'components/Images.dart';\nimport 'components/ListView.dart';\n\n// 浏览记录\nclass ViewLogsScreen extends StatefulWidget {\n  const ViewLogsScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ViewLogsScreenState();\n}\n\nclass _ViewLogsScreenState extends State<ViewLogsScreen> {\n  static const _pageSize = 24;\n  static const _scrollPhysics = AlwaysScrollableScrollPhysics(); // 即使不足一页仍可滚动\n\n  final _scrollController = ScrollController();\n  final _comicList = <ViewLog>[];\n\n  var _isLoading = false; // 是否加载中\n  var _scrollOvered = false; // 滚动到最后\n  var _offset = 0;\n\n  var _inSelection = false; // 是否进入选择模式\n  var _selectedList = <String>[]; // 选择列表\n\n  Future _clearAll() async {\n    if (await confirmDialog(\n      context,\n      tr('screen.view_logs.clear_all'),\n      tr('screen.view_logs.clear_all_desc'),\n    )) {\n      await method.clearAllViewLog();\n      setState(() {\n        _comicList.clear();\n        _isLoading = false;\n        _scrollOvered = true;\n        _offset = 0;\n      });\n    }\n  }\n\n  Future _deleteSelected() async {\n    if (_selectedList.isNotEmpty) {\n      var confirm = await confirmDialog(\n        context,\n        tr('screen.view_logs.clear_selected'),\n        tr('screen.view_logs.clear_selected_desc'),\n      );\n      if (!confirm) {\n        return;\n      }\n    }\n    if (_selectedList.isNotEmpty) {\n      await method.deleteViewLog(_selectedList.join(','));\n    }\n    setState(() {\n      _inSelection = false;\n      _selectedList.clear();\n      _comicList.clear();\n      _isLoading = false;\n      _scrollOvered = true;\n      _offset = 0;\n    });\n    _loadPage();\n  }\n\n  Future _viewSelected() async {\n    if (_selectedList.isNotEmpty) {\n      await method.viewComic(_selectedList.join(','));\n    }\n    setState(() {\n      _inSelection = false;\n      _selectedList.clear();\n      _comicList.clear();\n      _isLoading = false;\n      _scrollOvered = true;\n      _offset = 0;\n    });\n    _loadPage();\n  }\n\n  Future _clearOnce(String id) async {\n    if (await confirmDialog(\n      context,\n      tr('screen.view_logs.clear_one'),\n      tr('screen.view_logs.clear_one_desc'),\n    )) {\n      await method.deleteViewLog(id);\n      setState(() {\n        for (var i = 0; i < _comicList.length; i++) {\n          if (_comicList[i].id == id) {\n            _comicList.removeAt(i);\n            _offset--;\n            break;\n          }\n        }\n      });\n    }\n  }\n\n  // 加载一页\n  Future<dynamic> _loadPage() async {\n    setState(() {\n      _isLoading = true;\n    });\n    try {\n      var page = await method.viewLogPage(_offset, _pageSize);\n      if (page.isEmpty) {\n        _scrollOvered = true;\n      } else {\n        _comicList.addAll(page);\n      }\n      _offset += _pageSize;\n    } finally {\n      setState(() {\n        _isLoading = false;\n      });\n    }\n  }\n\n  // 滚动事件\n  void _handScroll() {\n    if (_scrollController.position.pixels +\n            MediaQuery.of(context).size.height / 2 <\n        _scrollController.position.maxScrollExtent) {\n      return;\n    }\n    if (_isLoading || _scrollOvered || _inSelection) return;\n    _loadPage();\n  }\n\n  @override\n  void initState() {\n    _loadPage();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var entries = _comicList.map((e) {\n      Widget card = InkWell(\n        onTap: () {\n          if (_inSelection) {\n            if (_selectedList.contains(e.id)) {\n              _selectedList.remove(e.id);\n            } else {\n              _selectedList.add(e.id);\n            }\n          } else {\n            _chooseComic(e.id);\n          }\n          setState(() {});\n        },\n        onLongPress: () {\n          if (_inSelection) {\n            if (_selectedList.contains(e.id)) {\n              _selectedList.remove(e.id);\n            } else {\n              _selectedList.add(e.id);\n            }\n          } else {\n            _clearOnce(e.id);\n          }\n          setState(() {});\n        },\n        child: ViewInfoCard(\n          fileServer: e.thumbFileServer,\n          author: e.author,\n          categories: _decodeCate(e.categories),\n          path: e.thumbPath,\n          title: e.title,\n        ),\n      );\n      if (_inSelection) {\n        card = Stack(\n          children: [\n            card,\n            Positioned(\n              top: 10,\n              right: 10,\n              child: Icon(\n                _selectedList.contains(e.id)\n                    ? Icons.check_box\n                    : Icons.check_box_outline_blank,\n                color: _selectedList.contains(e.id)\n                    ? Theme.of(context).colorScheme.primary\n                    : null,\n              ),\n            ),\n          ],\n        );\n      }\n      return card;\n    });\n\n    final screen = NotificationListener(\n      child: Scaffold(\n        appBar: AppBar(\n          title: Text(tr('screen.view_logs.title')),\n          actions: [\n            ..._inSelection\n                ? [\n                    IconButton(\n                      onPressed: () {\n                        setState(() {\n                          _inSelection = false;\n                          _selectedList.clear();\n                        });\n                      },\n                      icon: Icon(Icons.cancel),\n                    ),\n                    IconButton(\n                      onPressed: _viewSelected,\n                      icon: const Icon(Icons.move_up),\n                    ),\n                    IconButton(\n                      onPressed: _deleteSelected,\n                      icon: const Icon(Icons.delete),\n                    )\n                  ]\n                : [\n                    IconButton(\n                      icon: const Icon(Icons.rule),\n                      onPressed: () {\n                        setState(() {\n                          _inSelection = !_inSelection;\n                          _selectedList.clear();\n                        });\n                      },\n                    ),\n                    IconButton(\n                        onPressed: _clearAll,\n                        icon: const Icon(Icons.auto_delete)),\n                  ],\n          ],\n        ),\n        body: PikaListView(\n          physics: _scrollPhysics,\n          controller: _scrollController,\n          children: entries.toList(),\n        ),\n      ),\n      onNotification: (scrollNotification) {\n        if (scrollNotification is ScrollStartNotification) {\n          _handScroll();\n        }\n        return true;\n      },\n    );\n    return rightClickPop(\n      child: WillPopScope(\n        onWillPop: () async {\n          if (_inSelection) {\n            setState(() {\n              _inSelection = false;\n              _selectedList.clear();\n            });\n            return false;\n          }\n          return true;\n        },\n        child: screen,\n      ),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  void _chooseComic(String comicId) {\n    Navigator.push(\n      context,\n      mixRoute(\n        builder: (context) => ComicInfoScreen(\n          comicId: comicId,\n        ),\n      ),\n    );\n  }\n\n  List<String> _decodeCate(String categories) {\n    try {\n      var decode = jsonDecode(categories);\n      if (decode is List) {\n        return List.of(decode).cast();\n      }\n      return [decode];\n    } catch (e) {\n      return [categories];\n    }\n  }\n}\n\nclass ViewInfoCard extends StatelessWidget {\n  final String fileServer;\n  final String path;\n  final String title;\n  final String author;\n  final List<String> categories;\n\n  const ViewInfoCard({\n    Key? key,\n    required this.fileServer,\n    required this.path,\n    required this.title,\n    required this.author,\n    required this.categories,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.only(right: 10),\n            child: RemoteImage(\n              key: Key(\"$fileServer:$path\"),\n              fileServer: fileServer,\n              path: path,\n              width: imageWidth,\n              height: imageHeight,\n            ),\n          ),\n          Expanded(\n            child: Row(\n              children: [\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Text(title, style: titleStyle),\n                      Container(height: 5),\n                      Text(author, style: authorStyle),\n                      Container(height: 5),\n                      Text.rich(\n                        TextSpan(\n                            text:\n                                \"${tr('screen.view_logs.categories')} : ${categories.join(' ')}\"),\n                        style: TextStyle(\n                          fontSize: 13,\n                          color: Theme.of(context)\n                              .textTheme\n                              .bodyText1!\n                              .color!\n                              .withAlpha(0xCC),\n                        ),\n                      ),\n                      Container(height: 5),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/WebServerScreen.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/i18.dart';\n\nimport '../basic/Method.dart';\nimport 'components/ContentError.dart';\nimport 'components/ContentLoading.dart';\nimport 'components/ListView.dart';\nimport 'components/RightClickPop.dart';\n\nclass WebServerScreen extends StatefulWidget {\n  const WebServerScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _WebServerScreenState();\n}\n\nclass _WebServerScreenState extends State<WebServerScreen> {\n  late final Future<String> _ipFuture = method.clientIpSet();\n  late Future _future = method.startWebServer();\n\n  @override\n  void dispose() {\n    method.stopWebServer();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return rightClickPop(\n      child: buildScreen(context),\n      context: context,\n      canPop: true,\n    );\n  }\n\n  Widget buildScreen(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(tr('screen.web_server.title')),\n      ),\n      body: FutureBuilder(\n        future: _future,\n        builder: (BuildContext context, AsyncSnapshot snapshot) {\n          if (snapshot.hasError) {\n            return ContentError(\n                error: snapshot.error,\n                stackTrace: snapshot.stackTrace,\n                onRefresh: () async {\n                  setState(() {\n                    _future = method.startWebServer();\n                  });\n                });\n          }\n          if (snapshot.connectionState != ConnectionState.done) {\n            return ContentLoading(label: tr('app.loading'));\n          }\n          return PikaListView(\n            children: [\n              Container(\n                padding: const EdgeInsets.all(8),\n                child: Column(\n                  children: [\n                    FutureBuilder(\n                      future: _ipFuture,\n                      builder: (BuildContext context,\n                          AsyncSnapshot<String> snapshot) {\n                        if (snapshot.hasError) {\n                          return Text(tr('screen.web_server.get_ip_failed'));\n                        }\n                        if (snapshot.connectionState != ConnectionState.done) {\n                          return Text(tr('screen.web_server.getting_ip'));\n                        }\n                        return Text('${snapshot.data}');\n                      },\n                    ),\n                    Text(tr('screen.web_server.port')),\n                    const Text(''),\n                    Text(tr('screen.web_server.usage_instruction')),\n                    const Text(''),\n                    Text(tr('screen.web_server.leave_notice')),\n                  ],\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/calculator_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport '../basic/config/passed.dart';\nimport 'CloseAppScreen.dart';\n\nclass CalculatorScreen extends StatelessWidget {\n  const CalculatorScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) => Scaffold(\n        body: Container(\n          padding: const EdgeInsets.only(top: 10.0, left: 10.0),\n          color: Colors.black,\n          child: const ContentBody(),\n        ),\n      );\n}\n\nclass ContentBody extends StatefulWidget {\n  const ContentBody({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => ContentBodyState();\n}\n\nclass ContentBodyState extends State<ContentBody> {\n  String sums = '0';\n  String total = '0';\n  String flag = '';\n  bool isDouble = false;\n  int tag = 0;\n  List list = [\n    {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'},\n    {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'},\n    {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'},\n    {'bgc': '0xFFFF9800', 'color': '0xFFFFFFFFF'},\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        Expanded(child: Container()),\n        Container(\n          padding: const EdgeInsets.only(\n              top: 10.0, left: 10.0, right: 20.0, bottom: 10.0),\n          child: Container(\n            width: 750,\n            alignment: Alignment.bottomRight,\n            child: Text(\n              sums,\n              maxLines: 8,\n              style: const TextStyle(fontSize: 33, color: Colors.white),\n            ),\n          ),\n        ),\n        Column(\n          children: [\n            Center(\n              child: Row(\n                children: [\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(45),\n                        color: Colors.grey,\n                        splashColor: Colors.white,\n                        onPressed: () {\n                          btnclick('重置');\n                        },\n                        child: const Text('AC',\n                            style: TextStyle(\n                                color: Colors.black, fontSize: 20)),\n                        shape: const CircleBorder(\n                          side: BorderSide(color: Colors.grey),\n                        ),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(45),\n                        color: Colors.grey,\n                        splashColor: Colors.white,\n                        onPressed: () {\n                          btnclick('加/减');\n                        },\n                        child: const Text('+/-',\n                            style: TextStyle(\n                                color: Colors.black, fontSize: 20)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Colors.grey)),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(40),\n                        color: Colors.grey,\n                        splashColor: Colors.white,\n                        onPressed: () {\n                          btnclick('百分号');\n                        },\n                        child: const Text('%',\n                            style: TextStyle(\n                                color: Colors.black, fontSize: 25)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Colors.grey)),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(34),\n                        color: Color(int.parse(list[0]['bgc'])),\n                        splashColor: Color(int.parse(list[0]['bgc'])),\n                        onPressed: () {\n                          btnclick('除');\n                        },\n                        child: Text('÷',\n                            style: TextStyle(\n                                color:\n                                Color(int.parse(list[0]['color'])),\n                                fontSize: 30)),\n                        shape: CircleBorder(\n                            side: BorderSide(\n                                color:\n                                Color(int.parse(list[0]['bgc'])))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                ],\n              ),\n            ),\n            Center(\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceAround,\n                children: [\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('7');\n                        },\n                        child: const Text('7',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('8');\n                        },\n                        child: const Text('8',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('9');\n                        },\n                        child: const Text('9',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(34),\n                        color: Color(int.parse(list[1]['bgc'])),\n                        splashColor: Color(int.parse(list[1]['bgc'])),\n                        onPressed: () {\n                          btnclick('乘');\n                        },\n                        child: Text('×',\n                            style: TextStyle(\n                                color:\n                                Color(int.parse(list[1]['color'])),\n                                fontSize: 30)),\n                        shape: CircleBorder(\n                            side: BorderSide(\n                                color:\n                                Color(int.parse(list[1]['bgc'])))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                ],\n              ),\n            ),\n            Center(\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceAround,\n                children: [\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('4');\n                        },\n                        child: const Text('4',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('5');\n                        },\n                        child: const Text('5',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('6');\n                        },\n                        child: const Text('6',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(34),\n                        color: Color(int.parse(list[2]['bgc'])),\n                        splashColor: Color(int.parse(list[2]['bgc'])),\n                        onPressed: () {\n                          btnclick('减');\n                        },\n                        child: Text('—',\n                            style: TextStyle(\n                                color:\n                                Color(int.parse(list[2]['color'])),\n                                fontSize: 30)),\n                        shape: CircleBorder(\n                            side: BorderSide(\n                                color:\n                                Color(int.parse(list[2]['bgc'])))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                ],\n              ),\n            ),\n            Center(\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceAround,\n                children: [\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('1');\n                        },\n                        child: const Text('1',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('2');\n                        },\n                        child: const Text('2',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(36),\n                        color: const Color(0xFF3B3B3B),\n                        splashColor: Colors.grey,\n                        onPressed: () {\n                          numClick('3');\n                        },\n                        child: const Text('3',\n                            style: TextStyle(\n                                color: Colors.white, fontSize: 30)),\n                        shape: const CircleBorder(\n                            side: BorderSide(color: Color(0xFF3B3B3B))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                  Expanded(\n                    child: Container(\n                      child: MaterialButton(\n                        padding:\n                        EdgeInsets.all(34),\n                        color: Color(int.parse(list[3]['bgc'])),\n                        splashColor: Color(int.parse(list[3]['bgc'])),\n                        onPressed: () {\n                          btnclick('加');\n                        },\n                        child: Text('+',\n                            style: TextStyle(\n                                color:\n                                Color(int.parse(list[3]['color'])),\n                                fontSize: 30)),\n                        shape: CircleBorder(\n                            side: BorderSide(\n                                color:\n                                Color(int.parse(list[3]['bgc'])))),\n                      ),\n                      alignment: Alignment.center,\n                    ),\n                    flex: 1,\n                  ),\n                ],\n              ),\n            ),\n            Center(\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceAround,\n                children: [\n                  Container(\n                    child: MaterialButton(\n                      padding: const EdgeInsets.only(\n                          left: 70.0,\n                          top: 20.0,\n                          bottom: 20.0,\n                          right: 76.0),\n                      color: const Color(0xFF3B3B3B),\n                      splashColor: Colors.grey,\n                      onPressed: () {\n                        numClick('0');\n                      },\n                      child: const Text('0',\n                          style: TextStyle(\n                              color: Colors.white, fontSize: 30)),\n                      shape: RoundedRectangleBorder(\n                          borderRadius: BorderRadius.circular(40)),\n                    ),\n                    margin: const EdgeInsets.only(left: 10.0),\n                    alignment: Alignment.center,\n                  ),\n                  Container(\n                    child: MaterialButton(\n                      padding:\n                      EdgeInsets.all(36),\n                      color: const Color(0xFF3B3B3B),\n                      splashColor: Colors.grey,\n                      onPressed: () {\n                        numClick('.');\n                      },\n                      child: const Text('.',\n                          style: TextStyle(\n                              color: Colors.white, fontSize: 30)),\n                      shape: const CircleBorder(\n                          side: BorderSide(color: Color(0xFF3B3B3B))),\n                    ),\n                    alignment: Alignment.center,\n                  ),\n                  Container(\n                    child: MaterialButton(\n                      padding:\n                      EdgeInsets.all(34),\n                      color: Colors.orange,\n                      splashColor: Colors.orange,\n                      onPressed: () {\n                        btnclick('等于');\n                      },\n                      child: const Text('=',\n                          style: TextStyle(\n                              color: Colors.white, fontSize: 30)),\n                      shape: const CircleBorder(\n                          side: BorderSide(color: Colors.orange)),\n                    ),\n                    alignment: Alignment.center,\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n        Expanded(child: Container()),\n      ],\n    );\n  }\n\n  numClick(e) {\n    if (sums == '0') {\n      if (e == '.') {\n        setState(() {\n          isDouble = true;\n          sums += e;\n        });\n      } else {\n        setState(() {\n          sums = e;\n        });\n      }\n    } else {\n      if (flag != '') {\n        if (tag == 0) {\n          if (sums.length < 20) {\n            setState(() {\n              sums += e;\n            });\n          }\n        } else {\n          setState(() {\n            sums = e;\n            tag = 0;\n          });\n        }\n      } else {\n        if (sums.length < 20) {\n          setState(() {\n            sums += e;\n          });\n        }\n      }\n    }\n  }\n\n//  计算点击\n  btnclick(e) {\n    if (sums == \"77554422\") {\n      firstPassed().then((value) {\n        Navigator.pushReplacement(context, MaterialPageRoute(\n          builder: (BuildContext context) {\n            return const CloseAppScreen();\n          },\n        ));\n      });\n    }\n    for (var element in list) {\n      element['color'] = '0xFFFFFFFFF';\n      element['bgc'] = '0xFFFF9800';\n    }\n    switch (e) {\n      case '重置':\n        setState(() {\n          sums = '0';\n          tag = 0;\n          flag = '';\n        });\n        break;\n      case '加':\n        setState(() {\n          total = sums;\n          tag = 1;\n          flag = '加';\n          list[3]['bgc'] = '0xFFFFFFFFF';\n          list[3]['color'] = '0xFFFF9800';\n        });\n        break;\n      case '减':\n        setState(() {\n          total = sums;\n          tag = 1;\n          flag = '减';\n          list[2]['bgc'] = '0xFFFFFFFFF';\n          list[2]['color'] = '0xFFFF9800';\n        });\n        break;\n      case '乘':\n        setState(() {\n          total = sums;\n          tag = 1;\n          flag = '乘';\n          list[1]['bgc'] = '0xFFFFFFFFF';\n          list[1]['color'] = '0xFFFF9800';\n        });\n        break;\n      case '除':\n        setState(() {\n          total = sums;\n          tag = 1;\n          flag = '除';\n          list[0]['bgc'] = '0xFFFFFFFFF';\n          list[0]['color'] = '0xFFFF9800';\n        });\n        break;\n      case '百分号':\n        setState(() {\n          total = sums;\n          tag = 1;\n          flag = '百分号';\n          sums = (int.parse(sums) / 100).toString();\n          isDouble = true;\n        });\n        break;\n      case '等于':\n        sumClac();\n        setState(() {\n          tag = 1;\n          flag = 'true';\n        });\n        clacVlaue();\n    }\n  }\n\n// 计算函数\n  sumClac() {\n    if (flag == '加') {\n      if (isDouble) {\n        double c = double.parse(total) + double.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      } else {\n        int c = int.parse(total) + int.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      }\n      setState(() {\n        total = '';\n        isDouble = false;\n        flag = '';\n      });\n    } else if (flag == '减') {\n      if (isDouble) {\n        double c = double.parse(total) - double.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      } else {\n        int c = int.parse(total) - int.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      }\n      setState(() {\n        total = '';\n        flag = '';\n        isDouble = false;\n      });\n    } else if (flag == '乘') {\n      if (isDouble) {\n        double c = double.parse(total) * double.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      } else {\n        int c = int.parse(total) * int.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      }\n      setState(() {\n        total = '';\n        flag = '';\n        isDouble = false;\n      });\n    } else if (flag == '除') {\n      if (isDouble) {\n        double c = double.parse(total) * double.parse(sums);\n        setState(() {\n          sums = c.toString();\n        });\n      } else {\n        double c = int.parse(total) / int.parse(sums);\n        if (int.parse(total) % int.parse(sums) == 0) {\n          setState(() {\n            sums = c.toInt().toString();\n          });\n        } else {\n          setState(() {\n            sums = c.toString();\n          });\n        }\n      }\n      setState(() {\n        total = '';\n        flag = '';\n        isDouble = false;\n      });\n    }\n  }\n\n//  判断计算值\n  clacVlaue() {\n//    if(sums.length >10){\n//      setState(() {\n//        sums = sums.substring(0, 10);\n//      });\n//    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/Avatar.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'Images.dart';\n\nconst double _avatarMargin = 5;\nconst double _avatarBorderSize = 1.5;\n\n// 头像\nclass Avatar extends StatelessWidget {\n  final RemoteImageInfo avatarImage;\n  final double size;\n\n  const Avatar(this.avatarImage, {this.size = 50, Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    return Container(\n      margin: const EdgeInsets.all(_avatarMargin),\n      decoration: BoxDecoration(\n          shape: BoxShape.circle,\n          border: Border.all(\n            color: theme.colorScheme.secondary,\n            style: BorderStyle.solid,\n            width: _avatarBorderSize,\n          )),\n      child: ClipRRect(\n        borderRadius: BorderRadius.all(Radius.circular(this.size)),\n        child: RemoteImage(\n          fileServer: this.avatarImage.fileServer,\n          path: this.avatarImage.path,\n          width: this.size,\n          height: this.size,\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/Badge.dart",
    "content": "import 'package:flutter/material.dart';\n\n// 提示信息, 组件右上角的小红点\nclass Badged extends StatelessWidget {\n  final String? badge;\n  final Widget child;\n\n  const Badged({Key? key, required this.child, this.badge}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    if (badge == null) {\n      return child;\n    }\n    return Stack(\n      children: [\n        child,\n        Positioned(\n          right: 0,\n          child: Container(\n            padding: const EdgeInsets.all(1),\n            decoration: BoxDecoration(\n              color: Colors.red,\n              borderRadius: BorderRadius.circular(6),\n            ),\n            constraints: const BoxConstraints(\n              minWidth: 12,\n              minHeight: 12,\n            ),\n            child: Text(\n              badge!,\n              style: const TextStyle(\n                color: Colors.white,\n                fontSize: 8,\n              ),\n              textAlign: TextAlign.center,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/BottomSheetInput.dart",
    "content": "import 'dart:async';\n\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\n\nFuture showInputModalBottomSheet({\n  required BuildContext context,\n  required FutureOr<dynamic> Function(String) onSubmitted,\n  required String? hintText,\n  String? initialValue,\n}) async {\n  Navigator.of(context).push(\n    PageRouteBuilder(\n      opaque: false,\n      pageBuilder: (context, _, __) {\n        return BottomSheetInput(\n          onSubmitted: onSubmitted,\n          hintText: hintText,\n          initialValue: initialValue,\n        );\n      },\n    ),\n  );\n}\n\nclass BottomSheetInput extends StatefulWidget {\n  final FutureOr<dynamic> Function(String) onSubmitted;\n  final String? hintText;\n  final String? initialValue;\n\n  const BottomSheetInput({\n    Key? key,\n    required this.onSubmitted,\n    required this.hintText,\n    this.initialValue,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _BottomSheetInputState();\n}\n\nclass _BottomSheetInputState extends State<BottomSheetInput> {\n  late TextEditingController _controller;\n  bool submitting = false;\n\n  @override\n  void initState() {\n    _controller = TextEditingController(text: widget.initialValue);\n    super.initState();\n  }\n\n  @override\n  dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var mq = MediaQuery.of(context);\n    var size = mq.size;\n    return SizedBox(\n      width: size.width,\n      height: size.height,\n      child: Material(\n        color: Colors.black.withAlpha(50),\n        child: Column(\n          children: [\n            SafeArea(\n              bottom: false,\n              child: GestureDetector(\n                behavior: HitTestBehavior.translucent,\n                onTap: () {\n                  if (!submitting) Navigator.of(context).pop();\n                },\n                child: Container(),\n              ),\n            ),\n            Material(\n              child: Padding(\n                padding: const EdgeInsets.only(\n                  left: 20,\n                  right: 20,\n                  top: 30,\n                  bottom: 30,\n                ),\n                child: Column(\n                  children: [\n                    if (submitting)\n                      const Center(\n                        child: CircularProgressIndicator(),\n                      ),\n                    if (!submitting) _buildTextField(),\n                  ],\n                ),\n              ),\n            ),\n            Expanded(\n              child: GestureDetector(\n                behavior: HitTestBehavior.translucent,\n                onTap: () {\n                  if (!submitting) Navigator.of(context).pop();\n                },\n                child: Container(),\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n/*\nColumn(\n        children: [\n          SafeArea(\n            child: Container(),\n            bottom: false,\n          ),\n          Material(\n            child: Column(\n              children: [\n                Container(\n                  padding: const EdgeInsets.only(\n                    left: 20,\n                    right: 20,\n                    top: 30,\n                    bottom: 30,\n                  ),\n                  child: Column(\n                    children: [\n                      if (submitting)\n                        const Center(\n                          child: CircularProgressIndicator(),\n                        ),\n                      if (!submitting) _buildTextField(),\n                    ],\n                  ),\n                ),\n                Expanded(child: Container()),\n              ],\n            ),\n          ),\n          Expanded(\n            child: GestureDetector(\n              behavior: HitTestBehavior.translucent,\n              onTap: () {\n                if (!submitting) Navigator.of(context).pop();\n              },\n              child: Container(),\n            ),\n          ),\n        ],\n      ),\n      */\n\n  Widget _buildTextField() {\n    return TextField(\n      controller: _controller,\n      autofocus: true,\n      decoration: InputDecoration(\n        hintText: widget.hintText,\n        border: InputBorder.none,\n        contentPadding: const EdgeInsets.all(16),\n      ),\n      onSubmitted: (text) {\n        _onSubmitted(text);\n      },\n    );\n  }\n\n  _onSubmitted(String text) async {\n    setState(() {\n      submitting = true;\n    });\n    try {\n      await widget.onSubmitted(text);\n      Navigator.of(context).pop();\n    } catch (e, s) {\n      defaultToast(context, e.toString());\n    } finally {\n      setState(() {\n        submitting = false;\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ComicDescriptionCard.dart",
    "content": "import 'package:flutter/material.dart';\n\n// 漫画的说明\nclass ComicDescriptionCard extends StatelessWidget {\n  final String description;\n\n  const ComicDescriptionCard({Key? key, required this.description})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    return Container(\n      padding: const EdgeInsets.only(\n        top: 5,\n        bottom: 5,\n        left: 10,\n        right: 10,\n      ),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: SelectableText(description, style: _categoriesStyle),\n    );\n  }\n}\n\nconst _categoriesStyle = TextStyle(\n  fontSize: 13,\n  color: Colors.grey,\n);\n"
  },
  {
    "path": "lib/screens/components/ComicInfoCard.dart",
    "content": "import 'dart:convert';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/AutoDeleteDownloadOnUnfavorite.dart';\nimport 'package:pikapika/basic/config/AutoDownloadOnFavorite.dart';\nimport 'package:pikapika/basic/config/CopyFullName.dart';\nimport 'package:pikapika/basic/config/CopyFullNameTemplate.dart';\nimport 'package:pikapika/basic/config/DisableAutoDownloadOnMobile.dart';\nimport 'package:pikapika/basic/config/HideOnlineFavorite.dart';\nimport 'package:pikapika/basic/config/IsPro.dart';\nimport 'package:pikapika/basic/config/WebDav.dart';\nimport 'package:pikapika/screens/SearchAuthorScreen.dart';\nimport 'package:pikapika/basic/Navigator.dart';\nimport '../ComicsScreen.dart';\nimport 'Images.dart';\nimport 'package:pikapika/basic/connect.dart';\n\n// 漫画卡片\nclass ComicInfoCard extends StatefulWidget {\n  final bool linkItem;\n  final ComicSimple info;\n  final bool viewed;\n\n  const ComicInfoCard(\n    this.info, {\n    Key? key,\n    this.linkItem = false,\n    this.viewed = false,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicInfoCard();\n}\n\nclass _ComicInfoCard extends State<ComicInfoCard> {\n  bool _favouriteLoading = false;\n  bool _likeLoading = false;\n  bool _localFavoriteLoading = false;\n  LocalFavoriteComic? _localFavoriteComic;\n\n  @override\n  void initState() {\n    super.initState();\n    if (useLocalFavorite) {\n      _loadLocalFavoriteStatus();\n    }\n  }\n\n  Future<void> _loadLocalFavoriteStatus() async {\n    try {\n      _localFavoriteComic = await method.getLocalFavoriteComic(widget.info.id);\n      if (mounted) {\n        setState(() {});\n      }\n    } catch (e) {\n      print(\"Load local favorite status error: $e\");\n    }\n  }\n\n  String _encodeComicSimpleInfo(ComicSimple info) {\n    return jsonEncode({\n      \"_id\": info.id,\n      \"title\": info.title,\n      \"author\": info.author,\n      \"pagesCount\": info.pagesCount,\n      \"epsCount\": info.epsCount,\n      \"finished\": info.finished,\n      \"categories\": info.categories,\n      \"likesCount\": info.likesCount,\n      \"thumb\": {\n        \"originalName\": info.thumb.originalName,\n        \"fileServer\": info.thumb.fileServer,\n        \"path\": info.thumb.path,\n      },\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var info = widget.info;\n    var theme = Theme.of(context);\n    var view = info is ComicInfo ? info.viewsCount : 0;\n    bool? like = info is ComicInfo ? info.isLiked : null;\n    bool? favourite = (hideOnlineFavorite || info is! ComicInfo) ? null : info.isFavourite;\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.only(right: 10),\n            child: RemoteImage(\n              fileServer: info.thumb.fileServer,\n              path: info.thumb.path,\n              width: imageWidth,\n              height: imageHeight,\n            ),\n          ),\n          Expanded(\n            child: Row(\n              children: [\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      widget.linkItem\n                          ? GestureDetector(\n                              onLongPress: () {\n                                if (copyFullName()) {\n                                  var fin = copyFullNameTemplate()\n                                      .replaceAll(\"{title}\", info.title)\n                                      .replaceAll(\"{author}\", info.author);\n                                  if (fin.isEmpty) {\n                                    fin = info.title;\n                                  }\n                                  confirmCopy(\n                                    context,\n                                    fin,\n                                  );\n                                } else {\n                                  confirmCopy(context, info.title);\n                                }\n                              },\n                              child: Text(info.title, style: titleStyle),\n                            )\n                          : Text(info.title, style: titleStyle),\n                      Container(height: 5),\n                      widget.linkItem\n                          ? InkWell(\n                              onTap: () {\n                                navPushOrReplace(\n                                    context,\n                                    (context) => SearchAuthorScreen(\n                                        author: info.author));\n                              },\n                              onLongPress: () {\n                                confirmCopy(context, info.author);\n                              },\n                              child: Text(info.author, style: authorStyle),\n                            )\n                          : Text(info.author, style: authorStyle),\n                      Container(height: 5),\n                      Text.rich(\n                        widget.linkItem\n                            ? TextSpan(\n                                children: [\n                                  TextSpan(\n                                      text:\n                                          '${tr('components.comic_info_card.categories')} :'),\n                                  ...info.categories.map(\n                                    (e) => TextSpan(\n                                      children: [\n                                        const TextSpan(text: ' '),\n                                        TextSpan(\n                                          text: e,\n                                          recognizer: TapGestureRecognizer()\n                                            ..onTap = () => navPushOrReplace(\n                                                  context,\n                                                  (context) => ComicsScreen(\n                                                    category: e,\n                                                  ),\n                                                ),\n                                        ),\n                                      ],\n                                    ),\n                                  ),\n                                ],\n                              )\n                            : TextSpan(\n                                text:\n                                    \"${tr('components.comic_info_card.categories')} : ${info.categories.join(' ')}\"),\n                        style: TextStyle(\n                          fontSize: 13,\n                          color: Theme.of(context)\n                              .textTheme\n                              .bodyText1!\n                              .color!\n                              .withAlpha(0xCC),\n                        ),\n                      ),\n                      Container(height: 5),\n                      Wrap(\n                        crossAxisAlignment: WrapCrossAlignment.center,\n                        runSpacing: 5,\n                        children: [\n                          ...info.likesCount > 0\n                              ? [\n                                  iconFavorite,\n                                  iconSpacing,\n                                  Text(\n                                    '${info.likesCount}',\n                                    style: iconLabelStyle,\n                                    strutStyle: iconLabelStrutStyle,\n                                  ),\n                                  iconMargin,\n                                ]\n                              : [],\n                          ...(view > 0\n                              ? [\n                                  iconVisibility,\n                                  iconSpacing,\n                                  Text(\n                                    '$view',\n                                    style: iconLabelStyle,\n                                    strutStyle: iconLabelStrutStyle,\n                                  ),\n                                  iconMargin,\n                                ]\n                              : []),\n                          ...(info.epsCount > 0\n                              ? [\n                                  Text.rich(TextSpan(children: [\n                                    const WidgetSpan(child: iconPage),\n                                    WidgetSpan(child: iconSpacing),\n                                    WidgetSpan(\n                                        child: Text(\n                                      \"${info.epsCount}E / ${info.pagesCount}P\",\n                                      style: countLabelStyle,\n                                      strutStyle: iconLabelStrutStyle,\n                                      softWrap: false,\n                                    )),\n                                    WidgetSpan(child: iconMargin),\n                                  ])),\n                                ]\n                              : []),\n                          iconMargin,\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n                SizedBox(\n                  height: imageHeight,\n                  child: Column(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.center,\n                    children: [\n                      buildFinished(info.finished),\n                      ...buildViewed(widget.viewed),\n                      Expanded(child: Container()),\n                      ...(like == null\n                          ? []\n                          : [\n                              Container(height: 10),\n                              SizedBox(\n                                height: 26,\n                                child: _likeLoading\n                                    ? IconButton(\n                                        color: Colors.pink[400],\n                                        onPressed: () {},\n                                        icon: const Icon(\n                                          Icons.sync,\n                                        ),\n                                      )\n                                    : IconButton(\n                                        color: Colors.pink[400],\n                                        onPressed: _changeLike,\n                                        icon: Icon(\n                                          like\n                                              ? Icons.favorite\n                                              : Icons.favorite_border,\n                                        ),\n                                      ),\n                              ),\n                            ]),\n                      ...((!useLocalFavorite && favourite == null)\n                          ? []\n                          : [\n                              Container(height: 10),\n                              SizedBox(\n                                height: 26,\n                                child: Row(\n                                  mainAxisSize: MainAxisSize.min,\n                                  children: [\n                                    if (useLocalFavorite) ...[\n                                      _localFavoriteLoading\n                                          ? IconButton(\n                                              color: Colors.pink[400],\n                                              padding: EdgeInsets.zero,\n                                              constraints: const BoxConstraints(),\n                                              onPressed: () {},\n                                              icon: const Icon(Icons.sync),\n                                            )\n                                          : IconButton(\n                                              color: Colors.pink[400],\n                                              padding: EdgeInsets.zero,\n                                              constraints: const BoxConstraints(),\n                                              onPressed: _changeLocalFavorite,\n                                              icon: Icon(\n                                                _localFavoriteComic != null\n                                                    ? Icons.folder_special\n                                                    : Icons.folder_open,\n                                              ),\n                                            ),\n                                    ],\n                                    if (useLocalFavorite && favourite != null)\n                                      const SizedBox(width: 8),\n                                    if (favourite != null)\n                                      _favouriteLoading\n                                          ? IconButton(\n                                              color: Colors.pink[400],\n                                              padding: EdgeInsets.zero,\n                                              constraints: const BoxConstraints(),\n                                              onPressed: () {},\n                                              icon: const Icon(Icons.sync),\n                                            )\n                                          : IconButton(\n                                              color: Colors.pink[400],\n                                              padding: EdgeInsets.zero,\n                                              constraints: const BoxConstraints(),\n                                              onPressed: _changeFavourite,\n                                              icon: Icon(\n                                                favourite\n                                                    ? Icons.bookmark\n                                                    : Icons.bookmark_border,\n                                              ),\n                                            ),\n                                  ],\n                                ),\n                              ),\n                            ]),\n                      Container(height: 10),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Future _changeFavourite() async {\n    setState(() {\n      _favouriteLoading = true;\n    });\n    try {\n      var rst = await method.switchFavourite(widget.info.id);\n      final isNowFavourite = !rst.startsWith(\"un\");\n      setState(() {\n        (widget.info as ComicInfo).isFavourite = isNowFavourite;\n      });\n      if (isNowFavourite) {\n        await _maybeAutoDownloadOnFavorite();\n      } else {\n        await _maybeAutoDeleteOnUnfavorite();\n      }\n    } finally {\n      setState(() {\n        _favouriteLoading = false;\n      });\n    }\n  }\n\n  Future<void> _maybeAutoDownloadOnFavorite() async {\n    if (!autoDownloadOnFavorite()) {\n      return;\n    }\n    if (!isPro) {\n      defaultToast(context, tr('app.pro_required'));\n      return;\n    }\n    try {\n      if (disableAutoDownloadOnMobile() && await isMobileNetwork()) {\n        return;\n      }\n      await method.downloadAll([widget.info.id]);\n    } catch (e) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.download_failed'))),\n        );\n      }\n    }\n  }\n\n  Future<void> _maybeAutoDeleteOnUnfavorite() async {\n    if (!autoDeleteDownloadOnUnfavorite()) {\n      return;\n    }\n    try {\n      await method.deleteDownloadComic(widget.info.id);\n    } catch (e) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.remove_failed'))),\n        );\n      }\n    }\n  }\n\n  Future _changeLike() async {\n    setState(() {\n      _likeLoading = true;\n    });\n    try {\n      var rst = await method.switchLike(widget.info.id);\n      setState(() {\n        (widget.info as ComicInfo).isLiked = !rst.startsWith(\"un\");\n      });\n    } finally {\n      setState(() {\n        _likeLoading = false;\n      });\n    }\n  }\n\n  Future<void> _changeLocalFavorite() async {\n    if (_localFavoriteComic != null) {\n      // 已收藏，显示确认删除对话框\n      bool? confirm = await showDialog<bool>(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n            title: Text(tr('local_favorite.remove_confirm_title')),\n            content: Text(tr('local_favorite.remove_confirm_content')),\n            actions: <Widget>[\n              TextButton(\n                onPressed: () {\n                  Navigator.of(context).pop(false);\n                },\n                child: Text(tr('app.cancel')),\n              ),\n              TextButton(\n                onPressed: () {\n                  Navigator.of(context).pop(true);\n                },\n                child: Text(tr('app.confirm')),\n              ),\n            ],\n          );\n        },\n      );\n\n      if (confirm == true) {\n        setState(() {\n          _localFavoriteLoading = true;\n        });\n        try {\n          await method.removeLocalFavoriteComic(widget.info.id);\n          setState(() {\n            _localFavoriteComic = null;\n          });\n        } catch (e) {\n          if (mounted) {\n            ScaffoldMessenger.of(context).showSnackBar(\n              SnackBar(content: Text(tr('local_favorite.remove_failed'))),\n            );\n          }\n        } finally {\n          setState(() {\n            _localFavoriteLoading = false;\n          });\n        }\n      }\n    } else {\n      // 未收藏，显示文件夹选择对话框\n      await _showFolderSelector();\n    }\n  }\n\n  Future<void> _showFolderSelector() async {\n    setState(() {\n      _localFavoriteLoading = true;\n    });\n\n    try {\n      List<LocalFavoriteFolder> folders = await method.listLocalFavoriteFolders();\n      int folderCount = await method.countLocalFavoriteFolders();\n\n      if (!mounted) return;\n\n      setState(() {\n        _localFavoriteLoading = false;\n      });\n\n      // 显示文件夹选择对话框\n      String? selectedFolderId = await showDialog<String>(\n        context: context,\n        builder: (BuildContext context) {\n          return AlertDialog(\n            title: Text(tr('local_favorite.select_folder')),\n            content: SizedBox(\n              width: double.maxFinite,\n              child: Column(\n                mainAxisSize: MainAxisSize.min,\n                children: [\n                  ListTile(\n                    leading: const Icon(Icons.folder_special),\n                    title: Text(tr('local_favorite.all_folders')),\n                    onTap: () {\n                      Navigator.of(context).pop('__ALL__');\n                    },\n                  ),\n                  const Divider(),\n                  if (folders.isEmpty)\n                    Padding(\n                      padding: const EdgeInsets.all(16.0),\n                      child: Text(tr('local_favorite.no_folders')),\n                    )\n                  else\n                    ListView.builder(\n                      shrinkWrap: true,\n                      itemCount: folders.length,\n                      itemBuilder: (context, index) {\n                        final folder = folders[index];\n                        return ListTile(\n                          leading: const Icon(Icons.folder),\n                          title: Text(folder.name),\n                          onTap: () {\n                            Navigator.of(context).pop(folder.id);\n                          },\n                        );\n                      },\n                    ),\n                  const Divider(),\n                  ListTile(\n                    leading: Icon(\n                      Icons.create_new_folder,\n                      color: (isPro || folderCount < 3) ? null : Colors.grey,\n                    ),\n                    title: Text(\n                      tr('local_favorite.new_folder') +\n                          (isPro || folderCount < 3 ? \"\" : \" (${tr('app.pro')})\"),\n                      style: TextStyle(\n                        color: (isPro || folderCount < 3) ? null : Colors.grey,\n                      ),\n                    ),\n                    onTap: () async {\n                      if (!isPro && folderCount >= 3) {\n                        Navigator.of(context).pop();\n                        ScaffoldMessenger.of(context).showSnackBar(\n                          SnackBar(\n                            content: Text(tr('local_favorite.folder_limit_reached')),\n                          ),\n                        );\n                        return;\n                      }\n                      Navigator.of(context).pop('__CREATE_NEW__');\n                    },\n                  ),\n                ],\n              ),\n            ),\n            actions: <Widget>[\n              TextButton(\n                onPressed: () {\n                  Navigator.of(context).pop();\n                },\n                child: Text(tr('app.cancel')),\n              ),\n            ],\n          );\n        },\n      );\n\n      if (selectedFolderId == '__ALL__') {\n        await _addToFolder(\"\");\n      } else if (selectedFolderId == '__CREATE_NEW__') {\n        // 创建新文件夹\n        await _createNewFolder();\n      } else if (selectedFolderId != null) {\n        // 添加到选中的文件夹\n        await _addToFolder(selectedFolderId);\n      }\n    } catch (e) {\n      if (mounted) {\n        setState(() {\n          _localFavoriteLoading = false;\n        });\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.load_failed'))),\n        );\n      }\n    }\n  }\n\n  Future<void> _createNewFolder() async {\n    String? folderName = await showDialog<String>(\n      context: context,\n      builder: (BuildContext context) {\n        final controller = TextEditingController();\n        return AlertDialog(\n          title: Text(tr('local_favorite.new_folder')),\n          content: TextField(\n            controller: controller,\n            decoration: InputDecoration(\n              hintText: tr('local_favorite.folder_name'),\n            ),\n            autofocus: true,\n          ),\n          actions: <Widget>[\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop();\n              },\n              child: Text(tr('app.cancel')),\n            ),\n            TextButton(\n              onPressed: () {\n                Navigator.of(context).pop(controller.text.trim());\n              },\n              child: Text(tr('app.confirm')),\n            ),\n          ],\n        );\n      },\n    );\n\n    if (folderName != null && folderName.isNotEmpty) {\n      setState(() {\n        _localFavoriteLoading = true;\n      });\n\n      try {\n        LocalFavoriteFolder folder = await method.createLocalFavoriteFolder(folderName);\n        await _addToFolder(folder.id);\n      } catch (e) {\n        if (mounted) {\n          setState(() {\n            _localFavoriteLoading = false;\n          });\n          ScaffoldMessenger.of(context).showSnackBar(\n            SnackBar(content: Text(tr('local_favorite.create_folder_failed'))),\n          );\n        }\n      }\n    }\n  }\n\n  Future<void> _addToFolder(String folderId) async {\n    setState(() {\n      _localFavoriteLoading = true;\n    });\n\n    try {\n      await method.addLocalFavoriteComic(\n        widget.info.id,\n        folderId,\n        info: _encodeComicSimpleInfo(widget.info),\n      );\n      await _loadLocalFavoriteStatus();\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.add_success'))),\n        );\n      }\n    } catch (e) {\n      if (mounted) {\n        ScaffoldMessenger.of(context).showSnackBar(\n          SnackBar(content: Text(tr('local_favorite.add_failed'))),\n        );\n      }\n    } finally {\n      if (mounted) {\n        setState(() {\n          _localFavoriteLoading = false;\n        });\n      }\n    }\n  }\n}\n\ndouble imageWidth = 210 / 3.15;\ndouble imageHeight = 315 / 3.15;\n\nWidget buildFinished(bool comicFinished) {\n  if (comicFinished) {\n    return Container(\n      padding: const EdgeInsets.only(left: 8, right: 8),\n      decoration: BoxDecoration(\n        color: Colors.orange.shade800,\n        borderRadius: BorderRadius.circular(30),\n      ),\n      child: Text(\n        tr('components.comic_info_card.finished'),\n        style: const TextStyle(\n          fontSize: 10,\n          fontWeight: FontWeight.bold,\n          color: Colors.white,\n          height: 1.2,\n        ),\n        strutStyle: const StrutStyle(\n          height: 1.2,\n        ),\n      ),\n    );\n  }\n  return Container();\n}\n\nList<Widget> buildViewed(viewed) {\n  if (!viewed) {\n    return [];\n  }\n  return [\n    Container(height: 5),\n    Container(\n      padding: const EdgeInsets.only(left: 8, right: 8),\n      decoration: BoxDecoration(\n        color: Colors.yellow.shade800,\n        borderRadius: BorderRadius.circular(30),\n      ),\n      child: Text(\n        tr('components.comic_info_card.viewed'),\n        style: const TextStyle(\n          fontSize: 10,\n          fontWeight: FontWeight.bold,\n          color: Colors.white,\n          height: 1.2,\n        ),\n        strutStyle: const StrutStyle(\n          height: 1.2,\n        ),\n      ),\n    ),\n  ];\n}\n\nconst double _iconSize = 15;\n\nfinal iconFavorite =\n    Icon(Icons.favorite, size: _iconSize, color: Colors.pink[400]);\nfinal iconDownload =\n    Icon(Icons.download_rounded, size: _iconSize, color: Colors.pink[400]);\nfinal iconVisibility =\n    Icon(Icons.visibility, size: _iconSize, color: Colors.pink[400]);\n\nfinal iconLabelStyle = TextStyle(\n  fontSize: 13,\n  color: Colors.pink.shade400,\n  height: 1.2,\n);\nconst iconLabelStrutStyle = StrutStyle(\n  height: 1.2,\n);\n\nconst iconPage =\n    Icon(Icons.ballot_outlined, size: _iconSize, color: Colors.grey);\nconst countLabelStyle = TextStyle(\n  fontSize: 13,\n  color: Colors.grey,\n  height: 1.2,\n);\n\nfinal iconMargin = Container(width: 20);\nfinal iconSpacing = Container(width: 5);\n\nconst titleStyle = TextStyle(fontWeight: FontWeight.bold);\nfinal authorStyle = TextStyle(\n  fontSize: 13,\n  color: Colors.pink.shade300,\n);\n\nfinal authorStyleX = TextStyle(\n  fontSize: 13,\n  color: Colors.pink.shade300.withOpacity(.7),\n);\n"
  },
  {
    "path": "lib/screens/components/ComicList.dart",
    "content": "import 'dart:math';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/HiddenWords.dart';\nimport 'package:pikapika/basic/config/ShadowCategories.dart';\nimport 'package:pikapika/basic/config/ListLayout.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesMode.dart';\nimport 'package:pikapika/screens/components/CommonData.dart';\n\nimport '../../basic/config/HiddenViewed.dart';\nimport 'ComicInfoCard.dart';\nimport 'Images.dart';\nimport 'LinkToComicInfo.dart';\nimport 'ListView.dart';\n\nclass ComicListController {\n  _ComicListState? _state;\n\n  bool get selecting => _state?._selecting ?? false;\n\n  set selecting(bool value) => _state?._setSelect(value);\n\n  List<String> get selected => _state?._selected ?? [];\n\n  selectAll() {\n    _state?._selectAll();\n  }\n\n  loadViewed() {\n    WidgetsBinding.instance?.addPostFrameCallback((_) {\n      _state?._loadViewed();\n    });\n  }\n}\n\n// 漫画列表页\nclass ComicList extends StatefulWidget {\n  final Widget? appendWidget;\n  final List<ComicSimple> comicList;\n  final ScrollController? scrollController;\n  final ComicListController? listController;\n\n  const ComicList(\n    this.comicList, {\n    this.appendWidget,\n    this.scrollController,\n    Key? key,\n    // required\n    this.listController,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicListState();\n}\n\nclass _ComicListState extends State<ComicList> {\n  final List<String> viewedList = [];\n  bool _selecting = false;\n  List<String> _selected = [];\n\n  _selectAll() {\n    setState(() {\n      if (_selected.length == widget.comicList.length) {\n        _selected.clear();\n      } else {\n        _selected.addAll(widget.comicList.map((e) => e.id));\n      }\n    });\n  }\n\n  _setSelect(bool value) {\n    setState(() {\n      _selected.clear();\n      _selecting = value;\n    });\n  }\n\n  Future _loadViewed() async {\n    if (widget.comicList.isNotEmpty) {\n      var list = await method\n          .loadViewedList(widget.comicList.map((e) => e.id).toList());\n      viewedList.clear();\n      viewedList.addAll(list);\n      setState(() {});\n    }\n  }\n\n  @override\n  void initState() {\n    widget.listController?._state = this;\n    _loadViewed();\n    listLayoutEvent.subscribe(_onLayoutChange);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (widget.listController?._state == this) {\n      widget.listController?._state = null;\n    }\n    listLayoutEvent.unsubscribe(_onLayoutChange);\n    super.dispose();\n  }\n\n  void _onLayoutChange(EventArgs? args) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    switch (currentLayout) {\n      case ListLayout.INFO_CARD:\n        return _buildInfoCardList();\n      case ListLayout.ONLY_IMAGE:\n        return _buildGridImageWarp();\n      case ListLayout.COVER_AND_TITLE:\n        return _buildGridImageTitleWarp();\n      default:\n        return Container();\n    }\n  }\n\n  Widget _buildInfoCardList() {\n    return PikaListView(\n      controller: widget.scrollController,\n      physics: const AlwaysScrollableScrollPhysics(),\n      children: [\n        ...widget.comicList.map((e) {\n          bool viewed = viewedList.contains(e.id);\n          if (hiddenViewed && viewed) {\n            return InkWell(\n              onTap: () {},\n              child: Container(\n                padding: const EdgeInsets.all(10),\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: BorderSide(\n                      color: Theme.of(context).dividerColor,\n                    ),\n                  ),\n                ),\n                child: Center(\n                  child: Text(\n                    tr('components.comic_info_card.viewed'),\n                    style: TextStyle(\n                      fontSize: 12,\n                      color: (Theme.of(context).textTheme.bodyText1?.color ??\n                              Colors.black)\n                          .withOpacity(.3),\n                    ),\n                  ),\n                ),\n              ),\n            );\n          }\n          late bool shadow;\n          X:\n          switch (currentShadowCategoriesMode()) {\n            case ShadowCategoriesMode.BLACK_LIST:\n              shadow = e.categories\n                  .map((c) => shadowCategories.contains(c))\n                  .reduce((value, element) => value || element);\n              break;\n            case ShadowCategoriesMode.WHITE_LIST:\n              for (var c in e.categories) {\n                if (shadowCategories.contains(c)) {\n                  shadow = false;\n                  break X;\n                }\n              }\n              shadow = true;\n              break;\n          }\n          if (!shadow) {\n            for (var value in hiddenWords) {\n              if (e.title.toLowerCase().contains(value.toLowerCase()) ||\n                  e.author.toLowerCase().contains(value.toLowerCase())) {\n                shadow = true;\n                break;\n              }\n            }\n          }\n          if (shadow) {\n            return InkWell(\n              onTap: () {},\n              child: Container(\n                padding: const EdgeInsets.all(10),\n                decoration: BoxDecoration(\n                  border: Border(\n                    bottom: BorderSide(\n                      color: Theme.of(context).dividerColor,\n                    ),\n                  ),\n                ),\n                child: Center(\n                  child: Text(\n                    tr('components.comic_list.shadow'),\n                    style: TextStyle(\n                      fontSize: 12,\n                      color: (Theme.of(context).textTheme.bodyText1?.color ??\n                              Colors.black)\n                          .withOpacity(.3),\n                    ),\n                  ),\n                ),\n              ),\n            );\n          }\n          if (_selecting) {\n            return GestureDetector(\n              onTap: () {\n                setState(() {\n                  if (_selected.contains(e.id)) {\n                    _selected.remove(e.id);\n                  } else {\n                    _selected.add(e.id);\n                  }\n                });\n              },\n              child: Stack(children: [\n                AbsorbPointer(\n                  child: ComicInfoCard(\n                    e,\n                    viewed: viewedList.contains(e.id),\n                  ),\n                ),\n                SizedBox(\n                  height: imageHeight,\n                  child: Align(\n                    alignment: Alignment.bottomRight,\n                    child: Container(\n                      margin: const EdgeInsets.only(top: 5),\n                      padding: const EdgeInsets.only(right: 10, left: 5),\n                      decoration: BoxDecoration(\n                        color: Colors.grey.shade500.withOpacity(.1),\n                        borderRadius: const BorderRadius.only(\n                          topLeft: Radius.circular(5),\n                          bottomLeft: Radius.circular(5),\n                        ),\n                      ),\n                      child: Padding(\n                        padding: const EdgeInsets.all(5),\n                        child: Icon(\n                          _selected.contains(e.id)\n                              ? Icons.check_circle_sharp\n                              : Icons.circle_outlined,\n                          color: Theme.of(context).colorScheme.secondary,\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ]),\n            );\n          }\n          Widget card = ComicInfoCard(\n            e,\n            viewed: viewedList.contains(e.id),\n          );\n          if (allSubscribed.containsKey(e.id)) {\n            final subscribed = allSubscribed[e.id]!;\n            if (subscribed.newEpCount > 0) {\n              card = Stack(\n                children: [\n                  card,\n                  Positioned(\n                    top: 0,\n                    left: 0,\n                    child: Container(\n                      padding: const EdgeInsets.all(2),\n                      decoration: const BoxDecoration(\n                        color: Colors.red,\n                        borderRadius: BorderRadius.only(\n                          bottomRight: Radius.circular(5),\n                        ),\n                      ),\n                      child: Text(\n                        subscribed.newEpCount.toString(),\n                        style: const TextStyle(\n                          color: Colors.white,\n                          fontSize: 12,\n                        ),\n                      ),\n                    ),\n                  ),\n                ],\n              );\n            }\n          }\n          return LinkToComicInfo(comicId: e.id, child: card);\n        }).toList(),\n        ...widget.appendWidget != null\n            ? [\n                SizedBox(\n                  height: 80,\n                  child: widget.appendWidget,\n                ),\n              ]\n            : [],\n      ],\n    );\n  }\n\n  Widget _buildGridImageWarp() {\n    var gap = 3.0;\n    var size = MediaQuery.of(context).size;\n    var min = size.width < size.height ? size.width : size.height;\n    var widthAndGap = min / 4;\n    int rowCap = size.width ~/ widthAndGap;\n    var width = widthAndGap - gap * 2;\n    var height = width * coverHeight / coverWidth;\n    List<Widget> wraps = [];\n    List<Widget> tmp = [];\n    for (var e in widget.comicList) {\n      late bool shadow;\n      X:\n      switch (currentShadowCategoriesMode()) {\n        case ShadowCategoriesMode.BLACK_LIST:\n          shadow = e.categories\n              .map((c) => shadowCategories.contains(c))\n              .reduce((value, element) => value || element);\n          break;\n        case ShadowCategoriesMode.WHITE_LIST:\n          for (var c in e.categories) {\n            if (shadowCategories.contains(c)) {\n              shadow = false;\n              break X;\n            }\n          }\n          shadow = true;\n          break;\n      }\n      if (!shadow) {\n        for (var value in hiddenWords) {\n          if (e.title.toLowerCase().contains(value.toLowerCase()) ||\n              e.author.toLowerCase().contains(value.toLowerCase())) {\n            shadow = true;\n            break;\n          }\n        }\n      }\n\n      bool viewed = viewedList.contains(e.id);\n      if (hiddenViewed && viewed) {\n        tmp.add(\n          Container(\n            padding: EdgeInsets.all(gap),\n            child: Container(\n              width: width,\n              height: height,\n              color:\n                  (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                      .withOpacity(.05),\n              child: Center(\n                child: Text(\n                  tr('components.comic_info_card.viewed'),\n                  textAlign: TextAlign.center,\n                  style: TextStyle(\n                    fontSize: 12,\n                    color: (Theme.of(context).textTheme.bodyText1?.color ??\n                            Colors.black)\n                        .withOpacity(.5),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      } else if (shadow) {\n        tmp.add(\n          Container(\n            padding: EdgeInsets.all(gap),\n            child: Container(\n              width: width,\n              height: height,\n              color:\n                  (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                      .withOpacity(.05),\n              child: Center(\n                child: Text(\n                  tr('components.comic_list.shadow'),\n                  textAlign: TextAlign.center,\n                  style: TextStyle(\n                    fontSize: 12,\n                    color: (Theme.of(context).textTheme.bodyText1?.color ??\n                            Colors.black)\n                        .withOpacity(.5),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      } else if (_selecting) {\n        Widget c = Container(\n          padding: EdgeInsets.all(gap),\n          child: RemoteImage(\n            fileServer: e.thumb.fileServer,\n            path: e.thumb.path,\n            width: width,\n            height: height,\n          ),\n        );\n        c = GestureDetector(\n          onTap: () {\n            setState(() {\n              if (_selected.contains(e.id)) {\n                _selected.remove(e.id);\n              } else {\n                _selected.add(e.id);\n              }\n            });\n          },\n          child: Stack(children: [\n            AbsorbPointer(\n              child: c,\n            ),\n            SizedBox(\n              width: width,\n              height: height,\n              child: Align(\n                alignment: Alignment.bottomRight,\n                child: Container(\n                  padding: const EdgeInsets.all(5),\n                  decoration: BoxDecoration(\n                    color: Colors.grey.shade800.withOpacity(.75),\n                    borderRadius: const BorderRadius.all(\n                      Radius.circular(5),\n                    ),\n                  ),\n                  child: Padding(\n                    padding: const EdgeInsets.all(5),\n                    child: Icon(\n                      _selected.contains(e.id)\n                          ? Icons.check_circle_sharp\n                          : Icons.circle_outlined,\n                      color: Theme.of(context).colorScheme.secondary,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ]),\n        );\n        tmp.add(c);\n      } else {\n        Widget c = LinkToComicInfo(\n          comicId: e.id,\n          child: Container(\n            padding: EdgeInsets.all(gap),\n            child: RemoteImage(\n              fileServer: e.thumb.fileServer,\n              path: e.thumb.path,\n              width: width,\n              height: height,\n            ),\n          ),\n        );\n        if (allSubscribed.containsKey(e.id)) {\n          final subscribed = allSubscribed[e.id]!;\n          if (subscribed.newEpCount > 0) {\n            c = Stack(\n              children: [\n                c,\n                Positioned(\n                  top: 0,\n                  left: 0,\n                  child: Container(\n                    padding: const EdgeInsets.all(2),\n                    decoration: const BoxDecoration(\n                      color: Colors.red,\n                      borderRadius: BorderRadius.only(\n                        bottomRight: Radius.circular(5),\n                      ),\n                    ),\n                    child: Text(\n                      subscribed.newEpCount.toString(),\n                      style: const TextStyle(\n                        color: Colors.white,\n                        fontSize: 12,\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            );\n          }\n        }\n        tmp.add(c);\n      }\n      if (tmp.length == rowCap) {\n        wraps.add(Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisAlignment: MainAxisAlignment.spaceAround,\n          children: tmp,\n        ));\n        tmp = [];\n      }\n    }\n    // 追加特殊按钮\n    if (widget.appendWidget != null) {\n      tmp.add(Container(\n        color:\n            (Theme.of(context).textTheme.bodyText1?.color ?? Colors.transparent)\n                .withOpacity(.1),\n        margin: EdgeInsets.only(\n          left: (rowCap - tmp.length) * gap,\n          right: (rowCap - tmp.length) * gap,\n          top: gap,\n          bottom: gap,\n        ),\n        width: (rowCap - tmp.length) * width,\n        height: height,\n        child: widget.appendWidget,\n      ));\n    }\n    // 最后一页没有下一页所有有可能为空\n    if (tmp.isNotEmpty) {\n      wraps.add(Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.spaceAround,\n        children: tmp,\n      ));\n      tmp = [];\n    }\n    // 返回\n    return PikaListView(\n      controller: widget.scrollController,\n      physics: const AlwaysScrollableScrollPhysics(),\n      padding: EdgeInsets.only(top: gap, bottom: gap),\n      children: wraps,\n    );\n  }\n\n  Widget _buildGridImageTitleWarp() {\n    var gap = 3.0;\n    var size = MediaQuery.of(context).size;\n    var min = size.width < size.height ? size.width : size.height;\n    var widthAndGap = min / 3;\n    int rowCap = size.width ~/ widthAndGap;\n    var width = widthAndGap - gap * 2;\n    var height = width * coverHeight / coverWidth;\n    double titleFontSize = max(width / 11, 10);\n    double shadowFontSize = max(width / 9, 12);\n    List<Widget> wraps = [];\n    List<Widget> tmp = [];\n    for (var e in widget.comicList) {\n      late bool shadow;\n      X:\n      switch (currentShadowCategoriesMode()) {\n        case ShadowCategoriesMode.BLACK_LIST:\n          shadow = e.categories\n              .map((c) => shadowCategories.contains(c))\n              .reduce((value, element) => value || element);\n          break;\n        case ShadowCategoriesMode.WHITE_LIST:\n          for (var c in e.categories) {\n            if (shadowCategories.contains(c)) {\n              shadow = false;\n              break X;\n            }\n          }\n          shadow = true;\n          break;\n      }\n      if (!shadow) {\n        for (var value in hiddenWords) {\n          if (e.title.toLowerCase().contains(value.toLowerCase()) ||\n              e.author.toLowerCase().contains(value.toLowerCase())) {\n            shadow = true;\n            break;\n          }\n        }\n      }\n      bool viewed = viewedList.contains(e.id);\n      if (hiddenViewed && viewed) {\n        tmp.add(\n          Container(\n            padding: EdgeInsets.all(gap),\n            child: Container(\n              width: width,\n              height: height,\n              color:\n                  (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                      .withOpacity(.05),\n              child: Center(\n                child: Text(\n                  tr('components.comic_info_card.viewed'),\n                  textAlign: TextAlign.center,\n                  style: TextStyle(\n                    fontSize: shadowFontSize,\n                    color: (Theme.of(context).textTheme.bodyText1?.color ??\n                            Colors.black)\n                        .withOpacity(.5),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      } else if (shadow) {\n        tmp.add(\n          Container(\n            padding: EdgeInsets.all(gap),\n            child: Container(\n              width: width,\n              height: height,\n              color:\n                  (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n                      .withOpacity(.05),\n              child: Center(\n                child: Text(\n                  tr('components.comic_list.shadow'),\n                  textAlign: TextAlign.center,\n                  style: TextStyle(\n                    fontSize: shadowFontSize,\n                    color: (Theme.of(context).textTheme.bodyText1?.color ??\n                            Colors.black)\n                        .withOpacity(.5),\n                  ),\n                ),\n              ),\n            ),\n          ),\n        );\n      } else if (_selecting) {\n        Widget c = Container(\n          margin: EdgeInsets.all(gap),\n          width: width,\n          height: height,\n          child: Stack(\n            children: [\n              RemoteImage(\n                fileServer: e.thumb.fileServer,\n                path: e.thumb.path,\n                width: width,\n                height: height,\n              ),\n              Align(\n                alignment: Alignment.bottomCenter,\n                child: Container(\n                  color: Colors.black.withOpacity(.3),\n                  child: Row(\n                    children: [\n                      Expanded(\n                        child: Text(\n                          e.title + '\\n',\n                          style: TextStyle(\n                            color: Colors.white,\n                            fontSize: titleFontSize,\n                            height: 1.2,\n                          ),\n                          strutStyle: const StrutStyle(height: 1.2),\n                          maxLines: 1,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n            ],\n          ),\n        );\n        c = GestureDetector(\n          onTap: () {\n            setState(() {\n              if (_selected.contains(e.id)) {\n                _selected.remove(e.id);\n              } else {\n                _selected.add(e.id);\n              }\n            });\n          },\n          child: Stack(children: [\n            AbsorbPointer(\n              child: c,\n            ),\n            SizedBox(\n              width: width,\n              height: height,\n              child: Align(\n                alignment: Alignment.topRight,\n                child: Container(\n                  padding: const EdgeInsets.all(5),\n                  margin: const EdgeInsets.only(top: 5),\n                  decoration: BoxDecoration(\n                    color: Colors.grey.shade800.withOpacity(.75),\n                    borderRadius: const BorderRadius.all(\n                      Radius.circular(5),\n                    ),\n                  ),\n                  child: Padding(\n                    padding: const EdgeInsets.all(5),\n                    child: Icon(\n                      _selected.contains(e.id)\n                          ? Icons.check_circle_sharp\n                          : Icons.circle_outlined,\n                      color: Theme.of(context).colorScheme.secondary,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n          ]),\n        );\n        tmp.add(c);\n      } else {\n        Widget c = LinkToComicInfo(\n          comicId: e.id,\n          child: Container(\n            margin: EdgeInsets.all(gap),\n            width: width,\n            height: height,\n            child: Stack(\n              children: [\n                RemoteImage(\n                  fileServer: e.thumb.fileServer,\n                  path: e.thumb.path,\n                  width: width,\n                  height: height,\n                ),\n                Align(\n                  alignment: Alignment.bottomCenter,\n                  child: Container(\n                    color: Colors.black.withOpacity(.3),\n                    child: Row(\n                      children: [\n                        Expanded(\n                          child: Text(\n                            e.title + '\\n',\n                            style: TextStyle(\n                              color: Colors.white,\n                              fontSize: titleFontSize,\n                              height: 1.2,\n                            ),\n                            strutStyle: const StrutStyle(height: 1.2),\n                            maxLines: 1,\n                            overflow: TextOverflow.ellipsis,\n                          ),\n                        ),\n                      ],\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        );\n        if (allSubscribed.containsKey(e.id)) {\n          final subscribed = allSubscribed[e.id]!;\n          if (subscribed.newEpCount > 0) {\n            c = Stack(\n              children: [\n                c,\n                Positioned(\n                  top: 0,\n                  left: 0,\n                  child: Container(\n                    padding: const EdgeInsets.all(2),\n                    decoration: const BoxDecoration(\n                      color: Colors.red,\n                      borderRadius: BorderRadius.only(\n                        bottomRight: Radius.circular(5),\n                      ),\n                    ),\n                    child: Text(\n                      subscribed.newEpCount.toString(),\n                      style: const TextStyle(\n                        color: Colors.white,\n                        fontSize: 12,\n                      ),\n                    ),\n                  ),\n                ),\n              ],\n            );\n          }\n        }\n        tmp.add(c);\n      }\n      if (tmp.length == rowCap) {\n        wraps.add(Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisAlignment: MainAxisAlignment.spaceAround,\n          children: tmp,\n        ));\n        tmp = [];\n      }\n    }\n    // 追加特殊按钮\n    if (widget.appendWidget != null) {\n      tmp.add(Container(\n        color:\n            (Theme.of(context).textTheme.bodyText1?.color ?? Colors.transparent)\n                .withOpacity(.1),\n        margin: EdgeInsets.only(\n          left: (rowCap - tmp.length) * gap,\n          right: (rowCap - tmp.length) * gap,\n          top: gap,\n          bottom: gap,\n        ),\n        width: (rowCap - tmp.length) * width,\n        height: height,\n        child: widget.appendWidget,\n      ));\n    }\n    // 最后一页没有下一页所有有可能为空\n    if (tmp.isNotEmpty) {\n      wraps.add(Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        mainAxisAlignment: MainAxisAlignment.spaceAround,\n        children: tmp,\n      ));\n      tmp = [];\n    }\n    // 返回\n    return PikaListView(\n      controller: widget.scrollController,\n      physics: const AlwaysScrollableScrollPhysics(),\n      padding: EdgeInsets.only(top: gap, bottom: gap),\n      children: wraps,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ComicListBuilder.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesEvent.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\nimport 'package:pikapika/screens/components/FitButton.dart';\nimport 'ContentBuilder.dart';\n\nclass ComicListBuilder extends StatefulWidget {\n  final Future<List<ComicSimple>> Function() takeList;\n\n  const ComicListBuilder(this.takeList, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicListBuilderState();\n}\n\nclass _ComicListBuilderState extends State<ComicListBuilder> {\n  late Future<List<ComicSimple>> _future = widget.takeList();\n  late Key _key = UniqueKey();\n\n  @override\n  void initState() {\n    shadowCategoriesEvent.subscribe(_onShadowChange);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    shadowCategoriesEvent.unsubscribe(_onShadowChange);\n    super.dispose();\n  }\n\n  void _onShadowChange(EventArgs? args) {\n    setState(() {});\n  }\n\n  Future _reload() async {\n    setState(() {\n      _future = widget.takeList();\n      _key = UniqueKey();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ContentBuilder(\n      key: _key,\n      future: _future,\n      onRefresh: _reload,\n      successBuilder:\n          (BuildContext context, AsyncSnapshot<List<ComicSimple>> snapshot) {\n        return RefreshIndicator(\n          onRefresh: _reload,\n          child: ComicList(\n            snapshot.data!,\n            appendWidget: FitButton(\n              onPressed: _reload,\n              text: tr('app.refresh'),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ComicPager.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/config/PagerAction.dart';\nimport 'package:pikapika/basic/config/ShadowCategoriesEvent.dart';\nimport 'package:pikapika/basic/enum/Sort.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\nimport 'package:pikapika/screens/components/ContentError.dart';\nimport 'package:pikapika/screens/components/FitButton.dart';\nimport '../../basic/Common.dart';\nimport '../../basic/config/IsPro.dart';\nimport 'ContentLoading.dart';\n\n// 漫画列页\nclass ComicPager extends StatefulWidget {\n  final bool coll;\n  final ComicListController? comicListController;\n  final Future<ComicsPage> Function(String sort, int page) fetchPage;\n\n  const ComicPager({\n    required this.fetchPage,\n    this.coll = false,\n    Key? key,\n    // required\n    this.comicListController,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicPagerState();\n}\n\nclass _ComicPagerState extends State<ComicPager> {\n  @override\n  void initState() {\n    shadowCategoriesEvent.subscribe(_onShadowChange);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    shadowCategoriesEvent.unsubscribe(_onShadowChange);\n    super.dispose();\n  }\n\n  void _onShadowChange(EventArgs? args) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    switch (currentPagerAction()) {\n      case PagerAction.CONTROLLER:\n        return ControllerComicPager(\n          fetchPage: widget.fetchPage,\n          comicListController: widget.comicListController,\n          coll: widget.coll,\n        );\n      case PagerAction.STREAM:\n        return StreamComicPager(\n          fetchPage: widget.fetchPage,\n          comicListController: widget.comicListController,\n          coll: widget.coll,\n        );\n      default:\n        return Container();\n    }\n  }\n}\n\nclass ControllerComicPager extends StatefulWidget {\n  final bool coll;\n  final ComicListController? comicListController;\n  final Future<ComicsPage> Function(String sort, int page) fetchPage;\n\n  const ControllerComicPager({\n    Key? key,\n    required this.fetchPage,\n    required this.comicListController,\n    required this.coll,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ControllerComicPagerState();\n}\n\nclass _ControllerComicPagerState extends State<ControllerComicPager> {\n  final TextEditingController _textEditController =\n      TextEditingController(text: '');\n  late String _currentSort = SORT_TIME_NEWEST;\n  late int _currentPage = 1;\n  late Future<ComicsPage> _pageFuture;\n\n  Future<dynamic> _load() async {\n    setState(() {\n      _pageFuture = widget.fetchPage(_currentSort, _currentPage);\n    });\n  }\n\n  @override\n  void initState() {\n    _load();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _pageFuture,\n      builder: (BuildContext context, AsyncSnapshot<ComicsPage> snapshot) {\n        if (snapshot.connectionState == ConnectionState.none) {\n          return Text(tr('app.initializing'));\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return ContentLoading(label: tr('app.loading'));\n        }\n        if (snapshot.hasError) {\n          return ContentError(\n            error: snapshot.error,\n            stackTrace: snapshot.stackTrace,\n            onRefresh: _load,\n          );\n        }\n        var comicsPage = snapshot.data!;\n        return Scaffold(\n          appBar: _buildAppBar(comicsPage, context),\n          body: ComicList(\n            comicsPage.docs,\n            appendWidget: _buildNextButton(comicsPage),\n            listController: widget.comicListController,\n          ),\n        );\n      },\n    );\n  }\n\n  PreferredSize _buildAppBar(ComicsPage comicsPage, BuildContext context) {\n    return PreferredSize(\n      preferredSize: const Size.fromHeight(40),\n      child: Container(\n        decoration: BoxDecoration(\n          border: Border(\n            bottom: BorderSide(\n              width: .5,\n              style: BorderStyle.solid,\n              color: Colors.grey[200]!,\n            ),\n          ),\n        ),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Row(\n              children: [\n                Container(width: 10),\n                DropdownButton(\n                  items: widget.coll ? collItems : items,\n                  value: _currentSort,\n                  onChanged: (String? value) {\n                    if (value != null) {\n                      _currentPage = 1;\n                      _currentSort = value;\n                      _load();\n                    }\n                  },\n                ),\n              ],\n            ),\n            InkWell(\n              onTap: () {\n                _textEditController.clear();\n                showDialog(\n                  context: context,\n                  builder: (context) {\n                    return AlertDialog(\n                      content: Card(\n                        child: TextField(\n                          controller: _textEditController,\n                          decoration: const InputDecoration(\n                            labelText: \"请输入页数：\",\n                          ),\n                          keyboardType: TextInputType.number,\n                          inputFormatters: <TextInputFormatter>[\n                            FilteringTextInputFormatter.allow(RegExp(r'\\d+')),\n                          ],\n                        ),\n                      ),\n                      actions: <Widget>[\n                        MaterialButton(\n                          onPressed: () {\n                            Navigator.pop(context);\n                          },\n                          child: const Text('取消'),\n                        ),\n                        MaterialButton(\n                          onPressed: () {\n                            Navigator.pop(context);\n                            var text = _textEditController.text;\n                            if (text.isEmpty || text.length > 5) {\n                              return;\n                            }\n                            var num = int.parse(text);\n                            if (num == 0 || num > comicsPage.pages) {\n                              return;\n                            }\n                            if (num > 10 && !isPro) {\n                              defaultToast(context, \"发电以后才能看10页以后的内容\");\n                              return;\n                            }\n                            _currentPage = num;\n                            _load();\n                          },\n                          child: const Text('确定'),\n                        ),\n                      ],\n                    );\n                  },\n                );\n              },\n              child: Row(\n                children: [\n                  Text(\"第 ${comicsPage.page} / ${comicsPage.pages} 页\"),\n                ],\n              ),\n            ),\n            Row(\n              children: [\n                MaterialButton(\n                  minWidth: 0,\n                  onPressed: () {\n                    if (comicsPage.page > 1) {\n                      _currentPage = comicsPage.page - 1;\n                      _load();\n                    }\n                  },\n                  child: const Text('上一页'),\n                ),\n                MaterialButton(\n                  minWidth: 0,\n                  onPressed: () {\n                    if (comicsPage.page < comicsPage.pages) {\n                      if (_currentPage >= 10 && !isPro) {\n                        defaultToast(context, \"发电以后才能看10页以后的内容\");\n                        return;\n                      }\n                      _currentPage = comicsPage.page + 1;\n                      _load();\n                    }\n                  },\n                  child: const Text('下一页'),\n                )\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget? _buildNextButton(ComicsPage comicsPage) {\n    if (comicsPage.page < comicsPage.pages) {\n      return FitButton(\n        onPressed: () {\n          if (_currentPage >= 10 && !isPro) {\n            defaultToast(context, \"发电以后才能看10页以后的内容\");\n            return;\n          }\n          _currentPage = comicsPage.page + 1;\n          _load();\n        },\n        text: '下一页',\n      );\n    }\n    return null;\n  }\n}\n\nclass StreamComicPager extends StatefulWidget {\n  final bool coll;\n  final ComicListController? comicListController;\n  final Future<ComicsPage> Function(String sort, int page) fetchPage;\n\n  const StreamComicPager({\n    Key? key,\n    required this.fetchPage,\n    required this.comicListController,\n    required this.coll,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _StreamComicPagerState();\n}\n\nclass _StreamComicPagerState extends State<StreamComicPager> {\n  final TextEditingController _textEditController =\n      TextEditingController(text: '');\n  final _scrollController = ScrollController();\n  late String _currentSort = SORT_DEFAULT;\n  late int _currentPage = 1;\n  late int _maxPage = 0;\n  late List<ComicSimple> _list = [];\n  late bool _loading = false;\n  late bool _over = false;\n  late bool _error = false;\n  late bool _noPro = false;\n\n  // late Future<dynamic> _pageFuture;\n\n  _onSetOffset(int i) {\n    _list.clear();\n    _currentPage = i;\n    _load();\n  }\n\n  void _onScroll() {\n    if (_over || _error || _loading || _noPro) {\n      return;\n    }\n    if (_scrollController.offset + MediaQuery.of(context).size.height / 2 <\n        _scrollController.position.maxScrollExtent) {\n      return;\n    }\n    _load();\n  }\n\n  Future<dynamic> _load() async {\n    setState(() {\n      //_pageFuture =\n      _fetch();\n    });\n  }\n\n  Future<dynamic> _fetch() async {\n    _error = false;\n    setState(() {\n      _loading = true;\n    });\n    try {\n      var page = await widget.fetchPage(_currentSort, _currentPage);\n      // setState(() {\n      _currentPage++;\n      _maxPage = page.pages;\n      _list.addAll(page.docs);\n      _over = page.page >= page.pages;\n      _noPro = _currentPage > 10 && !isPro;\n      // });\n      widget.comicListController?.loadViewed();\n    } catch (e, s) {\n      _error = true;\n      print(\"$e\\n$s\");\n      rethrow;\n    } finally {\n      setState(() {\n        _loading = false;\n      });\n    }\n  }\n\n  @override\n  void initState() {\n    _load();\n    _scrollController.addListener(_onScroll);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _scrollController.removeListener(_onScroll);\n    _scrollController.dispose();\n    _textEditController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: _buildAppBar(context),\n      body: ComicList(\n        _list,\n        scrollController: _scrollController,\n        appendWidget: _buildLoadingCell(),\n        listController: widget.comicListController,\n      ),\n    );\n  }\n\n  PreferredSize _buildAppBar(BuildContext context) {\n    return PreferredSize(\n      preferredSize: const Size.fromHeight(40),\n      child: Container(\n        decoration: BoxDecoration(\n          border: Border(\n            bottom: BorderSide(\n              width: .5,\n              style: BorderStyle.solid,\n              color: Colors.grey[200]!,\n            ),\n          ),\n        ),\n        child: Row(\n          mainAxisAlignment: MainAxisAlignment.spaceBetween,\n          children: [\n            Row(\n              children: [\n                Container(width: 10),\n                DropdownButton(\n                  items: widget.coll ? collItems : items,\n                  value: _currentSort,\n                  onChanged: (String? value) {\n                    if (value != null) {\n                      _list = [];\n                      _currentPage = 1;\n                      _currentSort = value;\n                      _load();\n                    }\n                  },\n                ),\n              ],\n            ),\n            Row(\n              children: [\n                InkWell(\n                  onTap: () {\n                    _textEditController.clear();\n                    showDialog(\n                      context: context,\n                      builder: (context) {\n                        return AlertDialog(\n                          content: Card(\n                            child: TextField(\n                              controller: _textEditController,\n                              decoration: const InputDecoration(\n                                labelText: \"请输入页数：\",\n                              ),\n                              keyboardType: TextInputType.number,\n                              inputFormatters: <TextInputFormatter>[\n                                FilteringTextInputFormatter.allow(\n                                    RegExp(r'\\d+')),\n                              ],\n                            ),\n                          ),\n                          actions: <Widget>[\n                            MaterialButton(\n                              onPressed: () {\n                                Navigator.pop(context);\n                              },\n                              child: const Text('取消'),\n                            ),\n                            MaterialButton(\n                              onPressed: () {\n                                Navigator.pop(context);\n                                var text = _textEditController.text;\n                                if (text.isEmpty || text.length > 5) {\n                                  return;\n                                }\n                                var num = int.parse(text);\n                                if (num == 0 || num > _maxPage) {\n                                  return;\n                                }\n                                if (_currentPage >= 10 && !isPro) {\n                                  defaultToast(context, \"发电以后才能看10页以后的内容\");\n                                  return;\n                                }\n                                _currentPage = num;\n                                _onSetOffset(num);\n                              },\n                              child: const Text('确定'),\n                            ),\n                          ],\n                        );\n                      },\n                    );\n                  },\n                  child: Row(\n                    children: [\n                      Text(\"已经加载 ${_currentPage - 1} / $_maxPage 页\"),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget? _buildLoadingCell() {\n    if (_noPro) {\n      return FitButton(onPressed: () {}, text: '发电以后才能看10页以后的内容');\n    }\n    if (_error) {\n      return FitButton(\n          onPressed: () {\n            setState(() {\n              _error = false;\n            });\n            _load();\n          },\n          text: '网络错误 / 点击刷新');\n    }\n    if (_loading) {\n      return FitButton(onPressed: () {}, text: '加载中');\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ComicTagsCard.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/screens/ComicsScreen.dart';\nimport 'package:pikapika/basic/Navigator.dart';\n\n// 漫画tag\nclass ComicTagsCard extends StatelessWidget {\n  final List<String> tags;\n\n  const ComicTagsCard(this.tags, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    return Container(\n      padding: const EdgeInsets.only(top: 5, bottom: 5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: Wrap(\n        children: tags.map((e) {\n          return InkWell(\n            onTap: () {\n              navPushOrReplace(context, (context) => ComicsScreen(tag: e));\n            },\n            child: Container(\n              padding: const EdgeInsets.only(\n                left: 10,\n                right: 10,\n                top: 3,\n                bottom: 3,\n              ),\n              margin: const EdgeInsets.only(\n                left: 5,\n                right: 5,\n                top: 3,\n                bottom: 3,\n              ),\n              decoration: BoxDecoration(\n                color: Colors.pink.shade100,\n                border: Border.all(\n                  style: BorderStyle.solid,\n                  color: Colors.pink.shade400,\n                ),\n                borderRadius: const BorderRadius.all(Radius.circular(30)),\n              ),\n              child: Text(\n                e,\n                style: TextStyle(\n                  color: Colors.pink.shade500,\n                  height: 1.4,\n                ),\n                strutStyle: const StrutStyle(\n                  height: 1.4,\n                ),\n              ),\n            ),\n          );\n        }).toList(),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/CommentItem.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport '../../basic/Cross.dart';\nimport 'Avatar.dart';\nimport 'CommentMainType.dart';\n\nclass ComicCommentItem extends StatefulWidget {\n  final CommentMainType mainType;\n  final String mainId;\n  final CommentBase comment;\n\n  const ComicCommentItem(this.mainType, this.mainId, this.comment, {Key? key})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicCommentItemState();\n}\n\nclass _ComicCommentItemState extends State<ComicCommentItem> {\n  var likeLoading = false;\n\n  @override\n  Widget build(BuildContext context) {\n    var comment = widget.comment;\n    var theme = Theme.of(context);\n    var nameStyle = const TextStyle(fontWeight: FontWeight.bold);\n    var levelStyle = TextStyle(\n        fontSize: 12, color: theme.colorScheme.secondary.withOpacity(.8));\n    var connectStyle =\n        TextStyle(color: theme.textTheme.bodyText1?.color?.withOpacity(.8));\n    var datetimeStyle = TextStyle(\n        color: theme.textTheme.bodyText1?.color?.withOpacity(.6), fontSize: 12);\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          top: BorderSide(\n            width: .25,\n            style: BorderStyle.solid,\n            color: Colors.grey.shade500.withOpacity(.5),\n          ),\n          bottom: BorderSide(\n            width: .25,\n            style: BorderStyle.solid,\n            color: Colors.grey.shade500.withOpacity(.5),\n          ),\n        ),\n      ),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Avatar(comment.user.avatar),\n          Container(width: 5),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                LayoutBuilder(\n                  builder: (BuildContext context, BoxConstraints constraints) {\n                    return SizedBox(\n                      width: constraints.maxWidth,\n                      child: Wrap(\n                        crossAxisAlignment: WrapCrossAlignment.center,\n                        alignment: WrapAlignment.spaceBetween,\n                        children: [\n                          Text(comment.user.name, style: nameStyle),\n                          Text(\n                            formatTimeToDateTime(comment.createdAt),\n                            style: datetimeStyle,\n                          ),\n                        ],\n                      ),\n                    );\n                  },\n                ),\n                Container(height: 3),\n                LayoutBuilder(\n                  builder: (BuildContext context, BoxConstraints constraints) {\n                    return SizedBox(\n                      width: constraints.maxWidth,\n                      child: Wrap(\n                        crossAxisAlignment: WrapCrossAlignment.center,\n                        alignment: WrapAlignment.spaceBetween,\n                        children: [\n                          Text(\n                              \"Lv. ${comment.user.level} (${comment.user.title})\",\n                              style: levelStyle),\n                          Text.rich(TextSpan(\n                            style: levelStyle,\n                            children: [\n                              comment.commentsCount > 0\n                                  ? TextSpan(children: [\n                                      WidgetSpan(\n                                        alignment: PlaceholderAlignment.middle,\n                                        child: Icon(Icons.message,\n                                            size: 13,\n                                            color: theme.colorScheme.secondary\n                                                .withOpacity(.7)),\n                                      ),\n                                      WidgetSpan(child: Container(width: 5)),\n                                      TextSpan(\n                                        text: '${comment.commentsCount}',\n                                      ),\n                                    ])\n                                  : const TextSpan(),\n                              WidgetSpan(child: Container(width: 12)),\n                              WidgetSpan(\n                                  child: GestureDetector(\n                                onTap: () async {\n                                  setState(() {\n                                    likeLoading = true;\n                                  });\n                                  try {\n                                    switch (widget.mainType) {\n                                      case CommentMainType.COMIC:\n                                        await method.switchLikeComment(\n                                          comment.id,\n                                          widget.mainId,\n                                        );\n                                        break;\n                                      case CommentMainType.GAME:\n                                        await method.switchLikeGameComment(\n                                          comment.id,\n                                          widget.mainId,\n                                        );\n                                        break;\n                                    }\n                                    setState(() {\n                                      if (comment.isLiked) {\n                                        comment.isLiked = false;\n                                        comment.likesCount--;\n                                      } else {\n                                        comment.isLiked = true;\n                                        comment.likesCount++;\n                                      }\n                                    });\n                                  } catch (e, s) {\n                                    print(\"$e\\n$s\");\n                                    defaultToast(context, tr(\"app.like_failed\"));\n                                  } finally {\n                                    setState(() {\n                                      likeLoading = false;\n                                    });\n                                  }\n                                },\n                                child: Text.rich(\n                                  TextSpan(style: levelStyle, children: [\n                                    WidgetSpan(\n                                      alignment: PlaceholderAlignment.middle,\n                                      child: Icon(\n                                          likeLoading\n                                              ? Icons.refresh\n                                              : comment.isLiked\n                                                  ? Icons.favorite\n                                                  : Icons.favorite_border,\n                                          size: 13,\n                                          color: theme.colorScheme.secondary\n                                              .withOpacity(.7)),\n                                    ),\n                                    WidgetSpan(child: Container(width: 5)),\n                                    TextSpan(\n                                      text: '${comment.likesCount}',\n                                    ),\n                                  ]),\n                                ),\n                              )),\n                            ],\n                          )),\n                        ],\n                      ),\n                    );\n                  },\n                ),\n                Container(height: 5),\n                GestureDetector(\n                  onLongPress: () {\n                    confirmCopy(context, comment.content);\n                  },\n                  child: Text(comment.content, style: connectStyle),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/CommentList.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Entities.dart' as e;\nimport 'package:pikapika/screens/CommentScreen.dart';\nimport 'package:pikapika/screens/components/BottomSheetInput.dart';\nimport 'package:pikapika/screens/components/ItemBuilder.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport '../../basic/config/IconLoading.dart';\nimport 'CommentItem.dart';\nimport 'CommentMainType.dart';\n\nclass _CommentBasePage extends e.Page {\n  late List<CommentBase> docs;\n\n  _CommentBasePage.ofComic(CommentPage commentPage)\n      : super.of(commentPage.total, commentPage.limit, commentPage.page,\n            commentPage.pages) {\n    this.docs = commentPage.docs;\n  }\n\n  _CommentBasePage.ofGame(GameCommentPage commentPage)\n      : super.of(commentPage.total, commentPage.limit, commentPage.page,\n            commentPage.pages) {\n    this.docs = commentPage.docs;\n  }\n}\n\n// 漫画的评论列表\nclass CommentList extends StatefulWidget {\n  final CommentMainType mainType;\n  final String mainId;\n\n  const CommentList(this.mainType, this.mainId, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _CommentListState();\n}\n\nclass _CommentListState extends State<CommentList> {\n  late int _currentPage = 1;\n  late Future<_CommentBasePage> _future = _loadPage();\n\n  Future<_CommentBasePage> _loadPage() async {\n    switch (widget.mainType) {\n      case CommentMainType.COMIC:\n        return _CommentBasePage.ofComic(\n          await method.comments(widget.mainId, _currentPage),\n        );\n      case CommentMainType.GAME:\n        return _CommentBasePage.ofGame(\n          await method.gameComments(widget.mainId, _currentPage),\n        );\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ItemBuilder(\n      future: _future,\n      successBuilder:\n          (BuildContext context, AsyncSnapshot<_CommentBasePage> snapshot) {\n        var page = snapshot.data!;\n        return Column(\n          mainAxisAlignment: MainAxisAlignment.start,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            _buildPrePage(page),\n            ...page.docs.map((e) => _buildComment(e)),\n            _buildNextPage(page),\n            _buildPostComment(),\n          ],\n        );\n      },\n      onRefresh: () async => {\n        setState(() {\n          _future = _loadPage();\n        })\n      },\n    );\n  }\n\n  Widget _buildComment(CommentBase comment) {\n    return InkWell(\n      onTap: () {\n        Navigator.of(context).push(\n          mixRoute(\n            builder: (context) =>\n                CommentScreen(widget.mainType, widget.mainId, comment),\n          ),\n        );\n      },\n      child: ComicCommentItem(widget.mainType, widget.mainId, comment),\n    );\n  }\n\n  Widget _buildPostComment() {\n    return InkWell(\n      onTap: () async {\n        showInputModalBottomSheet(\n          context: context,\n          onSubmitted: (text) async {\n            switch (widget.mainType) {\n              case CommentMainType.COMIC:\n                await method.postComment(widget.mainId, text);\n                break;\n              case CommentMainType.GAME:\n                await method.postGameComment(widget.mainId, text);\n                break;\n            }\n            setState(() {\n              _future = _loadPage();\n            });\n            defaultToast(context, tr(\"screen.comment.comment_success\"));\n          },\n          hintText: tr(\"screen.comment.please_enter_comment\"),\n        );\n      },\n      child: Container(\n        decoration: BoxDecoration(\n          border: Border(\n            top: BorderSide(\n              width: .25,\n              style: BorderStyle.solid,\n              color: Colors.grey.shade500.withOpacity(.5),\n            ),\n            bottom: BorderSide(\n              width: .25,\n              style: BorderStyle.solid,\n              color: Colors.grey.shade500.withOpacity(.5),\n            ),\n          ),\n        ),\n        padding: const EdgeInsets.all(30),\n        child: Center(\n          child: Text(tr(\"screen.comment.i_have_something_to_say\")),\n        ),\n      ),\n    );\n  }\n\n  Widget _buildPrePage(_CommentBasePage page) {\n    if (page.page > 1) {\n      return InkWell(\n        onTap: () {\n          setState(() {\n            _currentPage = page.page - 1;\n            _future = _loadPage();\n          });\n        },\n        child: Container(\n          padding: const EdgeInsets.all(30),\n          child: Center(\n            child: Text(tr(\"app.previous_page\")),\n          ),\n        ),\n      );\n    }\n    return Container();\n  }\n\n  Widget _buildNextPage(_CommentBasePage page) {\n    if (page.page < page.pages) {\n      return InkWell(\n        onTap: () {\n          setState(() {\n            _currentPage = page.page + 1;\n            _future = _loadPage();\n          });\n        },\n        child: Container(\n          padding: const EdgeInsets.all(30),\n          child: Center(\n            child: Text(tr(\"app.next_page\")),\n          ),\n        ),\n      );\n    }\n    return Container();\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/CommentMainType.dart",
    "content": "enum CommentMainType {\n  COMIC,\n  GAME,\n}\n"
  },
  {
    "path": "lib/screens/components/Common.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\n\nimport '../../basic/config/IsPro.dart';\nimport '../../basic/config/ListLayout.dart';\nimport '../../basic/config/ShadowCategories.dart';\nimport '../../basic/config/ShadowCategoriesMode.dart';\n\nWidget commonPopMenu(\n  BuildContext context, {\n  ComicListController? comicListController,\n  void Function(VoidCallback fn)? setState,\n}) {\n  return PopupMenuButton<int>(\n    itemBuilder: (BuildContext context) => <PopupMenuItem<int>>[\n      PopupMenuItem<int>(\n        value: 0,\n        child: ListTile(\n          leading: const Icon(Icons.view_quilt),\n          title: Text(tr(\"components.common.display_mode\")),\n        ),\n      ),\n      PopupMenuItem<int>(\n        value: 1,\n        child: ListTile(\n          leading: const Icon(Icons.do_not_disturb_on_outlined),\n          title: Text(tr(\"components.common.shadow_mode\")),\n        ),\n      ),\n      PopupMenuItem<int>(\n        value: 2,\n        child: ListTile(\n          leading: const Icon(Icons.hide_source),\n          title: Text(tr(\"components.common.shadow_list\")),\n        ),\n      ),\n      ...comicListController != null && setState != null\n          ? [\n              PopupMenuItem<int>(\n                value: 3,\n                child: ListTile(\n                  leading: Icon(\n                    Icons.download,\n                    color: isPro ? null : Colors.grey,\n                  ),\n                  title: Text(\n                    tr(\"components.common.batch_download\") +\n                        (isPro ? \"\" : \"(${tr('app.pro')})\"),\n                    style: TextStyle(\n                      color: isPro ? null : Colors.grey,\n                    ),\n                  ),\n                ),\n              )\n            ]\n          : [],\n    ],\n    onSelected: (int value) {\n      switch (value) {\n        case 0:\n          chooseListLayout(context);\n          break;\n        case 1:\n          chooseShadowCategoriesMode(context);\n          break;\n        case 2:\n          chooseShadowCategories(context);\n          break;\n        case 3:\n          if (!isPro) {\n            defaultToast(context, tr(\"app.pro_required\"));\n            return;\n          }\n          if (setState != null) {\n            if (comicListController != null) {\n              setState(() {\n                comicListController.selecting = !comicListController.selecting;\n              });\n            }\n          }\n          break;\n      }\n    },\n  );\n}\n"
  },
  {
    "path": "lib/screens/components/CommonData.dart",
    "content": "import 'package:event/event.dart';\n\nimport '../../basic/Entities.dart';\nimport '../../basic/Method.dart';\n\nfinal subscribedEvent = Event();\n\nfinal Map<String, ComicSubscribe> allSubscribed = {};\n\nFuture updateSubscribed() async {\n  await method.updateSubscribed();\n  await _update();\n}\n\nFuture updateSubscribedForce() async {\n  await method.updateSubscribedForce();\n  await _update();\n}\n\nFuture _update() async {\n  final _allSubscribed = await method.allSubscribed();\n  allSubscribed.clear();\n  for (var subscribed in _allSubscribed) {\n    allSubscribed[subscribed.id] = subscribed;\n  }\n  subscribedEvent.broadcast();\n}\n\nFuture removeAllSubscribed() async {\n  await method.removeAllSubscribed();\n  allSubscribed.clear();\n  subscribedEvent.broadcast();\n}\n\nFuture subscribedViewed(String id) async {\n  if (allSubscribed.containsKey(id)) {\n    allSubscribed.remove(id);\n    subscribedEvent.broadcast();\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ContentBuilder.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'ContentError.dart';\nimport 'ContentLoading.dart';\n\nclass ContentBuilder<T> extends StatelessWidget {\n  final Future<T> future;\n  final Future<dynamic> Function() onRefresh;\n  final AsyncWidgetBuilder<T> successBuilder;\n\n  const ContentBuilder({\n    required Key? key,\n    required this.future,\n    required this.onRefresh,\n    required this.successBuilder,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: future,\n      builder: (BuildContext context, AsyncSnapshot<T> snapshot) {\n        if (snapshot.hasError) {\n          return ContentError(\n            error: snapshot.error,\n            stackTrace: snapshot.stackTrace,\n            onRefresh: onRefresh,\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return ContentLoading(label: tr('app.loading'));\n        }\n        return successBuilder(context, snapshot);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ContentError.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/ContentFailedReloadAction.dart';\n\nimport 'package:pikapika/basic/enum/ErrorTypes.dart';\n\nclass ContentError extends StatelessWidget {\n  final Object? error;\n  final StackTrace? stackTrace;\n  final Future<void> Function() onRefresh;\n\n  const ContentError({\n    Key? key,\n    required this.error,\n    required this.stackTrace,\n    required this.onRefresh,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var type = errorType(\"$error\");\n    late String message;\n    late IconData iconData;\n    switch (type) {\n      case ERROR_TYPE_NETWORK:\n        iconData = Icons.wifi_off_rounded;\n        message = tr(\"app.network_error\");\n        break;\n      case ERROR_TYPE_PERMISSION:\n        iconData = Icons.highlight_off;\n        message = tr(\"app.no_permission\");\n        break;\n      case ERROR_TYPE_TIME:\n        iconData = Icons.timer_off;\n        message = tr(\"app.check_device_time\");\n        break;\n      case ERROR_TYPE_UNDER_REVIEW:\n        iconData = Icons.highlight_off;\n        message = tr(\"app.resource_not_available\");\n        break;\n      default:\n        iconData = Icons.highlight_off;\n        message = tr(\"app.something_went_wrong\");\n        break;\n    }\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        print(\"$error\");\n        print(\"$stackTrace\");\n        var width = constraints.maxWidth;\n        var height = constraints.maxHeight;\n        var min = width < height ? width : height;\n        var iconSize = min / 2.3;\n        var textSize = min / 16;\n        var tipSize = min / 20;\n        var infoSize = min / 30;\n        if (contentFailedReloadAction ==\n            ContentFailedReloadAction.TOUCH_LOADER) {\n          return GestureDetector(\n            onTap: onRefresh,\n            child: ListView(\n              children: [\n                SizedBox(\n                  height: height,\n                  child: Column(\n                    children: [\n                      Expanded(child: Container()),\n                      Icon(\n                        iconData,\n                        size: iconSize,\n                        color: Colors.grey.shade600,\n                      ),\n                      Container(height: min / 10),\n                      Container(\n                        padding: const EdgeInsets.only(\n                          left: 30,\n                          right: 30,\n                        ),\n                        child: Text(\n                          message,\n                          style: TextStyle(fontSize: textSize),\n                          textAlign: TextAlign.center,\n                        ),\n                      ),\n                      Text('(${tr(\"app.click_refresh\")})', style: TextStyle(fontSize: tipSize)),\n                      Container(height: min / 15),\n                      Text('$error', style: TextStyle(fontSize: infoSize)),\n                      Expanded(child: Container()),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          );\n        }\n        return RefreshIndicator(\n          onRefresh: onRefresh,\n          child: ListView(\n            children: [\n              SizedBox(\n                height: height,\n                child: Column(\n                  children: [\n                    Expanded(child: Container()),\n                    Icon(\n                      iconData,\n                      size: iconSize,\n                      color: Colors.grey.shade600,\n                    ),\n                    Container(height: min / 10),\n                    Container(\n                      padding: const EdgeInsets.only(\n                        left: 30,\n                        right: 30,\n                      ),\n                      child: Text(\n                        message,\n                        style: TextStyle(fontSize: textSize),\n                        textAlign: TextAlign.center,\n                      ),\n                    ),\n                    Text('(${tr(\"app.pull_down_refresh\")})', style: TextStyle(fontSize: tipSize)),\n                    Container(height: min / 15),\n                    Text('$error', style: TextStyle(fontSize: infoSize)),\n                    Expanded(child: Container()),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ContentLoading.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/IconLoading.dart';\n\nclass ContentLoading extends StatelessWidget {\n  final String label;\n\n  const ContentLoading({Key? key, required this.label}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        var height = constraints.maxHeight;\n        var min = width < height ? width : height;\n        var theme = Theme.of(context);\n        return Center(\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              SizedBox(\n                width: min / 2,\n                height: min / 2,\n                child: currentIconLoading()\n                    ? Icon(Icons.refresh,\n                        color: Colors.grey[400], size: min / 2)\n                    : CircularProgressIndicator(\n                        color: theme.colorScheme.secondary,\n                        backgroundColor: Colors.grey[100],\n                      ),\n              ),\n              Container(height: min / 10),\n              Text(label, style: TextStyle(fontSize: min / 15)),\n              Expanded(child: Container()),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ContentMessage.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\n\nimport '../../basic/config/ContentFailedReloadAction.dart';\n\nclass ContentMessage extends StatelessWidget {\n  final RefreshCallback? onRefresh;\n  final IconData icon;\n  final String message;\n\n  const ContentMessage({\n    required this.message,\n    required this.icon,\n    this.onRefresh,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    if (onRefresh != null) {\n      return LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          var width = constraints.maxWidth;\n          var height = constraints.maxHeight;\n          var min = width < height ? width : height;\n          var iconSize = min / 2.3;\n          var textSize = min / 16;\n          var tipSize = min / 20;\n          if (contentFailedReloadAction ==\n              ContentFailedReloadAction.TOUCH_LOADER) {\n            return GestureDetector(\n              onTap: () {\n                onRefresh!();\n              },\n              child: ListView(\n                children: [\n                  SizedBox(\n                    height: height,\n                    child: Column(\n                      children: [\n                        Expanded(child: Container()),\n                        Icon(\n                          icon,\n                          size: iconSize,\n                          color: Colors.grey.shade600,\n                        ),\n                        Container(height: min / 10),\n                        Container(\n                          padding: const EdgeInsets.only(\n                            left: 30,\n                            right: 30,\n                          ),\n                          child: Text(\n                            message,\n                            style: TextStyle(fontSize: textSize),\n                            textAlign: TextAlign.center,\n                          ),\n                        ),\n                        Text('(${tr(\"app.click_refresh\")})', style: TextStyle(fontSize: tipSize)),\n                        Expanded(child: Container()),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            );\n          }\n          return RefreshIndicator(\n            onRefresh: () async {\n              onRefresh!();\n            },\n            child: ListView(\n              children: [\n                SizedBox(\n                  height: height,\n                  child: Column(\n                    children: [\n                      Expanded(child: Container()),\n                      Icon(\n                        icon,\n                        size: iconSize,\n                        color: Colors.grey.shade600,\n                      ),\n                      Container(height: min / 10),\n                      Container(\n                        padding: const EdgeInsets.only(\n                          left: 30,\n                          right: 30,\n                        ),\n                        child: Text(\n                          message,\n                          style: TextStyle(fontSize: textSize),\n                          textAlign: TextAlign.center,\n                        ),\n                      ),\n                      Text('(${tr(\"app.pull_down_refresh\")})', style: TextStyle(fontSize: tipSize)),\n                      Container(height: min / 15),\n                      Expanded(child: Container()),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          );\n        },\n      );\n    }\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        var height = constraints.maxHeight;\n        var min = width < height ? width : height;\n        return Center(\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              SizedBox(\n                width: min / 2,\n                height: min / 2,\n                child: Icon(icon, color: Colors.grey[100]),\n              ),\n              Container(height: min / 10),\n              Text(message, style: TextStyle(fontSize: min / 15)),\n              Expanded(child: Container()),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/ContinueReadButton.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\n\n// 继续阅读按钮\nclass ContinueReadButton extends StatefulWidget {\n  final Future<ViewLog?> viewFuture;\n  final Function(int? epOrder, int? pictureRank) onChoose;\n\n  const ContinueReadButton({\n    Key? key,\n    required this.viewFuture,\n    required this.onChoose,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ContinueReadButtonState();\n}\n\nclass _ContinueReadButtonState extends State<ContinueReadButton> {\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        return FutureBuilder(\n          future: widget.viewFuture,\n          builder: (BuildContext context, AsyncSnapshot<ViewLog?> snapshot) {\n            late void Function() onPressed;\n            late String text;\n            if (snapshot.connectionState != ConnectionState.done) {\n              onPressed = () {};\n              text = tr('app.loading');\n            }\n            if (snapshot.data != null && snapshot.data!.lastViewEpOrder > 0) {\n              onPressed = () => widget.onChoose(\n                    snapshot.data?.lastViewEpOrder,\n                    snapshot.data?.lastViewPictureRank,\n                  );\n              text =\n                  '${tr('app.continue_reading')} ${snapshot.data?.lastViewEpTitle} P. ${(snapshot.data?.lastViewPictureRank ?? 0) + 1}';\n            } else {\n              onPressed = () => widget.onChoose(null, null);\n              text = tr('app.start_reading');\n            }\n            return Container(\n              padding: const EdgeInsets.only(left: 10, right: 10),\n              margin: const EdgeInsets.only(bottom: 10),\n              width: width,\n              child: MaterialButton(\n                onPressed: onPressed,\n                child: Row(\n                  children: [\n                    Expanded(\n                      child: Container(\n                        color: Theme.of(context)\n                            .textTheme\n                            .bodyText1!\n                            .color!\n                            .withOpacity(.05),\n                        padding: const EdgeInsets.all(10),\n                        child: Text(\n                          text,\n                          textAlign: TextAlign.center,\n                        ),\n                      ),\n                    )\n                  ],\n                ),\n              ),\n            );\n          },\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/DesktopCropper.dart",
    "content": "import 'dart:io';\nimport 'dart:ui' as ui;\nimport 'package:pikapika/i18.dart';\nimport 'package:image/image.dart' as image;\nimport 'package:crop_image/crop_image.dart';\nimport 'package:flutter/material.dart';\n\nclass DesktopCropper extends StatefulWidget {\n  final String? title;\n  final double? aspectRatio;\n  final String file;\n\n  const DesktopCropper({\n    Key? key,\n    this.title,\n    this.aspectRatio,\n    required this.file,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DesktopCropperState();\n}\n\nclass _DesktopCropperState extends State<DesktopCropper> {\n  late final _controller = CropController(\n    aspectRatio: widget.aspectRatio,\n  );\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: Text(widget.title ?? tr(\"app.image_crop\")),\n        actions: [\n          IconButton(onPressed: _finish, icon: const Icon(Icons.done)),\n        ],\n      ),\n      body: Center(\n        child: Padding(\n          padding: const EdgeInsets.all(12.0),\n          child: CropImage(\n            controller: _controller,\n            image: Image.file(File(widget.file)),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future _finish() async {\n    var cropped = await _controller.croppedBitmap();\n    var data = await cropped.toByteData(format: ui.ImageByteFormat.png);\n    if (data != null) {\n      var u8list = data.buffer.asUint8List();\n      image.Image? baseSizeImage = image.decodePng(u8list);\n      if (baseSizeImage != null) {\n        if (cropped.width > 200) {\n          baseSizeImage =\n              image.copyResize(baseSizeImage, height: 200, width: 200);\n        }\n        var f = image.encodeJpg(baseSizeImage);\n        Navigator.of(context).pop(f);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/DownloadComicsScreen.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../../basic/Channels.dart';\nimport '../../basic/Common.dart';\nimport '../../basic/Method.dart';\nimport 'ContentLoading.dart';\n\nclass DownloadComicsScreen extends StatefulWidget {\n  final List<String> comicIds;\n\n  const DownloadComicsScreen(this.comicIds, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadComicsScreenState();\n}\n\nclass _DownloadComicsScreenState extends State<DownloadComicsScreen> {\n  bool exporting = false;\n  bool exported = false;\n  bool exportFail = false;\n  dynamic e;\n  String exportMessage = \"正在创建下载任务\";\n\n  @override\n  void initState() {\n    registerEvent(_onMessageChange, \"EXPORT\");\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    unregisterEvent(_onMessageChange);\n    super.dispose();\n  }\n\n  void _onMessageChange(event) {\n    setState(() {\n      exportMessage = event;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return WillPopScope(\n      child: Scaffold(\n        appBar: AppBar(\n          title: const Text(\"批量下载\"),\n        ),\n        body: _body(),\n      ),\n      onWillPop: () async {\n        if (exporting) {\n          defaultToast(context, \"创建下载任务中, 请稍后\");\n          return false;\n        }\n        return true;\n      },\n    );\n  }\n\n  Widget _body() {\n    if (exporting) {\n      return ContentLoading(label: exportMessage);\n    }\n    if (exportFail) {\n      return Center(child: Text(\"失败\\n$e\"));\n    }\n    if (exported) {\n      return const Center(child: Text(\"成功\"));\n    }\n    return ListView(\n      children: [\n        Container(height: 20),\n        Container(height: 20),\n        _buildButtonInner(\"您即将下载${widget.comicIds.length}部漫画, 如果漫画已经存在, 则补充新增加的章节\"),\n        Container(height: 20),\n        Container(height: 20),\n        MaterialButton(\n          onPressed: _create,\n          child: _buildButtonInner(\"确认\"),\n        ),\n        Container(height: 20),\n        Container(height: 20),\n        Container(height: 20),\n      ],\n    );\n  }\n\n  _create() async {\n    var name = \"\";\n    try {\n      setState(() {\n        exporting = true;\n      });\n      await method.downloadAll(\n        widget.comicIds,\n      );\n      exported = true;\n    } catch (err) {\n      e = err;\n      exportFail = true;\n    } finally {\n      setState(() {\n        exporting = false;\n      });\n    }\n  }\n\n  Widget _buildButtonInner(String text) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return Container(\n          width: constraints.maxWidth,\n          padding: const EdgeInsets.all(15),\n          color: (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black)\n              .withOpacity(.05),\n          child: Text(\n            text,\n            textAlign: TextAlign.center,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/DownloadInfoCard.dart",
    "content": "import 'dart:convert';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/screens/components/Images.dart';\n\nimport '../../basic/config/CopyFullName.dart';\nimport '../../basic/config/CopyFullNameTemplate.dart';\nimport 'ComicInfoCard.dart';\n\n// 下载项\nclass DownloadInfoCard extends StatelessWidget {\n  final DownloadComic task;\n  final bool downloading;\n  final bool linkItem;\n\n  const DownloadInfoCard({\n    Key? key,\n    required this.task,\n    this.downloading = false,\n    this.linkItem = false,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    var textColor = theme.textTheme.bodyText1!.color!;\n    var textColorAlpha = textColor.withAlpha(0x33);\n    var textColorSummary = textColor.withAlpha(0xCC);\n    var titleStyle = TextStyle(\n      color: textColor,\n      fontWeight: FontWeight.bold,\n    );\n    var categoriesStyle = TextStyle(\n      fontSize: 13,\n      color: textColorSummary,\n    );\n    var authorStyle = TextStyle(\n      fontSize: 13,\n      color: Colors.pink.shade300,\n    );\n    var iconColor = Colors.pink.shade300;\n    var iconLabelStyle = TextStyle(\n      fontSize: 13,\n      color: iconColor,\n    );\n    List<dynamic> categories = json.decode(task.categories);\n    var categoriesString = categories.map((e) => \"$e\").join(\" \");\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.only(right: 10),\n            child: task.thumbLocalPath == \"\"\n                ? RemoteImage(\n                    fileServer: task.thumbFileServer,\n                    path: task.thumbPath,\n                    width: imageWidth,\n                    height: imageHeight,\n                  )\n                : DownloadImage(\n                    path: task.thumbLocalPath,\n                    width: imageWidth,\n                    height: imageHeight,\n                  ),\n          ),\n          Expanded(\n            child: Row(\n              children: [\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      linkItem\n                          ? GestureDetector(\n                              onLongPress: () {\n                                if (copyFullName()) {\n                                  var fin =\n                                  copyFullNameTemplate()\n                                      .replaceAll(\"{title}\", task.title)\n                                      .replaceAll(\"{author}\", task.author);\n                                  if (fin.isEmpty) {\n                                    fin = task.title;\n                                  }\n                                  confirmCopy(\n                                      context, fin);\n                                } else {\n                                  confirmCopy(context, task.title);\n                                }\n                              },\n                              child: Text(task.title, style: titleStyle),\n                            )\n                          : Text(task.title, style: titleStyle),\n                      Container(height: 5),\n                      linkItem\n                          ? GestureDetector(\n                              onLongPress: () {\n                                confirmCopy(context, task.author);\n                              },\n                              child: Text(task.author, style: authorStyle),\n                            )\n                          : Text(task.author, style: authorStyle),\n                      Container(height: 5),\n                      Text(\n                        \"${tr('app.categories')}: $categoriesString\",\n                        style: categoriesStyle,\n                      ),\n                      Container(height: 5),\n                      Row(\n                        children: [\n                          Icon(\n                            Icons.download,\n                            size: iconSize,\n                            color: iconColor,\n                          ),\n                          Container(width: 5),\n                          Text(\n                            '${tr('app.download')} ${task.downloadPictureCount} / ${task.selectedPictureCount}',\n                            style: iconLabelStyle,\n                          ),\n                          Container(width: 20),\n                          task.deleting\n                              ? Text(tr('app.deleting'),\n                                  style: TextStyle(\n                                      color: Color.alphaBlend(\n                                          textColor.withAlpha(0x33),\n                                          Colors.red.shade500)))\n                              : task.downloadFailed\n                                  ? Text(tr('app.download_failed'),\n                                      style: TextStyle(\n                                          color: Color.alphaBlend(\n                                              textColor.withAlpha(0x33),\n                                              Colors.red.shade500)))\n                                  : task.downloadFinished\n                                      ? Text(tr('app.download_finished'),\n                                          style: TextStyle(\n                                              color: Color.alphaBlend(\n                                                  textColorAlpha,\n                                                  Colors.green.shade500)))\n                                      : downloading // downloader.downloadingTask() == task.id\n                                          ? Text(tr('app.downloading'),\n                                              style: TextStyle(\n                                                  color: Color.alphaBlend(\n                                                      textColorAlpha,\n                                                      Colors\n                                                          .blue.shade500)))\n                                          : Text(tr('app.queue'),\n                                              style: TextStyle(\n                                                  color: Color.alphaBlend(\n                                                      textColorAlpha,\n                                                      Colors.lightBlue\n                                                          .shade500))),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n                Container(\n                  padding: const EdgeInsets.only(left: 8),\n                  height: imageHeight,\n                  child: Column(\n                    mainAxisAlignment: MainAxisAlignment.start,\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      buildFinished(task.finished),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\ndouble imageWidth = 210 / 3.15;\ndouble imageHeight = 315 / 3.15;\ndouble iconSize = 15;\n"
  },
  {
    "path": "lib/screens/components/FitButton.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass FitButton extends StatelessWidget {\n  final void Function() onPressed;\n  final String text;\n\n  const FitButton({Key? key, required this.onPressed, required this.text})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return SizedBox(\n          width: constraints.maxWidth,\n          height: constraints.maxHeight,\n          child: Container(\n            padding: const EdgeInsets.all(10),\n            child: MaterialButton(\n              onPressed: onPressed,\n              child: Center(\n                child: Text(text),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/GameTitleCard.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter_svg/svg.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\n\nimport 'Images.dart';\n\n// 游戏信息卡\nclass GameTitleCard extends StatelessWidget {\n  final GameInfo info;\n\n  const GameTitleCard(this.info, {Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    double iconMargin = 20;\n    double iconSize = 60;\n    BorderRadius iconRadius = const BorderRadius.all(Radius.circular(6));\n    TextStyle titleStyle =\n        const TextStyle(fontSize: 16, fontWeight: FontWeight.bold);\n    TextStyle publisherStyle = TextStyle(\n      color: Theme.of(context).colorScheme.secondary,\n      fontSize: 12.5,\n    );\n    TextStyle versionStyle = const TextStyle(fontSize: 12.5);\n    double platformMargin = 10;\n    double platformSize = 25;\n    return Row(\n      children: [\n        Container(\n          padding: EdgeInsets.all(iconMargin),\n          child: ClipRRect(\n            borderRadius: iconRadius,\n            child: RemoteImage(\n              width: iconSize,\n              height: iconSize,\n              fileServer: info.icon.fileServer,\n              path: info.icon.path,\n            ),\n          ),\n        ),\n        Container(width: 10),\n        Expanded(\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.spaceAround,\n            crossAxisAlignment: CrossAxisAlignment.start,\n            children: [\n              GestureDetector(\n                onLongPress: () {\n                  confirmCopy(context, info.title);\n                },\n                child: Text(info.title, style: titleStyle),\n              ),\n              GestureDetector(\n                onLongPress: () {\n                  confirmCopy(context, info.publisher);\n                },\n                child: Text(info.publisher, style: publisherStyle),\n              ),\n              Text(info.version, style: versionStyle),\n            ],\n          ),\n        ),\n        Container(\n          margin: EdgeInsets.only(right: iconMargin),\n          // padding: const EdgeInsets.only(\n          //   left: platformMargin,\n          //   right: platformMargin,\n          // ),\n          child: Column(\n            children: [\n              ...info.android\n                  ? [\n                      SvgPicture.asset(\n                        'lib/assets/android.svg',\n                        fit: BoxFit.contain,\n                        width: platformSize,\n                        height: platformSize,\n                        color: Colors.green.shade500,\n                      ),\n                    ]\n                  : [],\n              Container(\n                height: platformMargin,\n              ),\n              ...info.ios\n                  ? [\n                      SvgPicture.asset(\n                        'lib/assets/apple.svg',\n                        fit: BoxFit.contain,\n                        width: platformSize,\n                        height: platformSize,\n                        color: Colors.grey.shade500,\n                      ),\n                    ]\n                  : [],\n            ],\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/GoDownloadSelect.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/screens/components/ComicList.dart';\n\nimport 'DownloadComicsScreen.dart';\n\nAppBar downAppBar(\n  BuildContext context,\n  ComicListController _comicListController,\n  void Function(VoidCallback fn) setState,\n) {\n  return AppBar(\n    actions: [\n      MaterialButton(\n        minWidth: 0,\n        onPressed: () async {\n          setState(() {\n            _comicListController.selecting = false;\n          });\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            const Icon(\n              Icons.cancel_outlined,\n              size: 18,\n              color: Colors.white,\n            ),\n            Text(\n              tr('app.cancel'),\n              style: const TextStyle(fontSize: 14, color: Colors.white),\n            ),\n            Expanded(child: Container()),\n          ],\n        ),\n      ),\n      MaterialButton(\n        minWidth: 0,\n        onPressed: () async {\n          _comicListController.selectAll();\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            const Icon(\n              Icons.select_all,\n              size: 18,\n              color: Colors.white,\n            ),\n            Text(\n              tr('app.select_all'),\n              style: const TextStyle(fontSize: 14, color: Colors.white),\n            ),\n            Expanded(child: Container()),\n          ],\n        ),\n      ),\n      MaterialButton(\n        minWidth: 0,\n        onPressed: () async {\n          var list = _comicListController.selected;\n          if (list.isEmpty) {\n            defaultToast(context, tr(\"app.please_select_comic\"));\n            return;\n          }\n          list = list.toList();\n          setState(() {\n            _comicListController.selecting = false;\n          });\n          Navigator.of(context).push(MaterialPageRoute(\n            builder: (BuildContext context) {\n              return DownloadComicsScreen(list);\n            },\n          ));\n        },\n        child: Column(\n          children: [\n            Expanded(child: Container()),\n            const Icon(\n              Icons.check,\n              size: 18,\n              color: Colors.white,\n            ),\n            Text(\n              tr('app.confirm'),\n              style: const TextStyle(fontSize: 14, color: Colors.white),\n            ),\n            Expanded(child: Container()),\n          ],\n        ),\n      ),\n    ],\n  );\n}\n"
  },
  {
    "path": "lib/screens/components/ImageReader.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\n\nimport 'package:another_xlider/another_xlider.dart';\nimport 'package:pikapika/i18.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:flutter_styled_toast/flutter_styled_toast.dart';\nimport 'package:photo_view/photo_view.dart';\nimport 'package:photo_view/photo_view_gallery.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/basic/config/Address.dart';\nimport 'package:pikapika/basic/config/AutoFullScreen.dart';\nimport 'package:pikapika/basic/config/AutoFullScreenOnForward.dart';\nimport 'package:pikapika/basic/config/CategoriesColumnCount.dart';\nimport 'package:pikapika/basic/config/FullScreenAction.dart';\nimport 'package:pikapika/basic/config/ImageAddress.dart';\nimport 'package:pikapika/basic/config/ImageFilter.dart';\nimport 'package:pikapika/basic/config/KeyboardController.dart';\nimport 'package:pikapika/basic/config/NoAnimation.dart';\nimport 'package:pikapika/basic/config/DragRegionLock.dart';\nimport 'package:pikapika/basic/config/GestureSpeed.dart';\nimport 'package:pikapika/basic/config/Quality.dart';\nimport 'package:pikapika/basic/config/ReaderDirection.dart';\nimport 'package:pikapika/basic/config/ReaderSliderPosition.dart';\nimport 'package:pikapika/basic/config/ReaderTwoPageDirection.dart';\nimport 'package:pikapika/basic/config/ReaderType.dart';\nimport 'package:pikapika/basic/config/ThreeKeepRight.dart';\nimport 'package:pikapika/basic/config/VolumeController.dart';\nimport 'package:pikapika/screens/components/PkzImages.dart';\nimport 'package:scrollable_positioned_list/scrollable_positioned_list.dart';\nimport 'package:zoomable_positioned_list/zoomable_positioned_list.dart' as zoomable;\nimport 'gesture_zoom_box.dart';\nimport '../../basic/config/IconLoading.dart';\nimport '../../basic/config/ReaderBackgroundColor.dart';\nimport '../../basic/config/ReaderScrollByScreenPercentage.dart';\nimport '../../basic/config/WebToonScrollMode.dart';\nimport '../../basic/config/ReaderZoomScale.dart';\nimport '../../basic/config/UseApiLoadImage.dart';\nimport '../../basic/config/VolumeNextChapter.dart';\nimport '../FilePhotoViewScreen.dart';\n\nimport 'Images.dart';\n\n///////////////\n\nEvent<_ReaderControllerEventArgs> _readerControllerEvent =\n    Event<_ReaderControllerEventArgs>();\n\nclass _ReaderControllerEventArgs extends EventArgs {\n  final String key;\n\n  _ReaderControllerEventArgs(this.key);\n}\n\nWidget readerKeyboardHolder(Widget widget) {\n  if (keyboardController &&\n      (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) {\n    widget = RawKeyboardListener(\n      focusNode: FocusNode(),\n      child: widget,\n      autofocus: true,\n      onKey: (event) {\n        if (event is RawKeyDownEvent) {\n          if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {\n            _readerControllerEvent.broadcast(_ReaderControllerEventArgs(\"UP\"));\n          }\n          if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {\n            _readerControllerEvent\n                .broadcast(_ReaderControllerEventArgs(\"DOWN\"));\n          }\n        }\n      },\n    );\n  }\n  return widget;\n}\n\nvoid _onVolumeEvent(dynamic args) {\n  _readerControllerEvent.broadcast(_ReaderControllerEventArgs(\"$args\"));\n}\n\nvar _volumeListenCount = 0;\n\n// 仅支持安卓\n// 监听后会拦截安卓手机音量键\n// 仅最后一次监听生效\n// event可能为DOWN/UP\nEventChannel volumeButtonChannel = const EventChannel(\"volume_button\");\nStreamSubscription? volumeS;\n\nvoid addVolumeListen() {\n  _volumeListenCount++;\n  if (_volumeListenCount == 1) {\n    volumeS =\n        volumeButtonChannel.receiveBroadcastStream().listen(_onVolumeEvent);\n  }\n}\n\nvoid delVolumeListen() {\n  _volumeListenCount--;\n  if (_volumeListenCount == 0) {\n    volumeS?.cancel();\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\n// 对Reader的传参以及封装\n\nclass PkzFile {\n  final String pkzPath;\n  final String path;\n\n  PkzFile(this.pkzPath, this.path);\n}\n\nclass ImageReaderItemPosition {\n  final int index;\n  final double itemLeadingEdge;\n  final double itemTrailingEdge;\n\n  ImageReaderItemPosition(\n      this.index, this.itemLeadingEdge, this.itemTrailingEdge);\n}\n\nclass ReaderImageInfo {\n  final String fileServer;\n  final String path;\n  final String? downloadLocalPath;\n  final int? width;\n  final int? height;\n  final String? format;\n  final int? fileSize;\n  final PkzFile? pkzFile;\n\n  ReaderImageInfo(\n    this.fileServer,\n    this.path,\n    this.downloadLocalPath,\n    this.width,\n    this.height,\n    this.format,\n    this.fileSize, {\n    this.pkzFile,\n  });\n}\n\nclass ImageReaderStruct {\n  final List<ReaderImageInfo> images;\n  final bool fullScreen;\n  final FutureOr<dynamic> Function(bool fullScreen) onFullScreenChange;\n  final FutureOr<dynamic> Function(int) onPositionChange;\n  final int? initPosition;\n  final Map<int, String> epNameMap;\n  final int epOrder;\n  final String comicTitle;\n  final FutureOr<dynamic> Function(int) onChangeEp;\n  final FutureOr<dynamic> Function() onReloadEp;\n  final FutureOr<dynamic> Function() onDownload;\n\n  const ImageReaderStruct({\n    required this.images,\n    required this.fullScreen,\n    required this.onFullScreenChange,\n    required this.onPositionChange,\n    this.initPosition,\n    required this.epNameMap,\n    required this.epOrder,\n    required this.comicTitle,\n    required this.onChangeEp,\n    required this.onReloadEp,\n    required this.onDownload,\n  });\n}\n\n//\n\nclass ImageReader extends StatefulWidget {\n  final ImageReaderStruct struct;\n\n  const ImageReader(this.struct, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ImageReaderState();\n}\n\nclass _ImageReaderState extends State<ImageReader> {\n  // 记录初始方向\n  final ReaderDirection _pagerDirection = gReaderDirection;\n\n  // 记录初始阅读器类型\n  final ReaderType _pagerType = currentReaderType();\n\n  // 记录了控制器\n  late final FullScreenAction _fullScreenAction = currentFullScreenAction();\n\n  late final ReaderSliderPosition _readerSliderPosition =\n      currentReaderSliderPosition();\n\n  @override\n  Widget build(BuildContext context) {\n    return _ImageReaderContent(\n      widget.struct,\n      _pagerDirection,\n      _pagerType,\n      _fullScreenAction,\n      _readerSliderPosition,\n    );\n  }\n}\n\n//\n\nclass _ImageReaderContent extends StatefulWidget {\n  // 记录初始方向\n  final ReaderDirection pagerDirection;\n\n  // 记录初始阅读器类型\n  final ReaderType pagerType;\n\n  final FullScreenAction fullScreenAction;\n\n  final ReaderSliderPosition readerSliderPosition;\n\n  final ImageReaderStruct struct;\n\n  const _ImageReaderContent(this.struct, this.pagerDirection, this.pagerType,\n      this.fullScreenAction, this.readerSliderPosition);\n\n  @override\n  State<StatefulWidget> createState() {\n    switch (pagerType) {\n      case ReaderType.WEB_TOON:\n        return _WebToonReaderState();\n      case ReaderType.WEB_TOON_ZOOM:\n        return _WebToonZoomReaderState();\n      case ReaderType.GALLERY:\n        return _GalleryReaderState();\n      // case ReaderType.WEB_TOON_FREE_ZOOM:\n      //   return _ListViewReaderState();\n      case ReaderType.TWO_PAGE_GALLERY:\n        return _TwoPageGalleryReaderState();\n      default:\n        throw Exception(\"ERROR READER TYPE\");\n    }\n  }\n}\n\nabstract class _ImageReaderContentState extends State<_ImageReaderContent> {\n  bool _sliderDragging = false;\n\n  // 阅读器\n  Widget _buildViewer();\n\n  Widget _buildViewerProcess() {\n    return Stack(\n      children: [\n        processImageFilter(_buildViewer()),\n        if (_sliderDragging) _sliderDraggingText(),\n      ],\n    );\n  }\n\n  Widget _sliderDraggingText() {\n    return Center(\n      child: Container(\n        padding: const EdgeInsets.all(10),\n        decoration: BoxDecoration(\n          color: const Color(0x88000000),\n          borderRadius: BorderRadius.circular(10),\n        ),\n        child: Text(\n          \"${_slider + 1} / ${widget.struct.images.length}\",\n          style: const TextStyle(\n            color: Colors.white,\n            fontSize: 30,\n          ),\n        ),\n      ),\n    );\n  }\n\n  // 键盘, 音量键 等事件\n  void _needJumpTo(int index, bool animation);\n\n  void _needScrollForward();\n\n  void _needScrollBackward();\n\n  double? get _remainingScrollHeight => null;\n\n  // 记录了是否切换了音量\n  late bool _listVolume;\n  \n  // 屏幕上的多个块的信息\n  List<ImageReaderItemPosition> _currentPositions = [];\n\n  void _onPositionsChange(List<ImageReaderItemPosition> positions) {\n    if (positions.isEmpty) return;\n    _currentPositions = positions;\n    var first = positions.reduce((a, b) => a.index < b.index ? a : b);\n    _onCurrentChange(first.index);\n  }\n\n  void _commonWebToonScrollForward(\n    Function(double offset) animateScroll,\n    Function(int index) jumpTo,\n  ) {\n    if (currentWebToonScrollMode() == WebToonScrollMode.SCREEN) {\n      double s;\n      if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n        s = MediaQuery.of(context).size.height;\n      } else {\n        s = MediaQuery.of(context).size.width;\n      }\n      var scrollSize = s * readerScrollByScreenPercentage;\n      animateScroll(scrollSize);\n    } else {\n      if (_currentPositions.isNotEmpty) {\n        var min =\n            _currentPositions.reduce((a, b) => a.index < b.index ? a : b);\n        var max =\n            _currentPositions.reduce((a, b) => a.index > b.index ? a : b);\n        if (min.index != max.index) {\n          // 多个图片\n          // 只要最后一个图片没有显示完全, 就把最后一个图片对齐\n          if (max.itemTrailingEdge > 1) {\n            jumpTo(max.index);\n          } else {\n            jumpTo(max.index + 1);\n          }\n        } else {\n          // 一个图片\n          jumpTo(max.index + 1);\n        }\n      } else {\n        jumpTo(_current + 1);\n      }\n    }\n  }\n\n  void _commonWebToonScrollBackward(\n    Function(double offset) animateScroll,\n    Function(int index) jumpTo,\n  ) {\n    if (currentWebToonScrollMode() == WebToonScrollMode.SCREEN) {\n      double s;\n      if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n        s = MediaQuery.of(context).size.height;\n      } else {\n        s = MediaQuery.of(context).size.width;\n      }\n      var scrollSize = s * readerScrollByScreenPercentage;\n      animateScroll(-scrollSize);\n    } else {\n      if (_currentPositions.isNotEmpty) {\n        var min =\n            _currentPositions.reduce((a, b) => a.index < b.index ? a : b);\n        // 第一张图片没有显示的部分超过10像素则对齐, 否则要显示更上一张\n        var leading = min.itemLeadingEdge;\n        var height = MediaQuery.of(context).size.height;\n        if (leading < 0 && leading.abs() * height > 10) {\n          jumpTo(min.index);\n        } else {\n          jumpTo(min.index - 1);\n        }\n      } else {\n        jumpTo(_current - 1);\n      }\n    }\n  }\n\n  // 和初始化与翻页有关\n\n  @override\n  void initState() {\n    _initCurrent();\n    _readerControllerEvent.subscribe(_onPageControl);\n    _listVolume = volumeController;\n    if (_listVolume) {\n      addVolumeListen();\n    }\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _readerControllerEvent.unsubscribe(_onPageControl);\n    if (_listVolume) {\n      delVolumeListen();\n    }\n    super.dispose();\n  }\n\n  void _onPageControl(_ReaderControllerEventArgs? args) {\n    if (args != null) {\n      var event = args.key;\n      switch (event) {\n        case \"UP\":\n          if (ReaderType.WEB_TOON == currentReaderType() ||\n              // ReaderType.WEB_TOON_FREE_ZOOM == currentReaderType() ||\n              (ReaderType.WEB_TOON_ZOOM == currentReaderType() &&\n                  currentWebToonScrollMode() == WebToonScrollMode.SCREEN)) {\n            _needScrollBackward();\n            break;\n          }\n          if (_current > 0) {\n            _needJumpTo(_current - 1, true);\n          }\n          break;\n        case \"DOWN\":\n          if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) {\n            widget.struct.onFullScreenChange(true);\n          }\n          if (ReaderType.WEB_TOON == currentReaderType() ||\n              // ReaderType.WEB_TOON_FREE_ZOOM == currentReaderType() ||\n              (ReaderType.WEB_TOON_ZOOM == currentReaderType() &&\n                  currentWebToonScrollMode() == WebToonScrollMode.SCREEN)) {\n            _needScrollForward();\n            if ((_remainingScrollHeight ?? 10000) < 100) {\n              if (volumeNextChapter()) {\n                final now = DateTime.now().millisecondsSinceEpoch;\n                if (_noticeTime + 3000 > now) {\n                  if (_hasNextEp()) {\n                    _onNextAction();\n                  } else {\n                    defaultToast(context,\n                        tr('components.image_reader.already_at_the_end'));\n                  }\n                } else {\n                  _noticeTime = now;\n                  defaultToast(context,\n                      tr('components.image_reader.click_to_next_chapter'));\n                }\n              }\n            }\n            break;\n          }\n          int point = 1;\n          if (ReaderType.TWO_PAGE_GALLERY == currentReaderType()) {\n            point = 2;\n          }\n          if (_current < widget.struct.images.length - point) {\n            _needJumpTo(_current + point, true);\n          } else {\n            if (volumeNextChapter()) {\n              final now = DateTime.now().millisecondsSinceEpoch;\n              if (_noticeTime + 3000 > now) {\n                if (_hasNextEp()) {\n                  _onNextAction();\n                } else {\n                  defaultToast(context, tr('components.image_reader.already_at_the_end'));\n                }\n              } else {\n                _noticeTime = now;\n                defaultToast(context, tr('components.image_reader.click_to_next_chapter'));\n              }\n            }\n          }\n          break;\n      }\n    }\n  }\n\n  int _noticeTime = 0;\n\n  late int _startIndex;\n  late int _current;\n  late int _slider;\n  bool? _hasNextEpCache;\n\n  void _initCurrent() {\n    if (widget.struct.initPosition != null &&\n        widget.struct.images.length > widget.struct.initPosition!) {\n      _startIndex = widget.struct.initPosition!;\n    } else {\n      _startIndex = 0;\n    }\n    _current = _startIndex;\n    _slider = _startIndex;\n  }\n\n  void _onCurrentChange(int index) {\n    if (index != _current) {\n      setState(() {\n        _current = index;\n        _slider = index;\n        widget.struct.onPositionChange(index);\n      });\n    }\n  }\n\n  // 与显示有关的方法\n\n  @override\n  Widget build(BuildContext context) {\n    switch (currentFullScreenAction()) {\n      // 按钮\n      case FullScreenAction.CONTROLLER:\n        return Stack(\n          children: [\n            _buildViewerProcess(),\n            _buildBar(_buildFullScreenControllerStackItem()),\n          ],\n        );\n      case FullScreenAction.TOUCH_ONCE:\n        return Stack(\n          children: [\n            _buildTouchOnceControllerAction(_buildViewerProcess()),\n            _buildBar(Container()),\n          ],\n        );\n      case FullScreenAction.TOUCH_DOUBLE:\n        return Stack(\n          children: [\n            _buildTouchDoubleControllerAction(_buildViewerProcess()),\n            _buildBar(Container()),\n          ],\n        );\n      case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT:\n        return Stack(\n          children: [\n            _buildTouchDoubleOnceNextControllerAction(_buildViewerProcess()),\n            _buildBar(Container()),\n          ],\n        );\n      case FullScreenAction.THREE_AREA:\n        return Stack(\n          children: [\n            _buildViewerProcess(),\n            _buildBar(_buildThreeAreaControllerAction()),\n          ],\n        );\n    }\n  }\n\n  Widget _buildBar(Widget child) {\n    switch (widget.readerSliderPosition) {\n      case ReaderSliderPosition.BOTTOM:\n        return Column(\n          children: [\n            _buildAppBar(),\n            Expanded(child: child),\n            widget.struct.fullScreen\n                ? Container()\n                : Container(\n                    height: 45,\n                    color: const Color(0x88000000),\n                    child: Row(\n                      crossAxisAlignment: CrossAxisAlignment.center,\n                      children: [\n                        Container(width: 15),\n                        IconButton(\n                          icon: const Icon(Icons.fullscreen),\n                          color: Colors.white,\n                          onPressed: () {\n                            widget.struct\n                                .onFullScreenChange(!widget.struct.fullScreen);\n                          },\n                        ),\n                        Container(width: 10),\n                        Expanded(\n                          child:\n                          //widget.pagerType != ReaderType.WEB_TOON_FREE_ZOOM\n                            //      ?\n                              _buildSliderBottom()\n                              //    : Container(),\n                        ),\n                        Container(width: 10),\n                        IconButton(\n                          icon: const Icon(Icons.skip_next_outlined),\n                          color: Colors.white,\n                          onPressed: _onNextAction,\n                        ),\n                        Container(width: 15),\n                      ],\n                    ),\n                  ),\n            widget.struct.fullScreen\n                ? Container()\n                : Container(\n                    color: const Color(0x88000000),\n                    child: SafeArea(\n                      top: false,\n                      child: Container(),\n                    ),\n                  ),\n          ],\n        );\n      case ReaderSliderPosition.RIGHT:\n        return Column(\n          children: [\n            _buildAppBar(),\n            Expanded(\n              child: Stack(\n                children: [\n                  child,\n                  _buildSliderRight(),\n                ],\n              ),\n            ),\n          ],\n        );\n      case ReaderSliderPosition.LEFT:\n        return Column(\n          children: [\n            _buildAppBar(),\n            Expanded(\n              child: Stack(\n                children: [\n                  child,\n                  _buildSliderLeft(),\n                ],\n              ),\n            ),\n          ],\n        );\n    }\n  }\n\n  Widget _buildAppBar() => widget.struct.fullScreen\n      ? Container()\n      : AppBar(\n          title: Text(\n              \"${widget.struct.epNameMap[widget.struct.epOrder] ?? \"\"} - ${widget.struct.comicTitle}\"),\n          actions: [\n            IconButton(\n              onPressed: _onChooseEp,\n              icon: const Icon(Icons.menu_open),\n            ),\n            IconButton(\n              onPressed: _onMoreSetting,\n              icon: const Icon(Icons.more_horiz),\n            ),\n          ],\n        );\n\n  Widget _buildSliderBottom() {\n    return Column(\n      children: [\n        Expanded(child: Container()),\n        SizedBox(\n          height: 25,\n          child: _buildSliderWidget(Axis.horizontal),\n        ),\n        Expanded(child: Container()),\n      ],\n    );\n  }\n\n  Widget _buildSliderLeft() => widget.struct.fullScreen\n      ? Container()\n      : Align(\n          alignment: Alignment.centerLeft,\n          child: Material(\n            color: Colors.transparent,\n            child: Container(\n              width: 35,\n              height: 300,\n              decoration: const BoxDecoration(\n                color: Color(0x66000000),\n                borderRadius: BorderRadius.only(\n                  topRight: Radius.circular(10),\n                  bottomRight: Radius.circular(10),\n                ),\n              ),\n              padding:\n                  const EdgeInsets.only(top: 10, bottom: 10, left: 6, right: 5),\n              child: Center(\n                child: _buildSliderWidget(Axis.vertical),\n              ),\n            ),\n          ),\n        );\n\n  Widget _buildSliderRight() => widget.struct.fullScreen\n      ? Container()\n      : Align(\n          alignment: Alignment.centerRight,\n          child: Material(\n            color: Colors.transparent,\n            child: Container(\n              width: 35,\n              height: 300,\n              decoration: const BoxDecoration(\n                color: Color(0x66000000),\n                borderRadius: BorderRadius.only(\n                  topLeft: Radius.circular(10),\n                  bottomLeft: Radius.circular(10),\n                ),\n              ),\n              padding:\n                  const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 6),\n              child: Center(\n                child: _buildSliderWidget(Axis.vertical),\n              ),\n            ),\n          ),\n        );\n\n  Widget _buildSliderWidget(Axis axis) {\n    return FlutterSlider(\n      axis: axis,\n      values: [_slider.toDouble()],\n      min: 0,\n      max: (widget.struct.images.length - 1).toDouble(),\n      onDragStarted: (handlerIndex, lowerValue, upperValue) {\n        setState(() {\n          _sliderDragging = true;\n        });\n      },\n      onDragging: (handlerIndex, lowerValue, upperValue) {\n        setState(() {\n          _slider = (lowerValue.toInt());\n        });\n      },\n      onDragCompleted: (handlerIndex, lowerValue, upperValue) {\n        setState(() {\n          _sliderDragging = false;\n        });\n        _slider = (lowerValue.toInt());\n        if (_slider != _current) {\n          _needJumpTo(_slider, false);\n        }\n      },\n      trackBar: FlutterSliderTrackBar(\n        inactiveTrackBar: BoxDecoration(\n          borderRadius: BorderRadius.circular(20),\n          color: Colors.grey.shade300,\n        ),\n        activeTrackBar: BoxDecoration(\n          borderRadius: BorderRadius.circular(4),\n          color: Theme.of(context).colorScheme.secondary,\n        ),\n      ),\n      step: const FlutterSliderStep(\n        step: 1,\n        isPercentRange: false,\n      ),\n      tooltip: FlutterSliderTooltip(disabled: true),\n    );\n  }\n\n  Widget _buildFullScreenControllerStackItem() {\n    if (widget.readerSliderPosition == ReaderSliderPosition.BOTTOM &&\n        !widget.struct.fullScreen) {\n      return Container();\n    }\n    if (widget.readerSliderPosition == ReaderSliderPosition.RIGHT) {\n      return SafeArea(\n          child: Align(\n        alignment: Alignment.bottomRight,\n        child: Material(\n          color: Colors.transparent,\n          child: Container(\n            padding:\n                const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4),\n            margin: const EdgeInsets.only(bottom: 10),\n            decoration: const BoxDecoration(\n              borderRadius: BorderRadius.only(\n                topLeft: Radius.circular(10),\n                bottomLeft: Radius.circular(10),\n              ),\n              color: Color(0x88000000),\n            ),\n            child: GestureDetector(\n              onTap: () {\n                widget.struct.onFullScreenChange(!widget.struct.fullScreen);\n              },\n              child: Icon(\n                widget.struct.fullScreen\n                    ? Icons.fullscreen_exit\n                    : Icons.fullscreen_outlined,\n                size: 30,\n                color: Colors.white,\n              ),\n            ),\n          ),\n        ),\n      ));\n    }\n    return SafeArea(\n        child: Align(\n      alignment: Alignment.bottomLeft,\n      child: Material(\n        color: Colors.transparent,\n        child: Container(\n          padding:\n              const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4),\n          margin: const EdgeInsets.only(bottom: 10),\n          decoration: const BoxDecoration(\n            borderRadius: BorderRadius.only(\n              topRight: Radius.circular(10),\n              bottomRight: Radius.circular(10),\n            ),\n            color: Color(0x88000000),\n          ),\n          child: GestureDetector(\n            onTap: () {\n              widget.struct.onFullScreenChange(!widget.struct.fullScreen);\n            },\n            child: Icon(\n              widget.struct.fullScreen\n                  ? Icons.fullscreen_exit\n                  : Icons.fullscreen_outlined,\n              size: 30,\n              color: Colors.white,\n            ),\n          ),\n        ),\n      ),\n    ));\n  }\n\n  Widget _buildTouchOnceControllerAction(Widget child) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: () {\n        widget.struct.onFullScreenChange(!widget.struct.fullScreen);\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildTouchDoubleControllerAction(Widget child) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onDoubleTap: () {\n        widget.struct.onFullScreenChange(!widget.struct.fullScreen);\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildTouchDoubleOnceNextControllerAction(Widget child) {\n    return GestureDetector(\n      behavior: HitTestBehavior.translucent,\n      onTap: () {\n        _readerControllerEvent.broadcast(_ReaderControllerEventArgs(\"DOWN\"));\n      },\n      onDoubleTap: () {\n        widget.struct.onFullScreenChange(!widget.struct.fullScreen);\n      },\n      child: child,\n    );\n  }\n\n  Widget _buildThreeAreaControllerAction() {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var up = Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () {\n              _readerControllerEvent\n                  .broadcast(_ReaderControllerEventArgs(\"UP\"));\n            },\n            child: Container(),\n          ),\n        );\n        var down = Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () {\n              _readerControllerEvent\n                  .broadcast(_ReaderControllerEventArgs(\"DOWN\"));\n            },\n            child: Container(),\n          ),\n        );\n        var fullScreen = Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () =>\n                widget.struct.onFullScreenChange(!widget.struct.fullScreen),\n            child: Container(),\n          ),\n        );\n        late Widget child;\n        switch (widget.pagerDirection) {\n          case ReaderDirection.TOP_TO_BOTTOM:\n            child = Column(children: [\n              up,\n              fullScreen,\n              down,\n            ]);\n            break;\n          case ReaderDirection.LEFT_TO_RIGHT:\n            child = Row(children: [\n              up,\n              fullScreen,\n              down,\n            ]);\n            break;\n          case ReaderDirection.RIGHT_TO_LEFT:\n            if (threeKeepRight) {\n              child = Row(children: [\n                up,\n                fullScreen,\n                down,\n              ]);\n              break;\n            }\n            child = Row(children: [\n              down,\n              fullScreen,\n              up,\n            ]);\n            break;\n        }\n        return SizedBox(\n          width: constraints.maxWidth,\n          height: constraints.maxHeight,\n          child: child,\n        );\n      },\n    );\n  }\n\n  Future _onChooseEp() async {\n    showModalBottomSheet(\n      context: context,\n      backgroundColor: const Color(0xAA000000),\n      isScrollControlled: true,\n      builder: (context) {\n        return SizedBox(\n          height: MediaQuery.of(context).size.height * (.45),\n          child: _EpChooser(\n            widget.struct.epNameMap,\n            widget.struct.epOrder,\n            widget.struct.onChangeEp,\n          ),\n        );\n      },\n    );\n  }\n\n  Future _onMoreSetting() async {\n    // 记录开始的画质\n    final currentQuality = currentQualityCode();\n    final cReaderSliderPosition = currentReaderSliderPosition();\n    //\n    await showModalBottomSheet(\n      context: context,\n      backgroundColor: const Color(0xAA000000),\n      isScrollControlled: true,\n      builder: (context) {\n        return SizedBox(\n          height: MediaQuery.of(context).size.height * (.45),\n          child: _SettingPanel(\n            widget.struct.onReloadEp,\n            widget.struct.onDownload,\n          ),\n        );\n      },\n    );\n    setState(() {});\n    if (widget.pagerDirection != gReaderDirection ||\n        widget.pagerType != currentReaderType() ||\n        currentQuality != currentQualityCode() ||\n        widget.fullScreenAction != currentFullScreenAction() ||\n        cReaderSliderPosition != currentReaderSliderPosition()) {\n      widget.struct.onReloadEp();\n    }\n  }\n\n  // 给子类调用的方法\n\n  bool _fullscreenController() {\n    switch (currentFullScreenAction()) {\n      case FullScreenAction.CONTROLLER:\n        return false;\n      case FullScreenAction.TOUCH_ONCE:\n        return false;\n      case FullScreenAction.TOUCH_DOUBLE:\n        return false;\n      case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT:\n        return false;\n      case FullScreenAction.THREE_AREA:\n        return true;\n    }\n  }\n\n  Future _onNextAction() async {\n    if (_hasNextEp()) {\n      widget.struct.onChangeEp(widget.struct.epOrder + 1);\n    } else {\n      defaultToast(context, tr('components.image_reader.already_at_the_end'));\n    }\n  }\n\n  bool _hasNextEp() {\n    if (_hasNextEpCache != null) {\n      return _hasNextEpCache!;\n    }\n    for (var element in widget.struct.epNameMap.keys) {\n      if (element > widget.struct.epOrder) {\n        _hasNextEpCache = true;\n        return true;\n      }\n    }\n    _hasNextEpCache = false;\n    return false;\n  }\n\n  double _topBarHeight() => Scaffold.of(context).appBarMaxHeight ?? 0;\n\n  double _bottomBarHeight() =>\n      widget.readerSliderPosition == ReaderSliderPosition.BOTTOM ? 45 : 0;\n}\n\nclass _EpChooser extends StatefulWidget {\n  final Map<int, String> epNameMap;\n  final int epOrder;\n  final FutureOr Function(int) onChangeEp;\n\n  _EpChooser(this.epNameMap, this.epOrder, this.onChangeEp);\n\n  @override\n  State<StatefulWidget> createState() => _EpChooserState();\n}\n\nclass _EpChooserState extends State<_EpChooser> {\n  @override\n  Widget build(BuildContext context) {\n    var entries = widget.epNameMap.entries.toList();\n    entries.sort((a, b) => a.key - b.key);\n    var widgets = [\n      Container(height: 20),\n      ...entries.map((e) {\n        return Container(\n          margin: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5),\n          decoration: BoxDecoration(\n            color: widget.epOrder == e.key ? Colors.grey.withAlpha(100) : null,\n            border: Border.all(\n              color: const Color(0xff484c60),\n              style: BorderStyle.solid,\n              width: .5,\n            ),\n          ),\n          child: MaterialButton(\n            onPressed: () {\n              Navigator.of(context).pop();\n              widget.onChangeEp(e.key);\n            },\n            textColor: Colors.white,\n            child: Text(e.value),\n          ),\n        );\n      })\n    ];\n    return ScrollablePositionedList.builder(\n      initialScrollIndex: widget.epOrder < 2 ? 0 : widget.epOrder - 2,\n      itemCount: widgets.length,\n      itemBuilder: (BuildContext context, int index) => widgets[index],\n    );\n  }\n}\n\nclass _SettingPanel extends StatefulWidget {\n  final FutureOr Function() onReloadEp;\n  final FutureOr Function() onDownload;\n\n  _SettingPanel(this.onReloadEp, this.onDownload);\n\n  @override\n  State<StatefulWidget> createState() => _SettingPanelState();\n}\n\nclass _SettingPanelState extends State<_SettingPanel> {\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      padding: const EdgeInsets.all(15),\n      children: [\n        _chooseTile(\n          icon: Icons.crop_sharp,\n          title: tr(\"settings.reader_direction.title\"),\n          value: gReaderDirectionName(),\n          onTap: () async {\n            await choosePagerDirection(context);\n            setState(() {});\n          },\n        ),\n        _chooseTile(\n          icon: Icons.view_day_outlined,\n          title: tr(\"settings.reader_type.title\"),\n          value: currentReaderTypeName(),\n          onTap: () async {\n            await choosePagerType(context);\n            setState(() {});\n          },\n        ),\n        _chooseTile(\n          icon: Icons.image_aspect_ratio_outlined,\n          title: tr(\"settings.quality.title\"),\n          value: currentQualityName(),\n          onTap: () async {\n            await chooseQuality(context);\n            setState(() {});\n          },\n        ),\n        _chooseTile(\n          icon: Icons.control_camera_outlined,\n          title: tr(\"settings.full_screen_action.title\"),\n          value: currentFullScreenActionName(),\n          onTap: () async {\n            await chooseFullScreenAction(context);\n            setState(() {});\n          },\n        ),\n        const Divider(color: Colors.white24),\n        _chooseTile(\n          icon: Icons.swap_vert,\n          title: tr(\"settings.web_toon_scroll_mode.title\"),\n          value: currentWebToonScrollModeName(),\n          onTap: () async {\n            await chooseWebToonScrollMode(context);\n            setState(() {});\n          },\n        ),\n        _sliderTile(\n          icon: Icons.straighten,\n          title: tr(\"settings.reader_scroll_by_screen_percentage.title\"),\n          valueLabel:\n              \"${currentReaderScrollByScreenPercentage()}%${tr(\"settings.reader_scroll_by_screen_percentage.screen_size\")}\",\n          min: 5,\n          max: 110,\n          divisions: 110 - 5,\n          value: currentReaderScrollByScreenPercentage().toDouble(),\n          onChanged: (v) async {\n            await setReaderScrollByScreenPercentage(v.toInt());\n            setState(() {});\n          },\n        ),\n        const Divider(color: Colors.white24),\n        _sliderTile(\n          icon: Icons.zoom_out_map,\n          title: tr(\"settings.reader_zoom.out_title\"),\n          valueLabel: \"${readerZoomMinScale.toStringAsFixed(1)}x\",\n          min: 0.1,\n          max: 1.0,\n          divisions: 9,\n          value: readerZoomMinScale.clamp(0.1, 1.0).toDouble(),\n          onChanged: (v) async {\n            final newValue = (v * 10).roundToDouble() / 10;\n            await setReaderZoomMinScale(newValue);\n            setState(() {});\n          },\n        ),\n        _sliderTile(\n          icon: Icons.zoom_in,\n          title: tr(\"settings.reader_zoom.in_title\"),\n          valueLabel: \"${readerZoomMaxScale.toStringAsFixed(1)}x\",\n          min: 1.0,\n          max: 30.0,\n          divisions: 29,\n          value: readerZoomMaxScale.clamp(1.0, 30.0).toDouble(),\n          onChanged: (v) async {\n            final newValue = v.roundToDouble();\n            await setReaderZoomMaxScale(newValue);\n            setState(() {});\n          },\n        ),\n        _sliderTile(\n          icon: Icons.touch_app,\n          title: tr(\"settings.reader_zoom.double_tap_title\"),\n          valueLabel: \"${readerZoomDoubleTapScale.toStringAsFixed(1)}x\",\n          min: 1.5,\n          max: 5.0,\n          divisions: 7,\n          value: readerZoomDoubleTapScale.clamp(1.5, 5.0).toDouble(),\n          onChanged: (v) async {\n            final newValue = (v * 2).roundToDouble() / 2;\n            await setReaderZoomDoubleTapScale(newValue);\n            setState(() {});\n          },\n        ),\n        _switchTile(\n          icon: Icons.border_inner,\n          title: tr('settings.drag_region_lock.title'),\n          value: dragRegionLock(),\n          onChanged: (v) async {\n            await setDragRegionLock(v);\n            setState(() {});\n          },\n        ),\n        _sliderTile(\n          icon: Icons.speed,\n          title: tr('settings.gesture_speed.title'),\n          valueLabel: \"${currentGestureSpeed().toStringAsFixed(1)}x\",\n          min: 0.1,\n          max: 5.0,\n          divisions: 49,\n          value: currentGestureSpeed(),\n          onChanged: (v) async {\n            await setGestureSpeed(v);\n            setState(() {});\n          },\n        ),\n        _switchTile(\n          icon: Icons.arrow_downward,\n          title: tr(\"settings.auto_full_screen_on_forward.title\"),\n          value: currentAutoFullScreenOnForward(),\n          onChanged: (v) async {\n            await setAutoFullScreenOnForward(v);\n            setState(() {});\n          },\n        ),\n        const Divider(color: Colors.white24),\n        _chooseTile(\n          icon: Icons.share,\n          title: tr('net.address'),\n          value: currentAddressName(),\n          onTap: () async {\n            await chooseAddressAndSwitch(context);\n            setState(() {});\n          },\n        ),\n        _chooseTile(\n          icon: Icons.image_search,\n          title: tr('settings.image_address.title'),\n          value: currentImageAddressName(),\n          onTap: () async {\n            await chooseImageAddress(context);\n            setState(() {});\n          },\n        ),\n        _chooseTile(\n          icon: Icons.network_ping,\n          title: tr('net.use_api_load_image'),\n          value: currentUseApiLoadImageName(),\n          onTap: () async {\n            await chooseUseApiLoadImage(context);\n            setState(() {});\n          },\n        ),\n        const Divider(color: Colors.white24),\n        _switchTile(\n          icon: Icons.access_time_filled_outlined,\n          title: tr('settings.no_animation.title'),\n          value: noAnimation(),\n          onChanged: (v) async {\n            await setNoAnimation(v);\n            setState(() {});\n          },\n        ),\n        _actionTile(\n          icon: Icons.refresh,\n          title: tr('components.image_reader.reload_page'),\n          onTap: () {\n            Navigator.of(context).pop();\n            widget.onReloadEp();\n          },\n        ),\n      ],\n    );\n  }\n\n  Widget _chooseTile({\n    required IconData icon,\n    required String title,\n    required String value,\n    required FutureOr<void> Function() onTap,\n  }) {\n    return ListTile(\n      leading: Icon(icon, color: Colors.white),\n      title: Text(title, style: const TextStyle(color: Colors.white)),\n      trailing: Row(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          ConstrainedBox(\n            constraints: const BoxConstraints(maxWidth: 100),\n            child: Text(\n              value,\n              textAlign: TextAlign.right,\n              maxLines: 1,\n              overflow: TextOverflow.ellipsis,\n              style: const TextStyle(color: Colors.white70),\n            ),\n          ),\n          const SizedBox(width: 4),\n          const Icon(Icons.chevron_right, color: Colors.white70),\n        ],\n      ),\n      onTap: () async => await onTap(),\n    );\n  }\n\n  Widget _actionTile({\n    required IconData icon,\n    required String title,\n    required VoidCallback onTap,\n  }) {\n    return ListTile(\n      leading: Icon(icon, color: Colors.white),\n      title: Text(title, style: const TextStyle(color: Colors.white)),\n      trailing: const Icon(Icons.chevron_right, color: Colors.white70),\n      onTap: onTap,\n    );\n  }\n\n  Widget _switchTile({\n    required IconData icon,\n    required String title,\n    required bool value,\n    required FutureOr<void> Function(bool) onChanged,\n  }) {\n    return ListTile(\n      leading: Icon(icon, color: Colors.white),\n      title: Text(title, style: const TextStyle(color: Colors.white)),\n      trailing: Switch(\n        value: value,\n        onChanged: (v) async => await onChanged(v),\n        activeColor: Theme.of(context).colorScheme.secondary,\n      ),\n    );\n  }\n\n  Widget _sliderTile({\n    required IconData icon,\n    required String title,\n    required String valueLabel,\n    required double min,\n    required double max,\n    required int divisions,\n    required double value,\n    required FutureOr<void> Function(double) onChanged,\n  }) {\n    return ListTile(\n      leading: Icon(icon, color: Colors.white),\n      title: Text(title, style: const TextStyle(color: Colors.white)),\n      subtitle: Row(\n        children: [\n          Expanded(\n            child: Slider(\n              min: min,\n              max: max,\n              divisions: divisions,\n              value: value,\n              onChanged: (v) async => await onChanged(v),\n            ),\n          ),\n          SizedBox(\n            width: 50,\n            child: Text(\n              valueLabel,\n              style: const TextStyle(color: Colors.white70, fontSize: 13),\n              textAlign: TextAlign.end,\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nclass _WebToonReaderState extends _ImageReaderContentState {\n  var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n  late final List<Size?> _trueSizes = [];\n  late final zoomable.ItemScrollController _itemScrollController;\n  late final zoomable.ItemPositionsListener _itemPositionsListener;\n  late final zoomable.ScrollOffsetController _scrollOffsetController;\n  late final zoomable.ScrollOffsetListener _scrollOffsetListener;\n  StreamSubscription<double>? _scrollOffsetSubscription;\n\n  @override\n  void initState() {\n    for (var e in widget.struct.images) {\n      if (e.pkzFile != null &&\n          e.width != null &&\n          e.height != null &&\n          e.width! > 0 &&\n          e.height! > 0) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else if (e.downloadLocalPath != null) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else {\n        _trueSizes.add(null);\n      }\n    }\n    _itemScrollController = zoomable.ItemScrollController();\n    _itemPositionsListener = zoomable.ItemPositionsListener.create();\n    _itemPositionsListener.itemPositions.addListener(_onListCurrentChange);\n    _scrollOffsetController = zoomable.ScrollOffsetController();\n    _scrollOffsetListener = zoomable.ScrollOffsetListener.create();\n    _scrollOffsetSubscription = _scrollOffsetListener.changes.listen(_onScroll);\n    super.initState();\n  }\n\n  void _onScroll(double delta) {\n    if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) {\n      if (delta > 0) {\n        widget.struct.onFullScreenChange(true);\n      }\n    }\n  }\n\n  @override\n  double? get _remainingScrollHeight {\n    try {\n      return _scrollOffsetController.maxScrollExtent -\n          _scrollOffsetController.offset;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  @override\n  void dispose() {\n    _itemPositionsListener.itemPositions.removeListener(_onListCurrentChange);\n    _scrollOffsetSubscription?.cancel();\n    super.dispose();\n  }\n\n  void _onListCurrentChange() {\n    var positions = _itemPositionsListener.itemPositions.value;\n    if (positions.isNotEmpty) {\n      super._onPositionsChange(positions\n          .map((e) => ImageReaderItemPosition(\n              e.index, e.itemLeadingEdge, e.itemTrailingEdge))\n          .toList());\n    }\n  }\n\n  @override\n  void _needJumpTo(int index, bool animation) {\n    if (noAnimation() || animation == false) {\n      _itemScrollController.jumpTo(\n        index: index,\n      );\n    } else {\n      if (DateTime.now().millisecondsSinceEpoch < _controllerTime) {\n        return;\n      }\n      _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n      _itemScrollController.scrollTo(\n        index: index,\n        duration: const Duration(milliseconds: 400),\n      );\n    }\n  }\n\n  @override\n  void _needScrollForward() {\n    _commonWebToonScrollForward(\n      (offset) {\n        _scrollOffsetController.animateScroll(\n          offset: offset,\n          duration: noAnimation()\n              ? Duration.zero\n              : const Duration(milliseconds: 200),\n          curve: Curves.easeOut,\n        );\n      },\n      (index) {\n        _needJumpTo(index, true);\n      },\n    );\n  }\n\n  @override\n  void _needScrollBackward() {\n    _commonWebToonScrollBackward(\n      (offset) {\n        _scrollOffsetController.animateScroll(\n          offset: offset,\n          duration: noAnimation()\n              ? Duration.zero\n              : const Duration(milliseconds: 200),\n          curve: Curves.easeOut,\n        );\n      },\n      (index) {\n        _needJumpTo(index, true);\n      },\n    );\n  }\n\n  @override\n  Widget _buildViewer() {\n    return Container(\n      decoration: BoxDecoration(\n        color: readerBackgroundColorObj,\n      ),\n      child: _buildList(),\n    );\n  }\n\n  Widget _buildList() {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        // reload _images size\n        List<Widget> _images = [];\n        for (var index = 0; index < widget.struct.images.length; index++) {\n          late Size renderSize;\n          if (_trueSizes[index] != null) {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(\n                constraints.maxWidth,\n                constraints.maxWidth *\n                    _trueSizes[index]!.height /\n                    _trueSizes[index]!.width,\n              );\n            } else {\n              var maxHeight = constraints.maxHeight -\n                  super._topBarHeight() -\n                  super._bottomBarHeight() -\n                  MediaQuery.of(context).padding.bottom;\n              renderSize = Size(\n                maxHeight *\n                    _trueSizes[index]!.width /\n                    _trueSizes[index]!.height,\n                maxHeight,\n              );\n            }\n          } else {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2);\n            } else {\n              // ReaderDirection.LEFT_TO_RIGHT\n              // ReaderDirection.RIGHT_TO_LEFT\n              renderSize =\n                  Size(constraints.maxWidth / 2, constraints.maxHeight);\n            }\n          }\n          var currentIndex = index;\n          onTrueSize(Size size) {\n            setState(() {\n              _trueSizes[currentIndex] = size;\n            });\n          }\n\n          var e = widget.struct.images[index];\n          if (e.pkzFile != null) {\n            _images.add(_WebToonPkzImage(\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n              pkzFile: e.pkzFile!,\n            ));\n          } else if (e.downloadLocalPath != null) {\n            _images.add(_WebToonDownloadImage(\n              fileServer: e.fileServer,\n              path: e.path,\n              localPath: e.downloadLocalPath!,\n              fileSize: e.fileSize!,\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n            ));\n          } else {\n            _images.add(_WebToonRemoteImage(\n              e.fileServer,\n              e.path,\n              renderSize,\n              onTrueSize,\n            ));\n          }\n        }\n        return zoomable.ZoomablePositionedList.builder(\n          enableZoom: false,\n          scrollOffsetController: _scrollOffsetController,\n          scrollOffsetListener: _scrollOffsetListener,\n          initialScrollIndex: super._startIndex,\n          scrollDirection:\n              widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                  ? Axis.vertical\n                  : Axis.horizontal,\n          reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,\n          padding: EdgeInsets.only(\n            // 不管全屏与否, 滚动方向如何, 顶部永远保持间距\n            top: super._topBarHeight(),\n            bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                ? 130 // 纵向滚动 底部永远都是130的空白\n                : (super._bottomBarHeight() +\n                    MediaQuery.of(context).padding.bottom)\n            // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果\n            ,\n          ),\n          itemScrollController: _itemScrollController,\n          itemPositionsListener: _itemPositionsListener,\n          itemCount: widget.struct.images.length + 1,\n          itemBuilder: (BuildContext context, int index) {\n            if (index >= widget.struct.images.length) {\n              return _buildNextEp();\n            }\n            return _images[index];\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildNextEp() {\n    if (super._fullscreenController()) {\n      return Container();\n    }\n    return Container(\n      color: Colors.transparent,\n      padding: const EdgeInsets.all(20),\n      child: MaterialButton(\n        onPressed: () {\n          if (super._hasNextEp()) {\n            super._onNextAction();\n          } else {\n            Navigator.of(context).pop();\n          }\n        },\n        textColor: invertColor(readerBackgroundColorObj),\n        child: Container(\n          padding: const EdgeInsets.only(top: 40, bottom: 40),\n          child: Text(super._hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading')),\n        ),\n      ),\n    );\n  }\n}\n\n// 来自下载\nclass _WebToonDownloadImage extends _WebToonReaderImage {\n  final String fileServer;\n  final String path;\n  final String localPath;\n  final int fileSize;\n  final int width;\n  final int height;\n  final String format;\n\n  _WebToonDownloadImage({\n    required this.fileServer,\n    required this.path,\n    required this.localPath,\n    required this.fileSize,\n    required this.width,\n    required this.height,\n    required this.format,\n    required Size size,\n    Function(Size)? onTrueSize,\n  }) : super(size, onTrueSize);\n\n  @override\n  Future<RemoteImageData> imageData() async {\n    if (localPath == \"\") {\n      return method.remoteImageData(fileServer, path);\n    }\n    var finalPath = await method.downloadImagePath(localPath);\n    return RemoteImageData.forData(\n      fileSize,\n      format,\n      width,\n      height,\n      finalPath,\n    );\n  }\n}\n\n// 来自PKZ\nclass _WebToonPkzImage extends StatelessWidget {\n  final PkzFile pkzFile;\n  final int width;\n  final int height;\n  final String format;\n  final Size size;\n  Function(Size)? onTrueSize;\n\n  _WebToonPkzImage({\n    required this.pkzFile,\n    required this.width,\n    required this.height,\n    required this.format,\n    required this.size,\n    required this.onTrueSize,\n  });\n\n  @override\n  Widget build(BuildContext context) {\n    return PkzLoadingImage(\n      pkzPath: pkzFile.pkzPath,\n      path: pkzFile.path,\n      width: size.width,\n      height: size.height,\n      onTrueSize: onTrueSize,\n    );\n  }\n}\n\n// 来自远端\nclass _WebToonRemoteImage extends _WebToonReaderImage {\n  final String fileServer;\n  final String path;\n\n  _WebToonRemoteImage(\n    this.fileServer,\n    this.path,\n    Size size,\n    Function(Size)? onTrueSize,\n  ) : super(size, onTrueSize);\n\n  @override\n  Future<RemoteImageData> imageData() async {\n    return method.remoteImageData(fileServer, path);\n  }\n}\n\n// 通用\nabstract class _WebToonReaderImage extends StatefulWidget {\n  final Size size;\n  final Function(Size)? onTrueSize;\n\n  _WebToonReaderImage(this.size, this.onTrueSize);\n\n  @override\n  State<StatefulWidget> createState() => _WebToonReaderImageState();\n\n  Future<RemoteImageData> imageData();\n}\n\nclass _WebToonReaderImageState extends State<_WebToonReaderImage> {\n  late Future<RemoteImageData> _future = _load();\n\n  Future<RemoteImageData> _load() {\n    return widget.imageData().then((value) {\n      widget.onTrueSize?.call(\n        Size(value.width.toDouble(), value.height.toDouble()),\n      );\n      return value;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return FutureBuilder(\n          future: _future,\n          builder: (\n            BuildContext context,\n            AsyncSnapshot<RemoteImageData> snapshot,\n          ) {\n            if (snapshot.hasError) {\n              return GestureDetector(\n                onLongPress: () async {\n                  String? choose =\n                      await chooseListDialog(context, tr('app.please_choose'), [tr('components.image_reader.reload_image')]);\n                      if (choose == null) {\n                        return;\n                      }\n                      if (choose == tr('components.image_reader.reload_image')) {\n                        setState(() {\n                          _future = _load();\n                        });\n                      }\n                },\n                child: buildError(widget.size.width, widget.size.height),\n              );\n            }\n            if (snapshot.connectionState != ConnectionState.done) {\n              return buildLoading(widget.size.width, widget.size.height);\n            }\n            var data = snapshot.data!;\n            return buildFile(\n              data.finalPath,\n              widget.size.width,\n              widget.size.height,\n              context: context,\n            );\n          },\n        );\n      },\n    );\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nclass _WebToonZoomReaderState extends _ImageReaderContentState {\n  var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n  late final List<Size?> _trueSizes = [];\n  late final zoomable.ItemScrollController _itemScrollController;\n  late final zoomable.ScrollOffsetController _scrollOffsetController;\n  late final zoomable.ItemPositionsListener _itemPositionsListener;\n  late final zoomable.ScrollOffsetListener _scrollOffsetListener;\n  StreamSubscription<double>? _scrollOffsetSubscription;\n\n  @override\n  void initState() {\n    for (var e in widget.struct.images) {\n      if (e.pkzFile != null &&\n          e.width != null &&\n          e.height != null &&\n          e.width! > 0 &&\n          e.height! > 0) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else if (e.downloadLocalPath != null) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else {\n        _trueSizes.add(null);\n      }\n    }\n    _itemScrollController = zoomable.ItemScrollController();\n    _scrollOffsetController = zoomable.ScrollOffsetController();\n    _itemPositionsListener = zoomable.ItemPositionsListener.create();\n    _itemPositionsListener.itemPositions.addListener(_onListCurrentChange);\n    _scrollOffsetListener = zoomable.ScrollOffsetListener.create();\n    _scrollOffsetSubscription = _scrollOffsetListener.changes.listen(_onScroll);\n    super.initState();\n  }\n\n  void _onScroll(double delta) {\n    if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) {\n      if (delta > 0) {\n        widget.struct.onFullScreenChange(true);\n      }\n    }\n  }\n\n  @override\n  void dispose() {\n    _itemPositionsListener.itemPositions.removeListener(_onListCurrentChange);\n    _scrollOffsetSubscription?.cancel();\n    super.dispose();\n  }\n\n  void _onListCurrentChange() {\n    var positions = _itemPositionsListener.itemPositions.value;\n    if (positions.isNotEmpty) {\n      super._onPositionsChange(positions\n          .map((e) => ImageReaderItemPosition(\n              e.index, e.itemLeadingEdge, e.itemTrailingEdge))\n          .toList());\n    }\n  }\n\n  @override\n  void _needJumpTo(int index, bool animation) {\n    if (noAnimation() || animation == false) {\n      _itemScrollController.jumpTo(\n        index: index,\n      );\n    } else {\n      if (DateTime.now().millisecondsSinceEpoch < _controllerTime) {\n        return;\n      }\n      _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n      _itemScrollController.scrollTo(\n        index: index,\n        duration: const Duration(milliseconds: 400),\n      );\n    }\n  }\n\n  @override\n  void _needScrollForward() {\n    _commonWebToonScrollForward(\n      (offset) {\n        _scrollOffsetController.animateScroll(\n          offset: offset,\n          duration: noAnimation()\n              ? Duration.zero\n              : const Duration(milliseconds: 200),\n          curve: Curves.easeOut,\n        );\n      },\n      (index) {\n        _needJumpTo(index, true);\n      },\n    );\n  }\n\n  @override\n  void _needScrollBackward() {\n    _commonWebToonScrollBackward(\n      (offset) {\n        _scrollOffsetController.animateScroll(\n          offset: offset,\n          duration: noAnimation()\n              ? Duration.zero\n              : const Duration(milliseconds: 200),\n          curve: Curves.easeOut,\n        );\n      },\n      (index) {\n        _needJumpTo(index, true);\n      },\n    );\n  }\n\n  @override\n  double? get _remainingScrollHeight {\n    try {\n      return _scrollOffsetController.maxScrollExtent -\n          _scrollOffsetController.offset;\n    } catch (e) {\n      return null;\n    }\n  }\n\n  @override\n  Widget _buildViewer() {\n    return Container(\n      decoration: BoxDecoration(\n        color: readerBackgroundColorObj,\n      ),\n      child: _buildList(),\n    );\n  }\n\n  Widget _buildList() {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        List<Widget> _images = [];\n        for (var index = 0; index < widget.struct.images.length; index++) {\n          late Size renderSize;\n          if (_trueSizes[index] != null) {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(\n                constraints.maxWidth,\n                constraints.maxWidth *\n                    _trueSizes[index]!.height /\n                    _trueSizes[index]!.width,\n              );\n            } else {\n              var maxHeight = constraints.maxHeight -\n                  super._topBarHeight() -\n                  super._bottomBarHeight() -\n                  MediaQuery.of(context).padding.bottom;\n              renderSize = Size(\n                maxHeight *\n                    _trueSizes[index]!.width /\n                    _trueSizes[index]!.height,\n                maxHeight,\n              );\n            }\n          } else {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2);\n            } else {\n              renderSize =\n                  Size(constraints.maxWidth / 2, constraints.maxHeight);\n            }\n          }\n          var currentIndex = index;\n          onTrueSize(Size size) {\n            setState(() {\n              _trueSizes[currentIndex] = size;\n            });\n          }\n\n          var e = widget.struct.images[index];\n          if (e.pkzFile != null) {\n            _images.add(_WebToonPkzImage(\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n              pkzFile: e.pkzFile!,\n            ));\n          } else if (e.downloadLocalPath != null) {\n            _images.add(_WebToonDownloadImage(\n              fileServer: e.fileServer,\n              path: e.path,\n              localPath: e.downloadLocalPath!,\n              fileSize: e.fileSize!,\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n            ));\n          } else {\n            _images.add(_WebToonRemoteImage(\n              e.fileServer,\n              e.path,\n              renderSize,\n              onTrueSize,\n            ));\n          }\n        }\n        return zoomable.ZoomablePositionedList.builder(\n          gestureSpeed: currentGestureSpeed(),\n          dragRegionLock: dragRegionLock(),\n          scrollOffsetListener: _scrollOffsetListener,\n          minScale: readerZoomMinScale,\n          maxScale: readerZoomMaxScale,\n          doubleTapScale: readerZoomDoubleTapScale,\n          doubleTapAnimationDuration: noAnimation()\n              ? Duration.zero\n              : const Duration(milliseconds: 200),\n          enableDoubleTapZoom: widget.fullScreenAction !=\n                  FullScreenAction.TOUCH_DOUBLE &&\n              widget.fullScreenAction != FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT,\n          initialScrollIndex: super._startIndex,\n          scrollDirection:\n              widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                  ? Axis.vertical\n                  : Axis.horizontal,\n          reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,\n          padding: EdgeInsets.only(\n            top: super._topBarHeight(),\n            bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                ? 130\n                : (super._bottomBarHeight() +\n                    MediaQuery.of(context).padding.bottom),\n          ),\n          itemScrollController: _itemScrollController,\n          scrollOffsetController: _scrollOffsetController,\n          itemPositionsListener: _itemPositionsListener,\n          itemCount: widget.struct.images.length + 1,\n          itemBuilder: (BuildContext context, int index) {\n            if (index >= widget.struct.images.length) {\n              return _buildNextEp();\n            }\n            return _images[index];\n          },\n        );\n      },\n    );\n  }\n\n  Widget _buildNextEp() {\n    if (super._fullscreenController()) {\n      return Container();\n    }\n    return Container(\n      color: Colors.transparent,\n      padding: const EdgeInsets.all(20),\n      child: MaterialButton(\n        onPressed: () {\n          if (super._hasNextEp()) {\n            super._onNextAction();\n          } else {\n            Navigator.of(context).pop();\n          }\n        },\n        textColor: invertColor(readerBackgroundColorObj),\n        child: Container(\n          padding: const EdgeInsets.only(top: 40, bottom: 40),\n          child: Text(super._hasNextEp()\n              ? tr('components.image_reader.next_chapter')\n              : tr('components.image_reader.end_reading')),\n        ),\n      ),\n    );\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nclass _ListViewReaderState extends _ImageReaderContentState\n    with SingleTickerProviderStateMixin {\n  final List<Size?> _trueSizes = [];\n  final _transformationController = TransformationController();\n  late TapDownDetails _doubleTapDetails;\n  late final _animationController = AnimationController(\n    vsync: this,\n    duration: const Duration(milliseconds: 100),\n  );\n  late final _scrollController = ScrollController();\n  double _lastScrollOffset = 0;\n\n  @override\n  void initState() {\n    _scrollController.addListener(_onScroll);\n    for (var e in widget.struct.images) {\n      if (e.pkzFile != null &&\n          e.width != null &&\n          e.height != null &&\n          e.width! > 0 &&\n          e.height! > 0) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else if (e.downloadLocalPath != null) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else {\n        _trueSizes.add(null);\n      }\n    }\n    super.initState();\n  }\n\n  void _onScroll() {\n    var offset = _scrollController.offset;\n    if (currentAutoFullScreenOnForward() && !widget.struct.fullScreen) {\n      if (offset > _lastScrollOffset) {\n        widget.struct.onFullScreenChange(true);\n      }\n    }\n    _lastScrollOffset = offset;\n  }\n\n  @override\n  void dispose() {\n    _transformationController.dispose();\n    _animationController.dispose();\n    _scrollController.dispose();\n    super.dispose();\n  }\n\n  @override\n  void _needJumpTo(int index, bool animation) {}\n\n  int _controllerTime = 0;\n\n  @override\n  void _needScrollForward() {\n    var first = _scrollController.offset;\n\n    double s;\n    if (gReaderDirection == ReaderDirection.TOP_TO_BOTTOM) {\n      s = MediaQuery.of(context).size.height;\n    } else {\n      s = MediaQuery.of(context).size.width;\n    }\n    var scrollSize = s * readerScrollByScreenPercentage;\n    var pos = first + scrollSize;\n    if (pos > _scrollController.position.maxScrollExtent) {\n      pos = _scrollController.position.maxScrollExtent;\n    }\n\n    if (noAnimation()) {\n      _scrollController.jumpTo(pos);\n    } else {\n      if (DateTime.now().millisecondsSinceEpoch < _controllerTime) {\n        return;\n      }\n      _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n      _scrollController.animateTo(\n        pos,\n        duration: const Duration(milliseconds: 400),\n        curve: Curves.ease,\n      );\n    }\n  }\n\n  @override\n  void _needScrollBackward() {}\n\n  @override\n  double? get _remainingScrollHeight {\n    if (_scrollController.hasClients) {\n      return _scrollController.position.maxScrollExtent -\n          _scrollController.offset;\n    }\n    return null;\n  }\n\n  @override\n  Widget _buildViewer() {\n    return Container(\n      decoration: BoxDecoration(\n        color: readerBackgroundColorObj,\n      ),\n      child: _buildList(),\n    );\n  }\n\n  Widget _buildList() {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        // reload _images size\n        List<Widget> _images = [];\n        for (var index = 0; index < widget.struct.images.length; index++) {\n          late Size renderSize;\n          if (_trueSizes[index] != null) {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(\n                constraints.maxWidth,\n                constraints.maxWidth *\n                    _trueSizes[index]!.height /\n                    _trueSizes[index]!.width,\n              );\n            } else {\n              var maxHeight = constraints.maxHeight -\n                  super._topBarHeight() -\n                  super._bottomBarHeight() -\n                  MediaQuery.of(context).padding.bottom;\n              renderSize = Size(\n                maxHeight *\n                    _trueSizes[index]!.width /\n                    _trueSizes[index]!.height,\n                maxHeight,\n              );\n            }\n          } else {\n            if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) {\n              renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2);\n            } else {\n              // ReaderDirection.LEFT_TO_RIGHT\n              // ReaderDirection.RIGHT_TO_LEFT\n              renderSize =\n                  Size(constraints.maxWidth / 2, constraints.maxHeight);\n            }\n          }\n          var currentIndex = index;\n          onTrueSize(Size size) {\n            setState(() {\n              _trueSizes[currentIndex] = size;\n            });\n          }\n\n          var e = widget.struct.images[index];\n          if (e.pkzFile != null) {\n            _images.add(_WebToonPkzImage(\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n              pkzFile: e.pkzFile!,\n            ));\n          } else if (e.downloadLocalPath != null) {\n            _images.add(_WebToonDownloadImage(\n              fileServer: e.fileServer,\n              path: e.path,\n              localPath: e.downloadLocalPath!,\n              fileSize: e.fileSize!,\n              width: e.width!,\n              height: e.height!,\n              format: e.format!,\n              size: renderSize,\n              onTrueSize: onTrueSize,\n            ));\n          } else {\n            _images.add(_WebToonRemoteImage(\n              e.fileServer,\n              e.path,\n              renderSize,\n              onTrueSize,\n            ));\n          }\n        }\n        var list = ListView.builder(\n          controller: _scrollController,\n          scrollDirection:\n              widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                  ? Axis.vertical\n                  : Axis.horizontal,\n          reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,\n          padding: EdgeInsets.only(\n            // 不管全屏与否, 滚动方向如何, 顶部永远保持间距\n            top: super._topBarHeight(),\n            bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n                ? 130 // 纵向滚动 底部永远都是130的空白\n                : (super._bottomBarHeight() +\n                    MediaQuery.of(context).padding.bottom)\n            // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果\n            ,\n          ),\n          itemCount: widget.struct.images.length + 1,\n          itemBuilder: (BuildContext context, int index) {\n            if (widget.struct.images.length == index) {\n              return _buildNextEp();\n            }\n            return _images[index];\n          },\n        );\n        var viewer = InteractiveViewer(\n          transformationController: _transformationController,\n          minScale: readerZoomMinScale,\n          maxScale: readerZoomMaxScale,\n          child: list,\n        );\n        if (FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() ||\n            FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT ==\n                currentFullScreenAction()) {\n          return viewer;\n        }\n        return GestureDetector(\n          onDoubleTap: _handleDoubleTap,\n          onDoubleTapDown: _handleDoubleTapDown,\n          child: viewer,\n        );\n      },\n    );\n  }\n\n  Widget _buildNextEp() {\n    if (super._fullscreenController()) {\n      return Container();\n    }\n    return Container(\n      padding: const EdgeInsets.all(20),\n      child: MaterialButton(\n        onPressed: () {\n          if (super._hasNextEp()) {\n            super._onNextAction();\n          } else {\n            Navigator.of(context).pop();\n          }\n        },\n        textColor: invertColor(readerBackgroundColorObj),\n        child: Container(\n          padding: const EdgeInsets.only(top: 40, bottom: 40),\n          child: Text(super._hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading')),\n        ),\n      ),\n    );\n  }\n\n  void _handleDoubleTapDown(TapDownDetails details) {\n    _doubleTapDetails = details;\n  }\n\n  void _handleDoubleTap() {\n    if (_animationController.isAnimating) {\n      return;\n    }\n    if (_transformationController.value != Matrix4.identity()) {\n      _transformationController.value = Matrix4.identity();\n    } else {\n      var position = _doubleTapDetails.localPosition;\n      final targetScale = readerZoomDoubleTapScale.clamp(1.0, readerZoomMaxScale);\n      var animation = Tween(begin: 0.0, end: 1.0).animate(_animationController);\n      animation.addListener(() {\n        final scale = 1.0 + (targetScale - 1.0) * animation.value;\n        _transformationController.value = Matrix4.identity()\n          ..translate(\n              -position.dx * (scale - 1.0), -position.dy * (scale - 1.0))\n          ..scale(scale);\n      });\n      _animationController.forward(from: 0);\n    }\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nclass _GalleryReaderState extends _ImageReaderContentState {\n  late PageController _pageController;\n  List<ImageProvider> ips = [];\n  List<PhotoViewGalleryPageOptions> options = [];\n  late Widget gallery;\n\n  @override\n  void initState() {\n    super.initState();\n    // 需要先初始化 super._startIndex 才能使用, 所以在上面\n    _pageController = PageController(initialPage: super._startIndex);\n    for (var index = 0; index < widget.struct.images.length; index++) {\n      var item = widget.struct.images[index];\n      late ImageProvider ip;\n      if (item.pkzFile != null) {\n        ip = PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path);\n      } else if (item.downloadLocalPath != null) {\n        ip = ResourceDownloadFileImageProvider(item.downloadLocalPath!);\n      } else {\n        ip = ResourceRemoteImageProvider(item.fileServer, item.path);\n      }\n      ips.add(ip);\n    }\n    for (var ip in ips) {\n      options.add(PhotoViewGalleryPageOptions(\n        disableGestures:\n            FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() ||\n                FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT ==\n                    currentFullScreenAction(),\n        imageProvider: ip,\n        initialScale: PhotoViewComputedScale.contained,\n        minScale: PhotoViewComputedScale.contained * readerZoomMinScale,\n        maxScale: PhotoViewComputedScale.contained * readerZoomMaxScale,\n        tightMode: dragRegionLock(),\n        errorBuilder: (b, e, s) {\n          print(\"$e,$s\");\n          return LayoutBuilder(\n            builder: (BuildContext context, BoxConstraints constraints) {\n              return buildError(constraints.maxWidth, constraints.maxHeight);\n            },\n          );\n        },\n        filterQuality: FilterQuality.high,\n      ));\n    }\n    _preloadJump(super._startIndex, init: true);\n  }\n\n  @override\n  void dispose() {\n    _pageController.dispose();\n    super.dispose();\n  }\n\n  @override\n  void _needJumpTo(int index, bool animation) {\n    if (noAnimation() || animation == false) {\n      _pageController.jumpToPage(\n        index,\n      );\n    } else {\n      _pageController.animateToPage(\n        index,\n        duration: const Duration(milliseconds: 400),\n        curve: Curves.ease,\n      );\n    }\n    _preloadJump(index);\n  }\n\n  @override\n  void _needScrollForward() {}\n\n  @override\n  void _needScrollBackward() {}\n\n  _preloadJump(int index, {bool init = false}) {\n    fn() {\n      for (var i = index - 1; i < index + 3; i++) {\n        if (i < 0 || i >= ips.length) continue;\n        final ip = ips[i];\n        precacheImage(ip, context);\n      }\n    }\n\n    if (init) {\n      WidgetsBinding.instance?.addPostFrameCallback((_) => fn());\n    } else {\n      fn();\n    }\n  }\n\n  Future _onLongPress() async {\n    if (_current >= 0 && _current < widget.struct.images.length) {\n      var item = widget.struct.images[_current];\n      if (item.pkzFile != null) {\n        return;\n      }\n      Future<String> load() async {\n        var item = widget.struct.images[_current];\n        if (item.downloadLocalPath != null) {\n          return method.downloadImagePath(item.downloadLocalPath!);\n        }\n        var data = await method.remoteImageData(item.fileServer, item.path);\n        return data.finalPath;\n      }\n\n      String? choose = await chooseListDialog(context, tr('app.please_choose'), [tr('app.preview_image'), tr('app.save_image')]);\n      if (choose == null) {\n        return;\n      }\n\n      if (choose == tr('app.preview_image')) {\n        try {\n          var file = await load();\n          Navigator.of(context).push(mixRoute(\n            builder: (context) => FilePhotoViewScreen(file),\n          ));\n        } catch (e) {\n          defaultToast(context, tr('components.image_reader.image_load_failed'));\n        }\n      } else if (choose == tr('app.save_image')) {\n        try {\n          var file = await load();\n          saveImage(file, context);\n        } catch (e) {\n          defaultToast(context, tr('components.image_reader.image_load_failed'));\n        }\n      }\n    }\n  }\n\n  void _onGalleryPageChange(int to) {\n    if (to > super._current &&\n        currentAutoFullScreenOnForward() &&\n        !widget.struct.fullScreen) {\n      widget.struct.onFullScreenChange(true);\n    }\n    for (var i = to; i < to + 3 && i < ips.length; i++) {\n      final ip = ips[i];\n      precacheImage(ip, context);\n    }\n    // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5\n    if (to >= 0 && to < widget.struct.images.length) {\n      super._onCurrentChange(to);\n    }\n  }\n\n  @override\n  Widget _buildViewer() {\n    gallery = PhotoViewGallery.builder(\n      scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n          ? Axis.vertical\n          : Axis.horizontal,\n      reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,\n      backgroundDecoration: BoxDecoration(color: readerBackgroundColorObj),\n      loadingBuilder: (context, event) => LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return buildLoading(constraints.maxWidth, constraints.maxHeight);\n        },\n      ),\n      pageController: _pageController,\n      onPageChanged: _onGalleryPageChange,\n      itemCount: widget.struct.images.length,\n      builder: (BuildContext context, int index) {\n        return options[index];\n      },\n      allowImplicitScrolling: true,\n    );\n    gallery = GestureDetector(\n      child: gallery,\n      onLongPress: _onLongPress,\n    );\n    gallery = Container(\n      padding: EdgeInsets.only(\n        top: widget.struct.fullScreen ? 0 : super._topBarHeight(),\n        bottom: widget.struct.fullScreen ? 0 : super._bottomBarHeight(),\n      ),\n      child: gallery,\n    );\n    gallery = Stack(\n      children: [\n        gallery,\n        _buildNextEpController(),\n      ],\n    );\n    return gallery;\n  }\n\n  Widget _buildNextEpController() {\n    if (super._fullscreenController() ||\n        _current < widget.struct.images.length - 1) return Container();\n    return Align(\n      alignment: Alignment.bottomRight,\n      child: Material(\n        color: Colors.transparent,\n        child: SafeArea(\n          top: false,\n          child: Container(\n            margin: const EdgeInsets.only(bottom: 10),\n            padding:\n                const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4),\n            decoration: const BoxDecoration(\n              borderRadius: BorderRadius.only(\n                topLeft: Radius.circular(10),\n                bottomLeft: Radius.circular(10),\n              ),\n              color: Color(0x88000000),\n            ),\n            child: GestureDetector(\n              onTap: () {\n                if (_hasNextEp()) {\n                  _onNextAction();\n                } else {\n                  Navigator.of(context).pop();\n                }\n              },\n              child: Text(\n                _hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading'),\n                style: const TextStyle(color: Colors.white),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nclass _TwoPageGalleryReaderState extends _ImageReaderContentState {\n  late PageController _pageController;\n  var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400;\n  late final List<Size?> _trueSizes = [];\n  List<ImageProvider> ips = [];\n  List<PhotoViewGalleryPageOptions> options = [];\n  late PhotoViewGallery _view;\n\n  @override\n  void initState() {\n    // 需要先初始化 super._startIndex 才能使用, 所以在上面\n    for (var e in widget.struct.images) {\n      if (e.pkzFile != null &&\n          e.width != null &&\n          e.height != null &&\n          e.width! > 0 &&\n          e.height! > 0) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else if (e.downloadLocalPath != null) {\n        _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble()));\n      } else {\n        _trueSizes.add(null);\n      }\n    }\n    super.initState();\n    _pageController = PageController(initialPage: super._startIndex ~/ 2);\n    for (var index = 0; index < widget.struct.images.length; index++) {\n      var item = widget.struct.images[index];\n      late ImageProvider ip;\n      if (item.pkzFile != null) {\n        ip = PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path);\n      } else if (item.downloadLocalPath != null) {\n        ip = ResourceDownloadFileImageProvider(item.downloadLocalPath!);\n      } else {\n        ip = ResourceRemoteImageProvider(item.fileServer, item.path);\n      }\n      ips.add(ip);\n    }\n    for (var index = 0; index < ips.length; index += 2) {\n      // 两页\n      late ImageProvider leftIp = ips[index];\n      late ImageProvider rightIp = ips[index + 1];\n      if (index + 1 < ips.length) {\n        leftIp = ips[index];\n        rightIp = ips[index + 1];\n      } else {\n        leftIp = ips[index];\n        // ImageProvider by color black\n        rightIp = const AssetImage('lib/assets/0.png');\n      }\n      if (twoPageDirection == TwoPageDirection.RIGHT_TO_LEFT) {\n        final temp = leftIp;\n        leftIp = rightIp;\n        rightIp = temp;\n      }\n      late Alignment leftAlignment, rightAlignment;\n      switch (gReaderTwoPageDirection) {\n        case ReaderTwoPageDirection.CLOSE_TO:\n          leftAlignment = Alignment.centerRight;\n          rightAlignment = Alignment.centerLeft;\n          break;\n        case ReaderTwoPageDirection.PULL_AWAY:\n          leftAlignment = Alignment.centerLeft;\n          rightAlignment = Alignment.centerRight;\n          break;\n        case ReaderTwoPageDirection.EACH_CENTERED:\n          leftAlignment = Alignment.center;\n          rightAlignment = Alignment.center;\n          break;\n      }\n\n      options.add(\n        PhotoViewGalleryPageOptions.customChild(\n          disableGestures:\n              FullScreenAction.TOUCH_DOUBLE == currentFullScreenAction() ||\n                  FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT ==\n                      currentFullScreenAction(),\n          initialScale: PhotoViewComputedScale.contained,\n          minScale: PhotoViewComputedScale.contained * readerZoomMinScale,\n          maxScale: PhotoViewComputedScale.contained * readerZoomMaxScale,\n          tightMode: dragRegionLock(),\n          child: LayoutBuilder(\n            builder: (BuildContext context, BoxConstraints constraints) {\n              return Row(\n                children: [\n                  Expanded(\n                    child: Align(\n                      alignment: leftAlignment,\n                      child: Image(\n                        image: leftIp,\n                        fit: BoxFit.contain,\n                        // loadingBuilder: (context, child, event) => buildLoading(constraints.maxWidth, constraints.maxHeight),\n                        errorBuilder: (b, e, s) {\n                          print(\"$e,$s\");\n                          return buildError(\n                            constraints.maxWidth / 2,\n                            constraints.maxHeight / 2,\n                          );\n                        },\n                      ),\n                    ),\n                  ),\n                  Expanded(\n                    child: Align(\n                      alignment: rightAlignment,\n                      child: Image(\n                        image: rightIp,\n                        fit: BoxFit.contain,\n                        // loadingBuilder: (context, child, event) => buildLoading(constraints.maxWidth, constraints.maxHeight),\n                        errorBuilder: (b, e, s) {\n                          print(\"$e,$s\");\n                          return buildError(\n                            constraints.maxWidth / 2,\n                            constraints.maxHeight / 2,\n                          );\n                        },\n                      ),\n                    ),\n                  ),\n                ],\n              );\n            },\n          ),\n        ),\n      );\n    }\n    _view = PhotoViewGallery(\n      pageController: _pageController,\n      pageOptions: options,\n      scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM\n          ? Axis.vertical\n          : Axis.horizontal,\n      reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT,\n      onPageChanged: _onGalleryPageChange,\n      backgroundDecoration: BoxDecoration(color: readerBackgroundColorObj),\n    );\n    _preloadJump(super._startIndex, init: true);\n  }\n\n  @override\n  void dispose() {\n    _pageController.dispose();\n    super.dispose();\n  }\n\n  @override\n  void _needJumpTo(int index, bool animation) {\n    if (noAnimation() || animation == false) {\n      _pageController.jumpToPage(\n        index ~/ 2,\n      );\n    } else {\n      _pageController.animateToPage(\n        index ~/ 2,\n        duration: const Duration(milliseconds: 400),\n        curve: Curves.ease,\n      );\n    }\n    _preloadJump(index);\n  }\n\n  @override\n  void _needScrollBackward() {\n    // TODO: implement _needScrollBackward\n  }\n\n  @override\n  void _needScrollForward() {\n    // TODO: implement _needScrollForward\n  }\n\n  _preloadJump(int index, {bool init = false}) {\n    fn() {\n      for (var i = index - 2; i < index + 5; i++) {\n        if (i < 0 || i >= ips.length) continue;\n        final ip = ips[i];\n        precacheImage(ip, context);\n      }\n    }\n\n    if (init) {\n      WidgetsBinding.instance?.addPostFrameCallback((_) => fn());\n    } else {\n      fn();\n    }\n  }\n\n  @override\n  Widget _buildViewer() {\n    return Stack(\n      children: [\n        GestureDetector(\n          onLongPress: _onLongPress,\n          child: _view,\n        ),\n        _buildNextEpController(),\n      ],\n    );\n  }\n\n  void _onGalleryPageChange(int to) {\n    var toIndex = to * 2;\n    if (toIndex > super._current &&\n        currentAutoFullScreenOnForward() &&\n        !widget.struct.fullScreen) {\n      widget.struct.onFullScreenChange(true);\n    }\n    // 提前加载\n    for (var i = toIndex + 2; i < toIndex + 5 && i < ips.length; i++) {\n      final ip = ips[i];\n      precacheImage(ip, context);\n    }\n    // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5\n    if (to >= 0 && to < widget.struct.images.length) {\n      super._onCurrentChange(toIndex);\n    }\n  }\n\n  Widget _buildNextEpController() {\n    if (super._fullscreenController() ||\n        _current < widget.struct.images.length - 2) return Container();\n    return Align(\n      alignment: Alignment.bottomRight,\n      child: Material(\n        color: Colors.transparent,\n        child: SafeArea(\n          top: false,\n          child: Container(\n            margin: const EdgeInsets.only(bottom: 10),\n            padding:\n            const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4),\n            decoration: const BoxDecoration(\n              borderRadius: BorderRadius.only(\n                topLeft: Radius.circular(10),\n                bottomLeft: Radius.circular(10),\n              ),\n              color: Color(0x88000000),\n            ),\n            child: GestureDetector(\n              onTap: () {\n                if (_hasNextEp()) {\n                  _onNextAction();\n                } else {\n                  Navigator.of(context).pop();\n                }\n              },\n              child: Text(\n                _hasNextEp() ? tr('components.image_reader.next_chapter') : tr('components.image_reader.end_reading'),\n                style: const TextStyle(color: Colors.white),\n              ),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Future _onLongPress() async {\n    List<ReaderImageInfo> matchImages = [];\n    if (_current >= 0 && _current < widget.struct.images.length) {\n      var item = widget.struct.images[_current];\n      if (item.pkzFile != null) {\n        return;\n      }\n      matchImages.add(item);\n    }\n    if (_current + 1 >= 0 && _current + 1 < widget.struct.images.length) {\n      var item = widget.struct.images[_current + 1];\n      if (item.pkzFile != null) {\n        return;\n      }\n      matchImages.add(item);\n    }\n    if (matchImages.isEmpty) {\n      return;\n    }\n    String? choose = await chooseListDialog(context, tr('app.please_choose'), [tr('components.image_reader.save_image_in_this_page')]);\n    if (choose == null) {\n      return;\n    }\n    if (choose == tr('components.image_reader.save_image_in_this_page')) {\n        for (var item in matchImages) {\n          if (item.downloadLocalPath != null) {\n            var file = await method.downloadImagePath(item.downloadLocalPath!);\n            saveImage(file, context);\n          } else {\n            var data = await method.remoteImageData(item.fileServer, item.path);\n            saveImage(data.finalPath, context);\n          }\n        }\n    }\n  }\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\nColor invertColor(Color color) {\n  return Color.fromRGBO(\n    255 - color.red,\n    255 - color.green,\n    255 - color.blue,\n    1.0,\n  );\n}\n\n///////////////////////////////////////////////////////////////////////////////\n\n///////////////////////////////////////////////////////////////////////////////\n"
  },
  {
    "path": "lib/screens/components/Images.dart",
    "content": "import 'dart:typed_data';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_svg/flutter_svg.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:flutter_svg/svg.dart';\nimport 'package:pikapika/basic/config/ImageAddress.dart';\nimport 'dart:io';\nimport 'dart:ui' as ui show Codec;\n\nimport '../../basic/config/IconLoading.dart';\nimport '../FilePhotoViewScreen.dart';\n\n// 从本地加载图片\nclass ResourceFileImageProvider\n    extends ImageProvider<ResourceFileImageProvider> {\n  final String path;\n  final double scale;\n\n  ResourceFileImageProvider(this.path, {this.scale = 1.0});\n\n  @override\n  ImageStreamCompleter load(\n    ResourceFileImageProvider key,\n    DecoderCallback decode,\n  ) {\n    return MultiFrameImageStreamCompleter(\n      codec: _loadAsync(key),\n      scale: key.scale,\n    );\n  }\n\n  @override\n  Future<ResourceFileImageProvider> obtainKey(\n      ImageConfiguration configuration) {\n    return SynchronousFuture<ResourceFileImageProvider>(this);\n  }\n\n  Future<ui.Codec> _loadAsync(ResourceFileImageProvider key) async {\n    assert(key == this);\n    return PaintingBinding.instance!.instantiateImageCodec(\n      await File(path).readAsBytes(),\n    );\n  }\n\n  @override\n  bool operator ==(dynamic other) {\n    if (other.runtimeType != runtimeType) return false;\n    final ResourceFileImageProvider typedOther = other;\n    return path == typedOther.path && scale == typedOther.scale;\n  }\n\n  @override\n  int get hashCode => hashValues(path, scale);\n\n  @override\n  String toString() => '$runtimeType('\n      'path: ${describeIdentity(path)},'\n      ' scale: $scale'\n      ')';\n}\n\n// 从本地加载图片\nclass ResourceDownloadFileImageProvider\n    extends ImageProvider<ResourceDownloadFileImageProvider> {\n  final String path;\n  final double scale;\n\n  ResourceDownloadFileImageProvider(this.path, {this.scale = 1.0});\n\n  @override\n  ImageStreamCompleter load(\n    ResourceDownloadFileImageProvider key,\n    DecoderCallback decode,\n  ) {\n    return MultiFrameImageStreamCompleter(\n      codec: _loadAsync(key),\n      scale: key.scale,\n    );\n  }\n\n  @override\n  Future<ResourceDownloadFileImageProvider> obtainKey(\n      ImageConfiguration configuration) {\n    return SynchronousFuture<ResourceDownloadFileImageProvider>(this);\n  }\n\n  Future<ui.Codec> _loadAsync(ResourceDownloadFileImageProvider key) async {\n    assert(key == this);\n    return PaintingBinding.instance!.instantiateImageCodec(\n        await File(await method.downloadImagePath(path)).readAsBytes());\n  }\n\n  @override\n  bool operator ==(dynamic other) {\n    if (other.runtimeType != runtimeType) return false;\n    final ResourceDownloadFileImageProvider typedOther = other;\n    return path == typedOther.path && scale == typedOther.scale;\n  }\n\n  @override\n  int get hashCode => hashValues(path, scale);\n\n  @override\n  String toString() => '$runtimeType('\n      'path: ${describeIdentity(path)},'\n      ' scale: $scale'\n      ')';\n}\n\n// 从远端加载图片\nclass ResourceRemoteImageProvider\n    extends ImageProvider<ResourceRemoteImageProvider> {\n  final String fileServer;\n  final String path;\n  final double scale;\n\n  ResourceRemoteImageProvider(this.fileServer, this.path, {this.scale = 1.0});\n\n  @override\n  ImageStreamCompleter load(\n    ResourceRemoteImageProvider key,\n    DecoderCallback decode,\n  ) {\n    return MultiFrameImageStreamCompleter(\n      codec: _loadAsync(key),\n      scale: key.scale,\n    );\n  }\n\n  @override\n  Future<ResourceRemoteImageProvider> obtainKey(\n      ImageConfiguration configuration) {\n    return SynchronousFuture<ResourceRemoteImageProvider>(this);\n  }\n\n  Future<ui.Codec> _loadAsync(ResourceRemoteImageProvider key) async {\n    assert(key == this);\n    var downloadTo = await method.remoteImageData(fileServer, path);\n    return PaintingBinding.instance!.instantiateImageCodec(\n      await File(downloadTo.finalPath).readAsBytes(),\n    );\n  }\n\n  @override\n  bool operator ==(dynamic other) {\n    if (other.runtimeType != runtimeType) return false;\n    final ResourceRemoteImageProvider typedOther = other;\n    return fileServer == typedOther.fileServer &&\n        path == typedOther.path &&\n        scale == typedOther.scale;\n  }\n\n  @override\n  int get hashCode => hashValues(fileServer, path, scale);\n\n  @override\n  String toString() => '$runtimeType('\n      'fileServer: ${describeIdentity(fileServer)},'\n      ' path: ${describeIdentity(path)},'\n      ' scale: $scale'\n      ')';\n}\n\n// 下载的图片\nclass DownloadImage extends StatefulWidget {\n  final String path;\n  final double? width;\n  final double? height;\n\n  const DownloadImage({\n    Key? key,\n    required this.path,\n    this.width,\n    this.height,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _DownloadImageState();\n}\n\nclass _DownloadImageState extends State<DownloadImage> {\n  late final Future<String> _future = method.downloadImagePath(widget.path);\n\n  @override\n  Widget build(BuildContext context) {\n    return pathFutureImage(\n      _future,\n      widget.width,\n      widget.height,\n      context: context,\n    );\n  }\n}\n\n// 远端图片\nclass RemoteImage extends StatefulWidget {\n  final String fileServer;\n  final String path;\n  final double? width;\n  final double? height;\n  final BoxFit fit;\n\n  const RemoteImage({\n    Key? key,\n    required this.fileServer,\n    required this.path,\n    this.width,\n    this.height,\n    this.fit = BoxFit.cover,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _RemoteImageState();\n}\n\nclass _RemoteImageState extends State<RemoteImage> {\n  late bool _mock;\n  late Future<String> _future;\n\n  @override\n  void initState() {\n    _mock = widget.fileServer == \"\" ||\n        (widget.fileServer.contains(\".xyz/\") && currentImageAddress() < 0);\n    if (!_mock) {\n      _future = method\n          .remoteImageData(widget.fileServer, widget.path)\n          .then((value) => value.finalPath);\n    }\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_mock) {\n      return buildMock(widget.width, widget.height);\n    }\n    return pathFutureImage(\n      _future,\n      widget.width,\n      widget.height,\n      fit: widget.fit,\n      context: context,\n    );\n  }\n}\n\nWidget pathFutureImage(Future<String> future, double? width, double? height,\n    {BoxFit fit = BoxFit.cover, BuildContext? context}) {\n  return FutureBuilder(\n      future: future,\n      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {\n        if (snapshot.hasError) {\n          print(\"${snapshot.error}\");\n          print(\"${snapshot.stackTrace}\");\n          return buildError(width, height);\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return buildLoading(width, height);\n        }\n        return buildFile(\n          snapshot.data!,\n          width,\n          height,\n          fit: fit,\n          context: context,\n        );\n      });\n}\n\n// 通用方法\n\nWidget buildSvg(String source, double? width, double? height,\n    {Color? color, double? margin}) {\n  var widget = Container(\n    width: width,\n    height: height,\n    padding: margin != null ? const EdgeInsets.all(10) : null,\n    child: Center(\n      child: SvgPicture.asset(\n        source,\n        width: width,\n        height: height,\n        color: color,\n      ),\n    ),\n  );\n  return GestureDetector(onLongPress: () {}, child: widget);\n}\n\nWidget buildMock(double? width, double? height) {\n  var widget = Container(\n    width: width,\n    height: height,\n    padding: const EdgeInsets.all(10),\n    child: Center(\n      child: SvgPicture.asset(\n        'lib/assets/unknown.svg',\n        width: width,\n        height: height,\n        color: Colors.grey.shade600,\n      ),\n    ),\n  );\n  return GestureDetector(onLongPress: () {}, child: widget);\n}\n\nWidget buildError(double? width, double? height) {\n  double? size;\n  if (width != null && height != null) {\n    size = width < height ? width : height;\n  }\n  return SizedBox(\n    width: width,\n    height: height,\n    child: Center(\n      child: Icon(\n        Icons.error_outline,\n        size: size,\n        color: Colors.grey,\n      ),\n    ),\n  );\n}\n\nWidget buildLoading(double? width, double? height) {\n  double? size;\n  if (width != null && height != null) {\n    size = width < height ? width : height;\n  }\n  return SizedBox(\n    width: width,\n    height: height,\n    child: Center(\n      child: Icon(\n        Icons.downloading,\n        size: size,\n        color: Colors.grey,\n      ),\n    ),\n  );\n}\n\nWidget buildFile(String file, double? width, double? height,\n    {BoxFit fit = BoxFit.cover, BuildContext? context}) {\n  var image = Image(\n    image: ResourceFileImageProvider(file),\n    width: width,\n    height: height,\n    errorBuilder: (a, b, c) {\n      print(\"$b\");\n      print(\"$c\");\n      return buildError(width, height);\n    },\n    fit: fit,\n  );\n  if (context == null) return image;\n  return GestureDetector(\n    onLongPress: () async {\n      String? choose = await chooseListDialog(context, '请选择', ['预览图片', '保存图片']);\n      switch (choose) {\n        case '预览图片':\n          Navigator.of(context).push(mixRoute(\n            builder: (context) => FilePhotoViewScreen(file),\n          ));\n          break;\n        case '保存图片':\n          saveImage(file, context);\n          break;\n      }\n    },\n    child: image,\n  );\n}\n"
  },
  {
    "path": "lib/screens/components/ItemBuilder.dart",
    "content": "import 'package:flutter/material.dart';\n\n// 非全屏FutureBuilder封装\nclass ItemBuilder<T> extends StatelessWidget {\n  final Future<T> future;\n  final AsyncWidgetBuilder<T> successBuilder;\n  final Future<dynamic> Function() onRefresh;\n  final double? loadingHeight;\n  final double? height;\n\n  const ItemBuilder({\n    Key? key,\n    required this.future,\n    required this.successBuilder,\n    required this.onRefresh,\n    this.height,\n    this.loadingHeight,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var _maxWidth = constraints.maxWidth;\n        var _loadingHeight = height ?? loadingHeight ?? _maxWidth / 2;\n        return FutureBuilder(\n            future: future,\n            builder: (BuildContext context, AsyncSnapshot<T> snapshot) {\n              if (snapshot.hasError) {\n                print(\"${snapshot.error}\");\n                print(\"${snapshot.stackTrace}\");\n                return InkWell(\n                  onTap: onRefresh,\n                  child: SizedBox(\n                    width: _maxWidth,\n                    height: _loadingHeight,\n                    child: Center(\n                      child:\n                          Icon(Icons.sync_problem, size: _loadingHeight / 1.5),\n                    ),\n                  ),\n                );\n              }\n              if (snapshot.connectionState != ConnectionState.done) {\n                return SizedBox(\n                  width: _maxWidth,\n                  height: _loadingHeight,\n                  child: Center(\n                    child: Icon(Icons.sync, size: _loadingHeight / 1.5),\n                  ),\n                );\n              }\n              return SizedBox(\n                width: _maxWidth,\n                height: height,\n                child: successBuilder(context, snapshot),\n              );\n            });\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/LinkToComicInfo.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Navigator.dart';\n\nimport '../ComicInfoScreen.dart';\n\nclass LinkToComicInfo extends StatelessWidget {\n  final String comicId;\n  final Widget child;\n\n  const LinkToComicInfo({\n    required this.comicId,\n    required this.child,\n    Key? key,\n  }):super(key: key);\n\n  @override\n  Widget build(BuildContext context) => InkWell(\n        onTap: () {\n          navPushOrReplace(\n            context,\n            (context) => ComicInfoScreen(comicId: comicId),\n          );\n        },\n        child: child,\n      );\n}\n"
  },
  {
    "path": "lib/screens/components/ListView.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/EBookScrolling.dart';\n\nimport '../../basic/config/EBookScrollingRange.dart';\nimport '../../basic/config/EBookScrollingTrigger.dart';\n\nclass PikaListView extends StatefulWidget {\n  final EdgeInsets? padding;\n  final ScrollController? controller;\n  final List<Widget> children;\n  final ScrollPhysics? physics;\n\n  const PikaListView({\n    Key? key,\n    required this.children,\n    this.controller,\n    this.padding,\n    this.physics,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PikaListViewState();\n}\n\nclass _PikaListViewState extends State<PikaListView> {\n  late ScrollController _privateController;\n\n  @override\n  void initState() {\n    if (widget.controller == null) {\n      _privateController = ScrollController();\n    }\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (widget.controller == null) {\n      _privateController.dispose();\n    }\n    super.dispose();\n  }\n\n  ScrollController get _controller => widget.controller ?? _privateController;\n  double _y = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    if (!eBookScrolling) {\n      return ListView(\n        children: widget.children,\n        controller: _controller,\n        padding: widget.padding,\n        physics: widget.physics,\n      );\n    }\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        return GestureDetector(\n          onPanDown: (details) {\n            _y = 0;\n          },\n          onPanUpdate: (details) {\n            _y += details.delta.dy;\n          },\n          onPanEnd: (details) {\n            final lmPoints =\n                (MediaQuery.of(context).devicePixelRatio * (160 / 2.54));\n            final double centimeters = _y / lmPoints;\n            late double off;\n            if (centimeters < -eBookScrollingTrigger) {\n              off = _controller.offset +\n                  eBookScrollingRange * constraints.maxHeight;\n              off = off.clamp(0, _controller.position.maxScrollExtent);\n              _controller.jumpTo(off);\n              _controller.notifyListeners();\n            } else if (centimeters > eBookScrollingTrigger) {\n              off = _controller.offset -\n                  eBookScrollingRange * constraints.maxHeight;\n              off = off.clamp(0, _controller.position.maxScrollExtent);\n              _controller.jumpTo(off);\n              _controller.notifyListeners();\n            }\n          },\n          child: ListView(\n            physics: const NeverScrollableScrollPhysics(),\n            children: widget.children,\n            controller: _controller,\n            padding: widget.padding,\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/MouseAndTouchScrollBehavior.dart",
    "content": "import 'dart:ui';\nimport 'package:flutter/material.dart';\n\nfinal mouseAndTouchScrollBehavior = MouseAndTouchScrollBehavior();\n\nclass MouseAndTouchScrollBehavior extends MaterialScrollBehavior {\n  @override\n  Set<PointerDeviceKind> get dragDevices => {\n    PointerDeviceKind.touch,\n    PointerDeviceKind.mouse,\n  };\n}\n"
  },
  {
    "path": "lib/screens/components/NetworkSetting.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/config/Address.dart';\nimport 'package:pikapika/basic/config/ImageAddress.dart';\nimport 'package:pikapika/basic/config/Proxy.dart';\nimport 'package:pikapika/basic/config/UseApiLoadImage.dart';\n\n// 网络设置\nclass NetworkSetting extends StatelessWidget {\n  const NetworkSetting({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        switchAddressSetting(),\n        imageSwitchAddressSetting(),\n        useApiLoadImageSetting(),\n        proxySetting(),\n        reloadSwitchAddressSetting(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/PkzComicInfoCard.dart",
    "content": "import 'package:pikapika/i18.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/SearchScreen.dart';\nimport 'package:pikapika/basic/Navigator.dart';\n\nimport '../../basic/config/CopyFullName.dart';\nimport '../../basic/config/CopyFullNameTemplate.dart';\nimport 'ComicInfoCard.dart';\nimport 'PkzImages.dart';\n\n// 漫画卡片\nclass PkzComicInfoCard extends StatefulWidget {\n  final String pkzPath;\n  final PkzComic info;\n  final bool linkItem;\n  final PkzComicViewLog? displayViewLog;\n\n  const PkzComicInfoCard({\n    required this.info,\n    required this.pkzPath,\n    this.linkItem = false,\n    Key? key,\n    this.displayViewLog,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicInfoCard();\n}\n\nclass _ComicInfoCard extends State<PkzComicInfoCard> {\n  @override\n  Widget build(BuildContext context) {\n    var info = widget.info;\n    final theme = Theme.of(context);\n    return Container(\n      padding: const EdgeInsets.all(5),\n      decoration: BoxDecoration(\n        border: Border(\n          bottom: BorderSide(\n            color: theme.dividerColor,\n          ),\n        ),\n      ),\n      child: Row(\n        children: [\n          Container(\n            padding: const EdgeInsets.only(right: 10),\n            child: PkzImage(\n              pkzPath: widget.pkzPath,\n              path: info.coverPath,\n              width: imageWidth,\n              height: imageHeight,\n            ),\n          ),\n          Expanded(\n            child: Row(\n              children: [\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      widget.linkItem\n                          ? GestureDetector(\n                              onLongPress: () {\n                                var fin =\n                                copyFullNameTemplate()\n                                    .replaceAll(\"{title}\", info.title)\n                                    .replaceAll(\"{author}\", info.author);\n                                if (fin.isEmpty) {\n                                  fin = info.title;\n                                }\n                                if (copyFullName()) {\n                                  confirmCopy(\n                                      context, fin);\n                                } else {\n                                  confirmCopy(context, info.title);\n                                }\n                              },\n                              child: Text(info.title, style: titleStyle),\n                            )\n                          : Text(info.title, style: titleStyle),\n                      Container(height: 5),\n                      widget.linkItem\n                          ? InkWell(\n                              onTap: () {\n                                // todo\n                              },\n                              onLongPress: () {\n                                confirmCopy(context, info.author);\n                              },\n                              child: Text(info.author, style: authorStyle),\n                            )\n                          : Text(info.author, style: authorStyle),\n                      Container(height: 5),\n                      Text.rich(\n                        widget.linkItem\n                            ? TextSpan(\n                                children: [\n                                  TextSpan(text: tr('app.categories') + ' :'),\n                                  ...info.categories.map(\n                                    (e) => TextSpan(\n                                      children: [\n                                        const TextSpan(text: ' '),\n                                        TextSpan(\n                                            text: e,\n                                            recognizer: TapGestureRecognizer()\n                                              ..onTap = () {\n                                                // todo\n                                              }),\n                                      ],\n                                    ),\n                                  ),\n                                ],\n                              )\n                            : TextSpan(\n                                text: \"${tr('app.categories')} : ${info.categories.join(' ')}\"),\n                        style: TextStyle(\n                          fontSize: 13,\n                          color: Theme.of(context)\n                              .textTheme\n                              .bodyText1!\n                              .color!\n                              .withAlpha(0xCC),\n                        ),\n                      ),\n                      Container(height: 5),\n                      widget.displayViewLog != null &&\n                              widget.displayViewLog!.lastViewEpId.isNotEmpty\n                          ? Container(\n                              padding: EdgeInsets.only(bottom: 5),\n                              child: Text(\n                                \"${tr('app.last_viewed')} ${widget.displayViewLog!.lastViewEpName}\",\n                                maxLines: 1,\n                                overflow: TextOverflow.ellipsis,\n                                style: authorStyleX,\n                              ),\n                            )\n                          : Container(),\n                    ],\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/PkzImages.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'dart:ui' as ui show Codec;\nimport 'Images.dart';\nimport 'dart:typed_data';\n\n// 从本地加载图片\nclass PkzImageProvider extends ImageProvider<PkzImageProvider> {\n  final String pkzPath;\n  final String path;\n  final double scale;\n\n  PkzImageProvider(this.pkzPath, this.path, {this.scale = 1.0});\n\n  @override\n  ImageStreamCompleter load(\n    PkzImageProvider key,\n    DecoderCallback decode,\n  ) {\n    return MultiFrameImageStreamCompleter(\n      codec: _loadAsync(key),\n      scale: key.scale,\n    );\n  }\n\n  @override\n  Future<PkzImageProvider> obtainKey(ImageConfiguration configuration) {\n    return SynchronousFuture<PkzImageProvider>(this);\n  }\n\n  Future<ui.Codec> _loadAsync(PkzImageProvider key) async {\n    assert(key == this);\n    return PaintingBinding.instance!.instantiateImageCodec(\n      await method.loadPkzFile(pkzPath, path),\n    );\n  }\n\n  @override\n  bool operator ==(dynamic other) {\n    if (other.runtimeType != runtimeType) return false;\n    final PkzImageProvider typedOther = other;\n    return pkzPath == typedOther.pkzPath &&\n        path == typedOther.path &&\n        scale == typedOther.scale;\n  }\n\n  @override\n  int get hashCode => hashValues(path, scale);\n\n  @override\n  String toString() => '$runtimeType('\n      ' pkzPath: ${describeIdentity(pkzPath)},'\n      ' path: ${describeIdentity(path)},'\n      ' scale: $scale'\n      ')';\n}\n\n// 远端图片\nclass PkzImage extends StatefulWidget {\n  final String pkzPath;\n  final String path;\n  final double? width;\n  final double? height;\n  final BoxFit fit;\n\n  const PkzImage({\n    Key? key,\n    required this.pkzPath,\n    required this.path,\n    this.width,\n    this.height,\n    this.fit = BoxFit.cover,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PkzImageState();\n}\n\nclass _PkzImageState extends State<PkzImage> {\n  late bool _mock;\n\n  @override\n  void initState() {\n    _mock = widget.path == \"\";\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_mock) {\n      return buildMock(widget.width, widget.height);\n    }\n    return Image(\n      image: PkzImageProvider(widget.pkzPath, widget.path),\n      width: widget.width,\n      height: widget.height,\n      errorBuilder: (a, b, c) {\n        print(\"$b\");\n        print(\"$c\");\n        return buildError(widget.width, widget.height);\n      },\n      fit: widget.fit,\n    );\n  }\n}\n\n// 远端图片\nclass PkzLoadingImage extends StatefulWidget {\n  final String pkzPath;\n  final String path;\n  final double? width;\n  final double? height;\n  final BoxFit fit;\n  final Function(Size)? onTrueSize;\n\n  const PkzLoadingImage({\n    Key? key,\n    required this.pkzPath,\n    required this.path,\n    this.width,\n    this.height,\n    this.fit = BoxFit.cover,\n    this.onTrueSize,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PkzLoadingImageState();\n}\n\nclass _PkzLoadingImageState extends State<PkzLoadingImage> {\n  late bool _mock;\n  late Future<Uint8List> data;\n\n  @override\n  void initState() {\n    _mock = widget.path == \"\";\n    if (!_mock) {\n      data = () async {\n        final data = await method.loadPkzFile(widget.pkzPath, widget.path);\n        if (widget.onTrueSize != null) {\n          var decodedImage = await decodeImageFromList(data);\n          widget.onTrueSize!(\n            Size(\n              decodedImage.width.toDouble(),\n              decodedImage.height.toDouble(),\n            ),\n          );\n        }\n        return data;\n      }();\n    }\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    if (_mock) {\n      return buildMock(widget.width, widget.height);\n    }\n    return Image(\n      image: PkzImageProvider(widget.pkzPath, widget.path),\n      width: widget.width,\n      height: widget.height,\n      errorBuilder: (a, b, c) {\n        print(\"$b\");\n        print(\"$c\");\n        return buildError(widget.width, widget.height);\n      },\n      fit: widget.fit,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/RecommendLinksPanel.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Cross.dart';\nimport 'package:pikapika/basic/config/RecommendLinks.dart';\n\nclass RecommendLinksPanel extends StatefulWidget {\n  final EdgeInsetsGeometry padding;\n\n  const RecommendLinksPanel({\n    Key? key,\n    this.padding = const EdgeInsets.fromLTRB(0, 0, 0, 0),\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _RecommendLinksPanelState();\n}\n\nclass _RecommendLinksPanelState extends State<RecommendLinksPanel> {\n  @override\n  void initState() {\n    recommendLinksEvent.subscribe(_setState);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    recommendLinksEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  void _setState(_) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final links = currentRecommendLinks();\n    if (links.isEmpty) {\n      return const SizedBox.shrink();\n    }\n    return Container(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Divider(),\n          ...links.entries.map((entry) {\n            return ListTile(\n              title: Text(entry.key),\n              onTap: () => openUrl(entry.value),\n            );\n          }).toList(),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/Recommendation.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/screens/ComicInfoScreen.dart';\nimport 'package:pikapika/basic/Method.dart';\n\nimport '../../basic/config/IconLoading.dart';\nimport 'ItemBuilder.dart';\nimport 'Images.dart';\n\n// 看过此本子的也在看\nclass Recommendation extends StatefulWidget {\n  final String comicId;\n\n  const Recommendation({Key? key, required this.comicId}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _RecommendationState();\n}\n\nclass _RecommendationState extends State<Recommendation> {\n  late Future<List<ComicSimple>> _future = method.recommendation(widget.comicId);\n\n  @override\n  Widget build(BuildContext context) {\n    return ItemBuilder(\n      future: _future,\n      successBuilder:\n          (BuildContext context, AsyncSnapshot<List<ComicSimple>> snapshot) {\n        var _comicList = snapshot.data!;\n        var size = MediaQuery.of(context).size;\n        var min = size.width < size.height ? size.width : size.height;\n        var width = (min - 45) / 4;\n        return Wrap(\n          alignment: WrapAlignment.spaceAround,\n          children: _comicList\n              .map((e) => InkWell(\n                    onTap: () {\n                      var i = 0;\n                      Navigator.pushAndRemoveUntil(\n                          context,\n                          mixRoute(\n                              builder: (context) =>\n                                  ComicInfoScreen(comicId: e.id)),\n                          (route) => i++ < 10);\n                    },\n                    child: Card(\n                      child: SizedBox(\n                        width: width,\n                        child: Column(\n                          children: [\n                            LayoutBuilder(builder: (BuildContext context,\n                                BoxConstraints constraints) {\n                              return RemoteImage(\n                                width: width,\n                                fileServer: e.thumb.fileServer,\n                                path: e.thumb.path,\n                              );\n                            }),\n                            Text(\n                              e.title + '\\n',\n                              maxLines: 2,\n                              overflow: TextOverflow.ellipsis,\n                              style: const TextStyle(height: 1.4),\n                              strutStyle: const StrutStyle(height: 1.4),\n                            ),\n                          ],\n                        ),\n                      ),\n                    ),\n                  ))\n              .toList(),\n        );\n      },\n      onRefresh: () async =>\n          setState(() => _future = method.recommendation(widget.comicId)),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/RightClickPop.dart",
    "content": "import 'package:flutter/material.dart';\n\nimport '../../basic/config/UsingRightClickPop.dart';\n\nWidget rightClickPop({\n  required Widget child,\n  required BuildContext context,\n  bool canPop = true,\n}) =>\n    currentUsingRightClickPop()\n        ? GestureDetector(\n            onSecondaryTap: () {\n              if (canPop) {\n                Navigator.of(context).pop();\n              }\n            },\n            child: child,\n          )\n        : child;\n"
  },
  {
    "path": "lib/screens/components/TimeoutLock.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\n\nimport '../../basic/config/Authentication.dart';\nimport '../../basic/config/TimeoutLock.dart';\n\nclass TimeoutLock extends StatefulWidget {\n  final Widget child;\n\n  const TimeoutLock({required this.child, Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _TimeoutLockState();\n}\n\nclass _TimeoutLockState extends State<TimeoutLock> with WidgetsBindingObserver {\n  DateTime? _appLostFocusTimestamp;\n  bool _locked = false;\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    super.dispose();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    super.didChangeAppLifecycleState(state);\n    if (!currentAuthentication() || timeoutLock == 0) return;\n    print(\"_locked: $_locked\");\n    if (_locked) {\n      return;\n    }\n    if (state == AppLifecycleState.paused ||\n        state == AppLifecycleState.inactive) {\n      if (_appLostFocusTimestamp != null) {\n        return;\n      }\n      _appLostFocusTimestamp = DateTime.now();\n    } else if (state == AppLifecycleState.resumed) {\n      if (_appLostFocusTimestamp == null) {\n        return;\n      }\n      final currentTimeStamp = DateTime.now();\n      final difference = currentTimeStamp.difference(_appLostFocusTimestamp!);\n      _appLostFocusTimestamp = null;\n      if (difference.inSeconds > timeoutLock) {\n        _locked = true;\n        Navigator.of(context)\n            .push(MaterialPageRoute(\n          builder: (context) => const TimeoutScreen(),\n        ))\n            .then((value) {\n          _locked = false;\n        });\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return widget.child;\n  }\n}\n\nclass TimeoutScreen extends StatelessWidget {\n  const TimeoutScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return WillPopScope(\n      onWillPop: () async {\n        return false;\n      },\n      child: Scaffold(\n        body: Center(\n          child: Column(\n            mainAxisAlignment: MainAxisAlignment.center,\n            children: [\n              MaterialButton(\n                onPressed: () async {\n                  if (true == await verifyAuthentication(context)) {\n                    Navigator.of(context).pop();\n                  }\n                },\n                child: const Text('您离开APP很久了，请验点击证身份'),\n              ),\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/UserProfileCard.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'dart:ui';\n\nimport 'package:pikapika/i18.dart';\nimport 'package:file_picker/file_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:image_cropper/image_cropper.dart';\nimport 'package:image_picker/image_picker.dart';\nimport 'package:pikapika/basic/Common.dart';\nimport 'package:pikapika/basic/Entities.dart';\nimport 'package:pikapika/basic/Method.dart';\nimport 'package:pikapika/screens/components/Avatar.dart';\nimport 'package:pikapika/screens/components/Images.dart';\nimport 'package:pikapika/screens/components/ItemBuilder.dart';\n\nimport '../../basic/config/IconLoading.dart';\nimport 'DesktopCropper.dart';\n\nconst double _cardHeight = 180;\n\n// 用户信息卡\nclass UserProfileCard extends StatefulWidget {\n  const UserProfileCard({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _UserProfileCardState();\n}\n\nclass _UserProfileCardState extends State<UserProfileCard> {\n  late Future<UserProfile> _future = _load();\n\n  Future<UserProfile> _load() async {\n    var profile = await method.userProfile();\n    if (!profile.isPunched) {\n      await method.punchIn();\n      profile.isPunched = true;\n      defaultToast(context, tr('app.auto_punch'));\n    }\n    return profile;\n  }\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var theme = Theme.of(context);\n    var nameStyle = const TextStyle(\n      fontSize: 14,\n      fontWeight: FontWeight.bold,\n    );\n    var nameStrutStyle = const StrutStyle(\n      fontSize: 14,\n      forceStrutHeight: true,\n      fontWeight: FontWeight.bold,\n    );\n    var levelStyle = TextStyle(\n      fontSize: 12,\n      color: theme.colorScheme.secondary.withOpacity(.9),\n      fontWeight: FontWeight.bold,\n    );\n    var levelStrutStyle = const StrutStyle(\n      fontSize: 12,\n      forceStrutHeight: true,\n      fontWeight: FontWeight.bold,\n    );\n    var sloganStyle = TextStyle(\n      fontSize: 10,\n      color: theme.textTheme.bodyText1?.color?.withOpacity(.5),\n    );\n    var sloganStrutStyle = const StrutStyle(\n      fontSize: 10,\n      forceStrutHeight: true,\n    );\n    return ItemBuilder(\n      future: _future,\n      onRefresh: () async {\n        setState(() => _future = method.userProfile());\n      },\n      height: _cardHeight,\n      successBuilder:\n          (BuildContext context, AsyncSnapshot<UserProfile> snapshot) {\n        UserProfile profile = snapshot.data!;\n        return Stack(\n          children: [\n            Stack(\n              children: [\n                Opacity(\n                  opacity: .25, //\n                  child: LayoutBuilder(\n                    builder:\n                        (BuildContext context, BoxConstraints constraints) {\n                      return RemoteImage(\n                        path: profile.avatar.path,\n                        fileServer: profile.avatar.fileServer,\n                        width: constraints.maxWidth,\n                        height: _cardHeight,\n                      );\n                    },\n                  ),\n                ),\n                Positioned.fromRect(\n                  rect: Rect.largest,\n                  child: BackdropFilter(\n                    filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),\n                    child: Container(),\n                  ),\n                ),\n              ],\n            ),\n            SizedBox(\n              height: _cardHeight,\n              child: Column(\n                children: [\n                  Expanded(child: Container()),\n                  GestureDetector(\n                    onTap: () async {\n                      if (Platform.isAndroid || Platform.isIOS) {\n                        await _updateAvatarPhone();\n                      } else if (Platform.isMacOS ||\n                          Platform.isWindows ||\n                          Platform.isLinux) {\n                        await _updateAvatarDesktop();\n                      }\n                    },\n                    child: Avatar(profile.avatar, size: 65),\n                  ),\n                  Container(height: 5),\n                  Text(\n                    profile.name,\n                    style: nameStyle,\n                    strutStyle: nameStrutStyle,\n                  ),\n                  Text(\n                    \"(Lv. ${profile.level}) (${profile.title})\",\n                    style: levelStyle,\n                    strutStyle: levelStrutStyle,\n                  ),\n                  Container(height: 8),\n                  GestureDetector(\n                    onTap: () async {\n                      var input = await inputString(\n                        context,\n                        \"更新签名\",\n                        defaultValue: profile.slogan ?? \"\",\n                      );\n                      if (input != null) {\n                        await method.updateSlogan(input);\n                        _reload();\n                      }\n                    },\n                    child: Text(\n                      profile.slogan == null || profile.slogan!.isEmpty\n                          ? \"这个人很懒, 什么也没留下\"\n                          : profile.slogan!,\n                      style: sloganStyle,\n                      strutStyle: sloganStrutStyle,\n                    ),\n                  ),\n                  Expanded(child: Container()),\n                ],\n              ),\n            )\n          ],\n        );\n      },\n    );\n  }\n\n  Future _updateAvatarPhone() async {\n    final ImagePicker _picker = ImagePicker();\n    final XFile? image = await _picker.pickImage(source: ImageSource.gallery);\n    if (image != null) {\n      final theme = Theme.of(context);\n      final cropper = ImageCropper();\n      File? croppedFile = await cropper.cropImage(\n        sourcePath: image.path,\n        aspectRatioPresets: [\n          CropAspectRatioPreset.square,\n        ],\n        aspectRatio: const CropAspectRatio(ratioX: 200, ratioY: 200),\n        maxWidth: 200,\n        maxHeight: 200,\n        androidUiSettings: AndroidUiSettings(\n          toolbarTitle: \"修改头像\",\n          toolbarColor: theme.appBarTheme.backgroundColor,\n          toolbarWidgetColor: Colors.white,\n          initAspectRatio: CropAspectRatioPreset.original,\n          lockAspectRatio: true,\n        ),\n        iosUiSettings: const IOSUiSettings(\n          resetAspectRatioEnabled: true,\n          aspectRatioLockEnabled: true,\n          title: \"修改头像\",\n        ),\n      );\n      if (croppedFile != null) {\n        var buff = await croppedFile.readAsBytes();\n        var data = base64Encode(buff);\n        await method.updateAvatar(data);\n        _reload();\n      }\n    }\n  }\n\n  Future _updateAvatarDesktop() async {\n    FilePickerResult? result = await FilePicker.platform.pickFiles(\n      type: FileType.image,\n      allowMultiple: false,\n    );\n    if (result != null) {\n      List<int>? buff = await Navigator.of(context).push(\n        mixRoute(builder: (BuildContext context) {\n          return DesktopCropper(\n            file: result.files.first.path!,\n            aspectRatio: 1,\n            title: \"裁剪头像\",\n          );\n        }),\n      );\n      if (buff != null) {\n        var data = base64Encode(buff);\n        await method.updateAvatar(data);\n        _reload();\n      }\n    }\n  }\n\n  void _reload() {\n    setState(() {\n      _future = _load();\n    });\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/flutter_search_bar.dart",
    "content": "// Copyright (c) 2017, Spencer. All rights reserved. Use of this source code\r\n// is governed by a BSD-style license that can be found in the LICENSE file.\r\n\r\nimport 'package:flutter/material.dart';\r\n\r\ntypedef Widget AppBarCallback(BuildContext context);\r\ntypedef void TextFieldSubmitCallback(String value);\r\ntypedef void TextFieldChangeCallback(String value);\r\ntypedef void SetStateCallback(void fn());\r\n\r\nclass SearchBar {\r\n  /// Whether the search should take place \"in the existing search bar\", meaning whether it has the same background or a flipped one. Defaults to true.\r\n  final bool inBar;\r\n\r\n  /// Whether or not the search bar should close on submit. Defaults to true.\r\n  final bool closeOnSubmit;\r\n\r\n  /// Whether the text field should be cleared when it is submitted\r\n  final bool clearOnSubmit;\r\n\r\n  /// A callback which should return an AppBar that is displayed until search is started. One of the actions in this AppBar should be a search button which you obtain from SearchBar.getSearchAction(). This will be called every time search is ended, etc. (like a build method on a widget)\r\n  final AppBarCallback buildDefaultAppBar;\r\n\r\n  /// A void callback which takes a string as an argument, this is fired every time the search is submitted. Do what you want with the result.\r\n  final TextFieldSubmitCallback? onSubmitted;\r\n\r\n  /// A void callback which gets fired on close button press.\r\n  final VoidCallback? onClosed;\r\n\r\n  /// A callback which is fired when clear button is pressed.\r\n  final VoidCallback? onCleared;\r\n\r\n  /// Since this should be inside of a State class, just pass setState to this.\r\n  final SetStateCallback setState;\r\n\r\n  /// Whether or not the search bar should add a clear input button, defaults to true.\r\n  final bool showClearButton;\r\n\r\n  /// What the hintText on the search bar should be. Defaults to 'Search'.\r\n  final String hintText;\r\n\r\n  /// Whether search is currently active.\r\n  final ValueNotifier<bool> isSearching = ValueNotifier(false);\r\n\r\n  /// A callback which is invoked each time the text field's value changes\r\n  final TextFieldChangeCallback? onChanged;\r\n\r\n  /// The type of keyboard to use for editing the search bar text. Defaults to 'TextInputType.text'.\r\n  final TextInputType keyboardType;\r\n\r\n  /// The controller to be used in the textField.\r\n  late TextEditingController controller;\r\n\r\n  /// Whether the clear button should be active (fully colored) or inactive (greyed out)\r\n  bool _clearActive = false;\r\n\r\n  SearchBar({\r\n    required this.setState,\r\n    required this.buildDefaultAppBar,\r\n    this.onSubmitted,\r\n    TextEditingController? controller,\r\n    this.hintText = 'Search',\r\n    this.inBar = true,\r\n    this.closeOnSubmit = true,\r\n    this.clearOnSubmit = true,\r\n    this.showClearButton = true,\r\n    this.onChanged,\r\n    this.onClosed,\r\n    this.onCleared,\r\n    this.keyboardType = TextInputType.text,\r\n  }) {\r\n    this.controller = controller ?? new TextEditingController();\r\n\r\n    // Don't waste resources on listeners for the text controller if the dev\r\n    // doesn't want a clear button anyways in the search bar\r\n    if (!this.showClearButton) {\r\n      return;\r\n    }\r\n\r\n    this.controller.addListener(() {\r\n      if (this.controller.text.isEmpty) {\r\n        // If clear is already disabled, don't disable it\r\n        if (_clearActive) {\r\n          setState(() {\r\n            _clearActive = false;\r\n          });\r\n        }\r\n\r\n        return;\r\n      }\r\n\r\n      // If clear is already enabled, don't enable it\r\n      if (!_clearActive) {\r\n        setState(() {\r\n          _clearActive = true;\r\n        });\r\n      }\r\n    });\r\n  }\r\n\r\n  /// Initializes the search bar.\r\n  ///\r\n  /// This adds a route that listens for onRemove (and stops the search when that happens), and then calls [setState] to rebuild and start the search.\r\n  void beginSearch(context) {\r\n    ModalRoute.of(context)!.addLocalHistoryEntry(LocalHistoryEntry(onRemove: () {\r\n      setState(() {\r\n        isSearching.value = false;\r\n      });\r\n    }));\r\n\r\n    setState(() {\r\n      isSearching.value = true;\r\n    });\r\n  }\r\n\r\n  /// Builds, saves and returns the default app bar.\r\n  ///\r\n  /// This calls the [buildDefaultAppBar] provided in the constructor.\r\n  AppBar buildAppBar(BuildContext context) {\r\n    return buildDefaultAppBar(context) as AppBar;\r\n  }\r\n\r\n  /// Builds the search bar!\r\n  ///\r\n  /// The leading will always be a back button.\r\n  /// backgroundColor is determined by the value of inBar\r\n  /// title is always a [TextField] with the key 'SearchBarTextField', and various text stylings based on [inBar]. This is also where [onSubmitted] has its listener registered.\r\n  ///\r\n  AppBar buildSearchBar(BuildContext context) {\r\n    ThemeData theme = Theme.of(context);\r\n    Color? buttonColor = inBar ? null : theme.iconTheme.color;\r\n\r\n    return AppBar(\r\n      leading: IconButton(\r\n          icon: const BackButtonIcon(),\r\n          color: buttonColor,\r\n          tooltip: MaterialLocalizations.of(context).backButtonTooltip,\r\n          onPressed: () {\r\n            onClosed?.call();\r\n            controller.clear();\r\n            Navigator.maybePop(context);\r\n          }),\r\n      backgroundColor: inBar ? null : theme.canvasColor,\r\n      title: Directionality(\r\n        textDirection: Directionality.of(context),\r\n        child: TextField(\r\n          key: Key('SearchBarTextField'),\r\n          keyboardType: keyboardType,\r\n          decoration: InputDecoration(\r\n              hintText: hintText,\r\n              hintStyle: inBar\r\n                  ? null\r\n                  : TextStyle(\r\n                      color: theme.textTheme.headline4!.color,\r\n                    ),\r\n              enabledBorder: InputBorder.none,\r\n              focusedBorder: InputBorder.none,\r\n              border: InputBorder.none),\r\n          onChanged: this.onChanged,\r\n          onSubmitted: (String val) async {\r\n            if (closeOnSubmit) {\r\n              await Navigator.maybePop(context);\r\n            }\r\n\r\n            if (clearOnSubmit) {\r\n              controller.clear();\r\n            }\r\n            onSubmitted?.call(val);\r\n          },\r\n          autofocus: true,\r\n          controller: controller,\r\n        ),\r\n      ),\r\n      actions: !showClearButton\r\n          ? null\r\n          : <Widget>[\r\n              // Show an icon if clear is not active, so there's no ripple on tap\r\n              IconButton(\r\n                  icon: Icon(Icons.clear, semanticLabel: \"Clear\"),\r\n                  color: inBar ? null : buttonColor,\r\n                  disabledColor: inBar ? null : theme.disabledColor,\r\n                  onPressed: !_clearActive\r\n                      ? null\r\n                      : () {\r\n                          onCleared?.call();\r\n                          controller.clear();\r\n                        }),\r\n            ],\r\n    );\r\n  }\r\n\r\n  /// Returns an [IconButton] suitable for an Action\r\n  ///\r\n  /// Put this inside your [buildDefaultAppBar] method!\r\n  IconButton getSearchAction(BuildContext context) {\r\n    return IconButton(\r\n        icon: Icon(Icons.search, semanticLabel: \"Search\"),\r\n        onPressed: () {\r\n          beginSearch(context);\r\n        });\r\n  }\r\n\r\n  /// Returns an AppBar based on the value of [isSearching]\r\n  AppBar build(BuildContext context) {\r\n    return isSearching.value ? buildSearchBar(context) : buildAppBar(context);\r\n  }\r\n}\r\n"
  },
  {
    "path": "lib/screens/components/gesture_zoom_box.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'dart:math';\n\nclass GestureZoomBox extends StatefulWidget {\n  final double minScale;\n  final double maxScale;\n  final double doubleTapScale;\n  final Widget child;\n  final Duration duration;\n\n  const GestureZoomBox({\n    Key? key,\n    this.minScale = 1.0,\n    this.maxScale = 2.0,\n    this.doubleTapScale = 2.0,\n    required this.child,\n    this.duration = const Duration(milliseconds: 200),\n  })  : assert(maxScale >= 1.0),\n        assert(minScale > 0),\n        assert(maxScale >= minScale),\n        assert(doubleTapScale >= 1.0 && doubleTapScale <= maxScale),\n        super(key: key);\n\n  @override\n  State<StatefulWidget> createState() {\n    return _GestureZoomBoxState();\n  }\n}\n\nclass _GestureZoomBoxState extends State<GestureZoomBox>\n    with TickerProviderStateMixin {\n  AnimationController? _scaleAnimController; // 缩放动画控制器\n  AnimationController? _offsetAnimController; // 偏移动画控制器\n  ScaleUpdateDetails? _latestScaleUpdateDetails; // 上次缩放变化数据\n\n  double _scale = 1.0; // 当前缩放值\n  Offset _offset = Offset.zero; // 当前偏移值\n  Offset? _doubleTapPosition; // 双击缩放的点击位置\n\n  bool _isScaling = false;\n  bool _isDragging = false;\n\n  final double _maxDragOver = 100; // 拖动超出边界的最大值\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Transform(\n      alignment: Alignment.center,\n      transform: Matrix4.identity()\n        ..translate(_offset.dx, _offset.dy)\n        ..scale(_scale, _scale),\n      child: Listener(\n        onPointerUp: _onPointerUp,\n        child: GestureDetector(\n          onDoubleTap: _onDoubleTap,\n          onScaleStart: _onScaleStart,\n          onScaleUpdate: _onScaleUpdate,\n          onScaleEnd: _onScaleEnd,\n          child: AbsorbPointer(\n            absorbing: _scale != 1,\n            child: widget.child,\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _scaleAnimController?.dispose();\n    _offsetAnimController?.dispose();\n    super.dispose();\n  }\n\n  /// 处理手指抬起事件 [event]\n  _onPointerUp(PointerUpEvent event) {\n    _doubleTapPosition = event.localPosition;\n  }\n\n  /// 处理双击\n  _onDoubleTap() {\n    double targetScale = _scale == 1.0 ? widget.doubleTapScale : 1.0;\n    _animationScale(targetScale);\n    if (targetScale == 1.0) {\n      _animationOffset(Offset.zero);\n    }\n  }\n\n  _onScaleStart(ScaleStartDetails details) {\n    _scaleAnimController?.stop();\n    _offsetAnimController?.stop();\n    _isScaling = false;\n    _isDragging = false;\n    _latestScaleUpdateDetails = null;\n  }\n\n  /// 处理缩放变化 [details]\n  _onScaleUpdate(ScaleUpdateDetails details) {\n    setState(() {\n      if (details.scale != 1.0) {\n        _scaling(details);\n      } else {\n        _dragging(details);\n      }\n    });\n  }\n\n  /// 执行缩放\n  _scaling(ScaleUpdateDetails details) {\n    if (_isDragging) {\n      return;\n    }\n    final latestScaleUpdateDetails = _latestScaleUpdateDetails,\n        size = context.size;\n    _isScaling = true;\n    if (latestScaleUpdateDetails == null || size == null) {\n      _latestScaleUpdateDetails = details;\n      return;\n    }\n\n    // 计算缩放比例\n    double scaleIncrement = details.scale - latestScaleUpdateDetails.scale;\n    if (details.scale < 1.0 && _scale > 1.0) {\n      scaleIncrement *= _scale;\n    }\n    if (_scale < 1.0 && scaleIncrement < 0) {\n      scaleIncrement *= (_scale - 0.5);\n    } else if (_scale > widget.maxScale && scaleIncrement > 0) {\n      scaleIncrement *= (2.0 - (_scale - widget.maxScale));\n    }\n    _scale = max(_scale + scaleIncrement, 0.0);\n\n    // 计算缩放后偏移前（缩放前后的内容中心对齐）的左上角坐标变化\n    double scaleOffsetX = size.width * (_scale - 1.0) / 2;\n    double scaleOffsetY = size.height * (_scale - 1.0) / 2;\n    // 将缩放前的触摸点映射到缩放后的内容上\n    double scalePointDX =\n        (details.localFocalPoint.dx + scaleOffsetX - _offset.dx) / _scale;\n    double scalePointDY =\n        (details.localFocalPoint.dy + scaleOffsetY - _offset.dy) / _scale;\n    // 计算偏移，使缩放中心在屏幕上的位置保持不变\n    _offset += Offset(\n      (size.width / 2 - scalePointDX) * scaleIncrement,\n      (size.height / 2 - scalePointDY) * scaleIncrement,\n    );\n\n    _latestScaleUpdateDetails = details;\n  }\n\n  /// 执行拖动\n  _dragging(ScaleUpdateDetails details) {\n    if (_isScaling) {\n      return;\n    }\n    final latestScaleUpdateDetails = _latestScaleUpdateDetails,\n        size = context.size;\n    _isDragging = true;\n    if (latestScaleUpdateDetails == null || size == null) {\n      _latestScaleUpdateDetails = details;\n      return;\n    }\n\n    // 计算本次拖动增量\n    double offsetXIncrement = (details.localFocalPoint.dx -\n            latestScaleUpdateDetails.localFocalPoint.dx) *\n        _scale;\n    double offsetYIncrement = (details.localFocalPoint.dy -\n            latestScaleUpdateDetails.localFocalPoint.dy) *\n        _scale;\n    // 处理 X 轴边界\n    double scaleOffsetX = size.width * (_scale - 1.0) / 2;\n    if (scaleOffsetX <= 0) {\n      offsetXIncrement = 0;\n    } else if (_offset.dx > scaleOffsetX) {\n      offsetXIncrement *=\n          (_maxDragOver - (_offset.dx - scaleOffsetX)) / _maxDragOver;\n    } else if (_offset.dx < -scaleOffsetX) {\n      offsetXIncrement *=\n          (_maxDragOver - (-scaleOffsetX - _offset.dx)) / _maxDragOver;\n    }\n    // 处理 Y 轴边界\n    double scaleOffsetY =\n        (size.height * _scale - MediaQuery.of(context).size.height) / 2;\n    if (scaleOffsetY <= 0) {\n      offsetYIncrement = 0;\n    } else if (_offset.dy > scaleOffsetY) {\n      offsetYIncrement *=\n          (_maxDragOver - (_offset.dy - scaleOffsetY)) / _maxDragOver;\n    } else if (_offset.dy < -scaleOffsetY) {\n      offsetYIncrement *=\n          (_maxDragOver - (-scaleOffsetY - _offset.dy)) / _maxDragOver;\n    }\n\n    _offset += Offset(offsetXIncrement, offsetYIncrement);\n\n    _latestScaleUpdateDetails = details;\n  }\n\n  /// 缩放/拖动结束\n  _onScaleEnd(ScaleEndDetails details) {\n    final size = context.size;\n    if (size == null) {\n      return;\n    }\n    if (_scale < widget.minScale) {\n      // 缩放值过小，恢复到最小值\n      _animationScale(widget.minScale);\n    } else if (_scale > widget.maxScale) {\n      // 缩放值过大，恢复到最大值\n      _animationScale(widget.maxScale);\n    }\n    if (_scale <= widget.minScale) {\n      // 缩放值过小，修改偏移值，使内容居中\n      _animationOffset(Offset.zero);\n    } else if (_isDragging) {\n      // 处理拖动超过边界的情况（自动回弹到边界）\n      double realScale = _scale > widget.maxScale ? widget.maxScale : _scale;\n      double targetOffsetX = _offset.dx, targetOffsetY = _offset.dy;\n      // 处理 X 轴边界\n      double scaleOffsetX = size.width * (realScale - 1.0) / 2;\n      if (scaleOffsetX <= 0) {\n        targetOffsetX = 0;\n      } else if (_offset.dx > scaleOffsetX) {\n        targetOffsetX = scaleOffsetX;\n      } else if (_offset.dx < -scaleOffsetX) {\n        targetOffsetX = -scaleOffsetX;\n      }\n      // 处理 Y 轴边界\n      double scaleOffsetY =\n          (size.height * realScale - MediaQuery.of(context).size.height) / 2;\n      if (scaleOffsetY < 0) {\n        targetOffsetY = 0;\n      } else if (_offset.dy > scaleOffsetY) {\n        targetOffsetY = scaleOffsetY;\n      } else if (_offset.dy < -scaleOffsetY) {\n        targetOffsetY = -scaleOffsetY;\n      }\n      if (_offset.dx != targetOffsetX || _offset.dy != targetOffsetY) {\n        // 启动越界回弹\n        _animationOffset(Offset(targetOffsetX, targetOffsetY));\n      } else {\n        // 处理 X 轴边界\n        double duration =\n            (widget.duration.inSeconds + widget.duration.inMilliseconds / 1000);\n        Offset targetOffset =\n            _offset + details.velocity.pixelsPerSecond * duration;\n        targetOffsetX = targetOffset.dx;\n        if (targetOffsetX > scaleOffsetX) {\n          targetOffsetX = scaleOffsetX;\n        } else if (targetOffsetX < -scaleOffsetX) {\n          targetOffsetX = -scaleOffsetX;\n        }\n        // 处理 X 轴边界\n        targetOffsetY = targetOffset.dy;\n        if (targetOffsetY > scaleOffsetY) {\n          targetOffsetY = scaleOffsetY;\n        } else if (targetOffsetY < -scaleOffsetY) {\n          targetOffsetY = -scaleOffsetY;\n        }\n        // 启动惯性滚动\n        _animationOffset(Offset(targetOffsetX, targetOffsetY));\n      }\n    }\n\n    _isScaling = false;\n    _isDragging = false;\n    _latestScaleUpdateDetails = null;\n  }\n\n  /// 执行动画缩放内容到 [targetScale]\n  _animationScale(double targetScale) {\n    _scaleAnimController?.dispose();\n    final scaleAnimController = _scaleAnimController =\n        AnimationController(vsync: this, duration: widget.duration);\n    Animation anim = Tween<double>(begin: _scale, end: targetScale)\n        .animate(scaleAnimController);\n    anim.addListener(() {\n      setState(() {\n        _scaling(ScaleUpdateDetails(\n          focalPoint: _doubleTapPosition!,\n          localFocalPoint: _doubleTapPosition!,\n          scale: anim.value,\n          horizontalScale: anim.value,\n          verticalScale: anim.value,\n        ));\n      });\n    });\n    anim.addStatusListener((status) {\n      if (status == AnimationStatus.completed) {\n        _onScaleEnd(ScaleEndDetails());\n      }\n    });\n    scaleAnimController.forward();\n  }\n\n  /// 执行动画偏移内容到 [targetOffset]\n  _animationOffset(Offset targetOffset) {\n    _offsetAnimController?.dispose();\n    final offsetAnimController = _offsetAnimController =\n        AnimationController(vsync: this, duration: widget.duration);\n    Animation anim = offsetAnimController\n        .drive(Tween<Offset>(begin: _offset, end: targetOffset));\n    anim.addListener(() {\n      setState(() {\n        _offset = anim.value;\n      });\n    });\n    offsetAnimController.fling();\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\nset(BINARY_NAME \"pikapika\")\nset(APPLICATION_ID \"opensource.pikapika\")\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": "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": "linux/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <url_launcher_linux/url_launcher_plugin.h>\n\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"UrlLauncherPlugin\");\n  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);\n}\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter_linux/flutter_linux.h>\n\n// Registers Flutter plugins.\nvoid fl_register_plugins(FlPluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  url_launcher_linux\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "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": "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, \"pikapi\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  }\n  else {\n    gtk_window_set_title(window, \"pikapi\");\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": "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": "macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/xcuserdata/\n"
  },
  {
    "path": "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": "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": "macos/Flutter/GeneratedPluginRegistrant.swift",
    "content": "//\n//  Generated file. Do not edit.\n//\n\nimport FlutterMacOS\nimport Foundation\n\nimport shared_preferences_foundation\nimport url_launcher_macos\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: \"SharedPreferencesPlugin\"))\n  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: \"UrlLauncherPlugin\"))\n}\n"
  },
  {
    "path": "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__))\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": "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": "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": "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": "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 = pikapika\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = opensource\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "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": "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": "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": "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": "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": "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\tB7746CD3B58046AB2A30373A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F51BCF4C17559FFBAB4E95D7 /* 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\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 /* pikapika.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pikapika.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\t6DDC9F2D722240B8A73326EB /* 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\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\tB0D4B875C41B50DACC24CB89 /* 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\tF2AF3FFEFCDFF4E0D5A2FFB1 /* 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\tF51BCF4C17559FFBAB4E95D7 /* 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\tB7746CD3B58046AB2A30373A /* 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\t99812F7FCCD1CE46B3B8E505 /* 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 /* pikapika.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\t99812F7FCCD1CE46B3B8E505 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tF2AF3FFEFCDFF4E0D5A2FFB1 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\tB0D4B875C41B50DACC24CB89 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t6DDC9F2D722240B8A73326EB /* 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\tF51BCF4C17559FFBAB4E95D7 /* 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\t410398265F6EBE1BF9230F69 /* [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\tB17A392332FC2746FE84EE4D /* [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 /* pikapika.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 = 1300;\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\t410398265F6EBE1BF9230F69 /* [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\tB17A392332FC2746FE84EE4D /* [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/* 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": "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": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1300\"\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 = \"pikapika.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 = \"pikapika.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 = \"pikapika.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 = \"pikapika.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": "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": "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": "pubspec.yaml",
    "content": "name: pikapika\ndescription: A cross platform comic client.\npublish_to: 'none'\n\nversion: 1.8.19+43\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n\n  cupertino_icons: ^1.0.2\n  isolate: ^2.1.1\n  event: ^2.0.5\n  flutter_svg: ^1.0.3\n  flutter_styled_toast: 2.0.0\n  another_xlider: ^1.0.1+2\n  scrollable_positioned_list: ^0.2.0-nullsafety.0\n  permission_handler: ^10.1.0\n  url_launcher: ^6.0.9\n  clipboard: ^0.1.3\n  photo_view: 0.14.0\n  multi_select_flutter: ^4.0.0\n  # flutter_datetime_picker: 1.5.1\n  modal_bottom_sheet: ^3.0.0-pre\n  image_cropper: 1.5.0\n  image_picker: ^0.8.6\n  file_picker: 5.2.5\n  crop_image: 1.0.2\n  image: ^3.1.3\n  path: ^1.8.0\n  uri_to_file: ^0.2.0\n  uni_links: ^0.5.1\n  filesystem_picker: ^3.0.0-beta.1\n  easy_localization: ^3.0.7+1\n  zoomable_positioned_list: 1.0.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^1.0.0\n\nflutter:\n  uses-material-design: true\n  assets:\n    - lib/assets/\n    - lib/assets/translations/\n"
  },
  {
    "path": "scripts/README.md",
    "content": "用于记录作者构建时使用的脚本\n"
  },
  {
    "path": "scripts/bind-android-arm64.sh",
    "content": "\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/arm64 -o lib/Mobile.aar ./\n"
  },
  {
    "path": "scripts/bind-android-debug.sh",
    "content": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./\n"
  },
  {
    "path": "scripts/bind-ios-arm64.sh",
    "content": "\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "scripts/bind-ios.sh",
    "content": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "scripts/build-apk-arm.sh",
    "content": "# 仅构建arm的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/arm -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-arm\n"
  },
  {
    "path": "scripts/build-apk-arm64.sh",
    "content": "# 仅构建arm64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/arm64 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-arm64\n"
  },
  {
    "path": "scripts/build-apk-x64.sh",
    "content": "# 仅构建x86_64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/amd64 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-x64\n"
  },
  {
    "path": "scripts/build-apk-x86.sh",
    "content": "# 仅构建x86的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -androidapi 21 -target=android/386 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-x86\n"
  },
  {
    "path": "scripts/build-ipa.sh",
    "content": "# 构建未签名的IPA\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngomobile init\ngomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./\ncd ../..\nflutter build ios --release --no-codesign\n\ncd build\nmkdir -p Payload\nmv ios/iphoneos/Runner.app Payload\n\nsh ../scripts/thin-payload.sh\nzip -9 nosign.ipa -r Payload\n"
  },
  {
    "path": "scripts/build-linux.sh",
    "content": "#!/usr/bin/env bash\n\ncurl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf\nmkdir -p fonts\nmv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf\ncat ci/linux_font.yaml >> pubspec.yaml\nhover build linux-appimage\nmv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage\n"
  },
  {
    "path": "scripts/build-macos-dmg.sh",
    "content": "# 构建macos\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\nhover build darwin-dmg\n"
  },
  {
    "path": "scripts/json_compairer.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport json\nimport os\nimport sys\nimport argparse\nfrom pathlib import Path\nfrom typing import Dict, List, Set, Any, Union\nfrom datetime import datetime\n\ndef flatten_json(data: Dict[str, Any], parent_key: str = '', separator: str = '.') -> Dict[str, Any]:\n    \"\"\"\n    将嵌套的JSON对象扁平化为点分隔的键值对\n    \n    Args:\n        data: 要扁平化的JSON数据\n        parent_key: 父级键名\n        separator: 键名分隔符\n    \n    Returns:\n        扁平化后的字典\n    \"\"\"\n    items = []\n    for key, value in data.items():\n        new_key = f\"{parent_key}{separator}{key}\" if parent_key else key\n        if isinstance(value, dict):\n            items.extend(flatten_json(value, new_key, separator).items())\n        else:\n            items.append((new_key, value))\n    return dict(items)\n\ndef unflatten_json(data: Dict[str, Any], separator: str = '.') -> Dict[str, Any]:\n    \"\"\"\n    将扁平化的JSON对象还原为嵌套结构\n    \n    Args:\n        data: 扁平化的JSON数据\n        separator: 键名分隔符\n    \n    Returns:\n        嵌套的JSON对象\n    \"\"\"\n    result = {}\n    for key, value in data.items():\n        parts = key.split(separator)\n        current = result\n        for part in parts[:-1]:\n            if part not in current:\n                current[part] = {}\n            current = current[part]\n        current[parts[-1]] = value\n    return result\n\ndef load_json_file(file_path: str) -> Dict[str, Any]:\n    \"\"\"\n    加载JSON文件\n    \n    Args:\n        file_path: JSON文件路径\n    \n    Returns:\n        JSON数据字典\n    \"\"\"\n    try:\n        with open(file_path, 'r', encoding='utf-8') as f:\n            return json.load(f)\n    except FileNotFoundError:\n        print(f\"❌ 文件不存在: {file_path}\")\n        return {}\n    except json.JSONDecodeError as e:\n        print(f\"❌ JSON解析错误 {file_path}: {e}\")\n        return {}\n    except Exception as e:\n        print(f\"❌ 读取文件错误 {file_path}: {e}\")\n        return {}\n\ndef save_json_file(file_path: str, data: Dict[str, Any]) -> bool:\n    \"\"\"\n    保存JSON文件\n    \n    Args:\n        file_path: JSON文件路径\n        data: 要保存的数据\n    \n    Returns:\n        是否保存成功\n    \"\"\"\n    try:\n        with open(file_path, 'w', encoding='utf-8') as f:\n            json.dump(data, f, ensure_ascii=False, indent=2)\n        return True\n    except Exception as e:\n        print(f\"❌ 保存文件错误 {file_path}: {e}\")\n        return False\n\ndef get_all_json_files(translations_dir: str) -> List[str]:\n    \"\"\"\n    获取翻译目录下的所有JSON文件\n    \n    Args:\n        translations_dir: 翻译文件目录\n    \n    Returns:\n        JSON文件路径列表\n    \"\"\"\n    json_files = []\n    if os.path.exists(translations_dir):\n        for file in os.listdir(translations_dir):\n            if file.endswith('.json'):\n                json_files.append(os.path.join(translations_dir, file))\n    return sorted(json_files)\n\ndef find_reference_value(key: str, all_data: Dict[str, Dict[str, Any]]) -> str:\n    \"\"\"\n    为缺失的键找到参考值\n    \n    Args:\n        key: 缺失的键\n        all_data: 所有翻译数据\n    \n    Returns:\n        参考值或占位符\n    \"\"\"\n    # 首先尝试从其他文件中找到这个键的值\n    for filename, data in all_data.items():\n        flattened = flatten_json(data)\n        if key in flattened:\n            return flattened[key]\n    \n    # 如果没找到，生成一个占位符\n    key_parts = key.split('.')\n    last_part = key_parts[-1]\n    \n    # 根据键名生成合理的占位符\n    placeholders = {\n        'title': 'Title',\n        'name': 'Name',\n        'description': 'Description',\n        'hint': 'Hint',\n        'label': 'Label',\n        'placeholder': 'Placeholder',\n        'button': 'Button',\n        'confirm': 'Confirm',\n        'cancel': 'Cancel',\n        'yes': 'Yes',\n        'no': 'No',\n        'loading': 'Loading...',\n        'error': 'Error',\n        'success': 'Success',\n        'failed': 'Failed',\n        'warning': 'Warning',\n    }\n    \n    for placeholder_key, placeholder_value in placeholders.items():\n        if placeholder_key in last_part.lower():\n            return placeholder_value\n    \n    # 默认占位符\n    return f\"TODO: {last_part.replace('_', ' ').title()}\"\n\ndef generate_missing_keys_report(translations_dir: str, output_file: Union[str, None] = None):\n    \"\"\"\n    生成缺失键的报告\n    \n    Args:\n        translations_dir: 翻译文件目录\n        output_file: 输出文件路径\n    \"\"\"\n    json_files = get_all_json_files(translations_dir)\n    \n    if not json_files:\n        print(f\"❌ 在目录 {translations_dir} 中没有找到JSON文件\")\n        return\n    \n    # 加载所有JSON文件\n    all_data = {}\n    all_keys = {}\n    \n    for file_path in json_files:\n        filename = os.path.basename(file_path)\n        data = load_json_file(file_path)\n        if data:\n            all_data[filename] = data\n            all_keys[filename] = set(flatten_json(data).keys())\n    \n    if not all_data:\n        print(\"❌ 没有成功加载任何JSON文件\")\n        return\n    \n    # 找出所有键的并集\n    all_possible_keys = set()\n    for keys in all_keys.values():\n        all_possible_keys.update(keys)\n    \n    # 生成报告\n    report = []\n    report.append(f\"# 翻译文件缺失键报告\")\n    report.append(f\"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n    report.append(f\"翻译文件目录: {translations_dir}\")\n    report.append(f\"总键数: {len(all_possible_keys)}\")\n    report.append(\"\")\n    \n    # 统计信息\n    report.append(\"## 统计信息\")\n    report.append(\"| 文件名 | 键数量 | 缺失键数量 |\")\n    report.append(\"|--------|--------|------------|\")\n    \n    for filename, keys in all_keys.items():\n        missing_count = len(all_possible_keys - keys)\n        report.append(f\"| {filename} | {len(keys)} | {missing_count} |\")\n    \n    report.append(\"\")\n    \n    # 详细缺失信息\n    report.append(\"## 缺失键详情\")\n    \n    for filename, keys in all_keys.items():\n        missing_keys = all_possible_keys - keys\n        if missing_keys:\n            report.append(f\"### {filename}\")\n            report.append(f\"缺失 {len(missing_keys)} 个键:\")\n            for key in sorted(missing_keys):\n                report.append(f\"- `{key}`\")\n            report.append(\"\")\n    \n    # 输出报告\n    report_content = \"\\n\".join(report)\n    \n    if output_file:\n        try:\n            with open(output_file, 'w', encoding='utf-8') as f:\n                f.write(report_content)\n            print(f\"✅ 报告已保存到: {output_file}\")\n        except Exception as e:\n            print(f\"❌ 保存报告失败: {e}\")\n    else:\n        print(report_content)\n\ndef fix_missing_keys(translations_dir: str, dry_run: bool = True):\n    \"\"\"\n    修复缺失的键\n    \n    Args:\n        translations_dir: 翻译文件目录\n        dry_run: 是否为试运行模式\n    \"\"\"\n    json_files = get_all_json_files(translations_dir)\n    \n    if not json_files:\n        print(f\"❌ 在目录 {translations_dir} 中没有找到JSON文件\")\n        return\n    \n    # 加载所有JSON文件\n    all_data = {}\n    all_keys = {}\n    \n    for file_path in json_files:\n        filename = os.path.basename(file_path)\n        data = load_json_file(file_path)\n        if data:\n            all_data[filename] = data\n            all_keys[filename] = set(flatten_json(data).keys())\n    \n    if not all_data:\n        print(\"❌ 没有成功加载任何JSON文件\")\n        return\n    \n    # 找出所有键的并集\n    all_possible_keys = set()\n    for keys in all_keys.values():\n        all_possible_keys.update(keys)\n    \n    # 修复每个文件的缺失键\n    for file_path in json_files:\n        filename = os.path.basename(file_path)\n        if filename not in all_data:\n            continue\n        \n        missing_keys = all_possible_keys - all_keys[filename]\n        if not missing_keys:\n            print(f\"✅ {filename} 无需修复\")\n            continue\n        \n        print(f\"🔧 {'[试运行] ' if dry_run else ''}修复 {filename} 的 {len(missing_keys)} 个缺失键:\")\n        \n        # 获取当前文件的扁平化数据\n        flattened_data = flatten_json(all_data[filename])\n        \n        # 添加缺失的键\n        for key in sorted(missing_keys):\n            reference_value = find_reference_value(key, all_data)\n            flattened_data[key] = reference_value\n            print(f\"  + {key} = \\\"{reference_value}\\\"\")\n        \n        # 如果不是试运行，保存文件\n        if not dry_run:\n            # 将扁平化数据还原为嵌套结构\n            nested_data = unflatten_json(flattened_data)\n            \n            # 保存文件\n            if save_json_file(file_path, nested_data):\n                print(f\"  ✅ 已保存 {filename}\")\n            else:\n                print(f\"  ❌ 保存 {filename} 失败\")\n        \n        print()\n\ndef compare_translation_files(translations_dir: str):\n    \"\"\"\n    比较翻译文件，找出缺失的键\n    \n    Args:\n        translations_dir: 翻译文件目录\n    \"\"\"\n    json_files = get_all_json_files(translations_dir)\n    \n    if not json_files:\n        print(f\"❌ 在目录 {translations_dir} 中没有找到JSON文件\")\n        return\n    \n    print(f\"🔍 找到 {len(json_files)} 个翻译文件:\")\n    for file in json_files:\n        print(f\"  - {os.path.basename(file)}\")\n    print()\n    \n    # 加载所有JSON文件\n    all_data = {}\n    all_keys = {}\n    \n    for file_path in json_files:\n        filename = os.path.basename(file_path)\n        data = load_json_file(file_path)\n        if data:\n            all_data[filename] = data\n            all_keys[filename] = set(flatten_json(data).keys())\n        else:\n            print(f\"⚠️  跳过空文件: {filename}\")\n    \n    if not all_data:\n        print(\"❌ 没有成功加载任何JSON文件\")\n        return\n    \n    # 找出所有键的并集\n    all_possible_keys = set()\n    for keys in all_keys.values():\n        all_possible_keys.update(keys)\n    \n    print(f\"📊 总共发现 {len(all_possible_keys)} 个唯一键\")\n    print()\n    \n    # 检查每个文件缺失的键\n    has_missing_keys = False\n    \n    for filename, keys in all_keys.items():\n        missing_keys = all_possible_keys - keys\n        if missing_keys:\n            has_missing_keys = True\n            print(f\"❌ {filename} 缺失 {len(missing_keys)} 个键:\")\n            for key in sorted(missing_keys):\n                print(f\"  - {key}\")\n            print()\n        else:\n            print(f\"✅ {filename} 包含所有键\")\n    \n    if not has_missing_keys:\n        print(\"🎉 所有翻译文件都包含相同的键！\")\n        return\n    \n    # 显示键统计信息\n    print(\"\\n📈 键统计信息:\")\n    print(f\"{'文件名':<20} {'键数量':<10} {'缺失键数量':<12}\")\n    print(\"-\" * 45)\n    \n    for filename, keys in all_keys.items():\n        missing_count = len(all_possible_keys - keys)\n        print(f\"{filename:<20} {len(keys):<10} {missing_count:<12}\")\n    \n    # 找出只在某些文件中存在的键\n    print(\"\\n🔍 键分布分析:\")\n    key_distribution = {}\n    for key in all_possible_keys:\n        files_with_key = [filename for filename, keys in all_keys.items() if key in keys]\n        key_distribution[key] = files_with_key\n    \n    # 找出不在所有文件中的键\n    incomplete_keys = {key: files for key, files in key_distribution.items() if len(files) < len(all_keys)}\n    \n    if incomplete_keys:\n        print(f\"发现 {len(incomplete_keys)} 个键不在所有文件中:\")\n        for key, files in sorted(incomplete_keys.items()):\n            missing_files = [f for f in all_keys.keys() if f not in files]\n            print(f\"  {key}\")\n            print(f\"    存在于: {', '.join(files)}\")\n            print(f\"    缺失于: {', '.join(missing_files)}\")\n            print()\n    else:\n        print(\"所有键都在所有文件中存在！\")\n\ndef main():\n    \"\"\"主函数\"\"\"\n    parser = argparse.ArgumentParser(description='比较翻译文件中的键，找出缺失的键')\n    parser.add_argument('translations_dir', nargs='?', \n                       help='翻译文件目录路径 (默认: ../lib/assets/translations)')\n    parser.add_argument('--report', '-r', metavar='FILE',\n                       help='生成报告并保存到指定文件')\n    parser.add_argument('--fix', '-f', action='store_true',\n                       help='修复缺失的键')\n    parser.add_argument('--dry-run', '-d', action='store_true',\n                       help='试运行模式，不实际修改文件')\n    \n    args = parser.parse_args()\n    \n    # 获取脚本所在目录\n    script_dir = os.path.dirname(os.path.abspath(__file__))\n    \n    # 翻译文件目录路径\n    if args.translations_dir:\n        translations_dir = args.translations_dir\n    else:\n        translations_dir = os.path.join(script_dir, '..', 'lib', 'assets', 'translations')\n        translations_dir = os.path.normpath(translations_dir)\n    \n    print(f\"🚀 开始比较翻译文件...\")\n    print(f\"📁 翻译文件目录: {translations_dir}\")\n    print()\n    \n    if not os.path.exists(translations_dir):\n        print(f\"❌ 翻译文件目录不存在: {translations_dir}\")\n        sys.exit(1)\n    \n    if args.report:\n        generate_missing_keys_report(translations_dir, args.report)\n    elif args.fix:\n        fix_missing_keys(translations_dir, dry_run=args.dry_run)\n    else:\n        compare_translation_files(translations_dir)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/sign-apk-github-actions.sh",
    "content": "cd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\necho $KEY_FILE_BASE64 > key.jks.base64\nbase64 -d key.jks.base64 > key.jks\necho $KEY_PASSWORD | $ANDROID_HOME/build-tools/30.0.2/apksigner sign --ks key.jks build/app/outputs/flutter-apk/app-release.apk"
  },
  {
    "path": "scripts/thin-payload.sh",
    "content": "# 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa)\n\nforeachThin(){\n  for file in $1/*\n  do\n      if test -f $file\n      then\n           mime=$(file --mime-type -b $file)\n           if [ \"$mime\" == 'application/x-mach-binary' ]  || [ \"${file##*.}\"x = \"dylib\"x ]\n           then\n                echo thin $file\n                xcrun -sdk iphoneos lipo \"$file\" -thin arm64 -output \"$file\"\n                xcrun -sdk iphoneos bitcode_strip \"$file\" -r -o  \"$file\"\n                strip -S -x \"$file\" -o \"$file\"\n           fi\n      fi\n      if test -d $file\n      then\n          foreachThin $file\n      fi\n  done\n}\n\nforeachThin ./Payload\n"
  },
  {
    "path": "scripts/version.sh",
    "content": "# 设置版本号\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\nif [ \"$1\" == \"set\" ] ; then\n  if [ \"$2\" != \"\" ] ; then\n    echo $2 > lib/assets/version.txt\n  fi\n\nelif [ \"$1\" == \"unset\" ]; then\n    rm -f lib/assets/version.txt\nfi\n"
  },
  {
    "path": "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\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:pikapika/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 PikapikaApp());\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": "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": "windows/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\nproject(pikapika LANGUAGES CXX)\n\nset(BINARY_NAME \"pikapika\")\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": "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": "windows/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <permission_handler_windows/permission_handler_windows_plugin.h>\n#include <url_launcher_windows/url_launcher_windows.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  PermissionHandlerWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"PermissionHandlerWindowsPlugin\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n}\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter/plugin_registry.h>\n\n// Registers Flutter plugins.\nvoid RegisterPlugins(flutter::PluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  permission_handler_windows\n  url_launcher_windows\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "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  \"run_loop.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": "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\", \"A new Flutter project.\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"pikapika\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2021 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"pikapika.exe\" \"\\0\"\n            VALUE \"ProductName\", \"pikapika\" \"\\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": "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(RunLoop* run_loop,\n                             const flutter::DartProject& project)\n    : run_loop_(run_loop), 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  run_loop_->RegisterFlutterInstance(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    run_loop_->UnregisterFlutterInstance(flutter_controller_->engine());\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": "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 \"run_loop.h\"\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 driven by the |run_loop|, hosting a\n  // Flutter view running |project|.\n  explicit FlutterWindow(RunLoop* run_loop,\n                         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 run loop driving events for this window.\n  RunLoop* run_loop_;\n\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": "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 \"run_loop.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  RunLoop run_loop;\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(&run_loop, project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.CreateAndShow(L\"pikapika\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  run_loop.Run();\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "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": "windows/runner/run_loop.cpp",
    "content": "#include \"run_loop.h\"\n\n#include <windows.h>\n\n#include <algorithm>\n\nRunLoop::RunLoop() {}\n\nRunLoop::~RunLoop() {}\n\nvoid RunLoop::Run() {\n  bool keep_running = true;\n  TimePoint next_flutter_event_time = TimePoint::clock::now();\n  while (keep_running) {\n    std::chrono::nanoseconds wait_duration =\n        std::max(std::chrono::nanoseconds(0),\n                 next_flutter_event_time - TimePoint::clock::now());\n    ::MsgWaitForMultipleObjects(\n        0, nullptr, FALSE, static_cast<DWORD>(wait_duration.count() / 1000),\n        QS_ALLINPUT);\n    bool processed_events = false;\n    MSG message;\n    // All pending Windows messages must be processed; MsgWaitForMultipleObjects\n    // won't return again for items left in the queue after PeekMessage.\n    while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) {\n      processed_events = true;\n      if (message.message == WM_QUIT) {\n        keep_running = false;\n        break;\n      }\n      ::TranslateMessage(&message);\n      ::DispatchMessage(&message);\n      // Allow Flutter to process messages each time a Windows message is\n      // processed, to prevent starvation.\n      next_flutter_event_time =\n          std::min(next_flutter_event_time, ProcessFlutterMessages());\n    }\n    // If the PeekMessage loop didn't run, process Flutter messages.\n    if (!processed_events) {\n      next_flutter_event_time =\n          std::min(next_flutter_event_time, ProcessFlutterMessages());\n    }\n  }\n}\n\nvoid RunLoop::RegisterFlutterInstance(\n    flutter::FlutterEngine* flutter_instance) {\n  flutter_instances_.insert(flutter_instance);\n}\n\nvoid RunLoop::UnregisterFlutterInstance(\n    flutter::FlutterEngine* flutter_instance) {\n  flutter_instances_.erase(flutter_instance);\n}\n\nRunLoop::TimePoint RunLoop::ProcessFlutterMessages() {\n  TimePoint next_event_time = TimePoint::max();\n  for (auto instance : flutter_instances_) {\n    std::chrono::nanoseconds wait_duration = instance->ProcessMessages();\n    if (wait_duration != std::chrono::nanoseconds::max()) {\n      next_event_time =\n          std::min(next_event_time, TimePoint::clock::now() + wait_duration);\n    }\n  }\n  return next_event_time;\n}\n"
  },
  {
    "path": "windows/runner/run_loop.h",
    "content": "#ifndef RUNNER_RUN_LOOP_H_\n#define RUNNER_RUN_LOOP_H_\n\n#include <flutter/flutter_engine.h>\n\n#include <chrono>\n#include <set>\n\n// A runloop that will service events for Flutter instances as well\n// as native messages.\nclass RunLoop {\n public:\n  RunLoop();\n  ~RunLoop();\n\n  // Prevent copying\n  RunLoop(RunLoop const&) = delete;\n  RunLoop& operator=(RunLoop const&) = delete;\n\n  // Runs the run loop until the application quits.\n  void Run();\n\n  // Registers the given Flutter instance for event servicing.\n  void RegisterFlutterInstance(\n      flutter::FlutterEngine* flutter_instance);\n\n  // Unregisters the given Flutter instance from event servicing.\n  void UnregisterFlutterInstance(\n      flutter::FlutterEngine* flutter_instance);\n\n private:\n  using TimePoint = std::chrono::steady_clock::time_point;\n\n  // Processes all currently pending messages for registered Flutter instances.\n  TimePoint ProcessFlutterMessages();\n\n  std::set<flutter::FlutterEngine*> flutter_instances_;\n};\n\n#endif  // RUNNER_RUN_LOOP_H_\n"
  },
  {
    "path": "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": "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": "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": "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": "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"
  }
]