[
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build ALL\nrun-name: Build ALL\non:\n  workflow_dispatch:\n    inputs:\n      # macOS build disabled: the macOS job frequently exceeds GitHub Actions' job time limit during Flutter builds.\n      # build_macos:\n      #   description: Build macOS\n      #   type: boolean\n      #   default: true\n      build_ios:\n        description: Build iOS\n        type: boolean\n        default: true\n      build_android:\n        description: Build Android\n        type: boolean\n        default: true\n      build_windows:\n        description: Build Windows\n        type: boolean\n        default: true\n      build_linux:\n        description: Build Linux (x64)\n        type: boolean\n        default: true\n      build_linux_arm64:\n        description: Build Linux (ARM64)\n        type: boolean\n        default: true\n  release:\n    types: [published]\n\njobs:\n  # macOS build disabled: macOS builds on GitHub-hosted runners often hit the job timeout.\n  # Build_MacOS:\n  #   if: github.event_name == 'release' || github.event.inputs.build_macos == 'true'\n  #   runs-on: macos-15\n  #   steps:\n  #     - uses: actions/checkout@v3\n  #     - uses: subosito/flutter-action@v2\n  #       with:\n  #         channel: \"stable\"\n  #         flutter-version-file: pubspec.yaml\n  #     - run: sudo xcode-select --switch /Applications/Xcode_16.4.0.app\n  #     - run: flutter pub get\n  #       # Step 1: Decode and install the certificate\n  #     - name: Decode and install certificate\n  #       env:\n  #         CERTIFICATE: ${{ secrets.CERTIFICATE }}\n  #         CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}\n  #       run: |\n  #         echo \"$CERTIFICATE\" | base64 --decode > signing_certificate.p12\n  #         security import signing_certificate.p12 -k ~/Library/Keychains/login.keychain -P \"$CERTIFICATE_PASSWORD\" -T /usr/bin/codesign\n  #\n  #     # Step 2: Build the Flutter macOS app\n  #     - name: Build Flutter macOS App\n  #       run: flutter build macos --release\n  #\n  #     # Step 3: Create the DMG file\n  #     - name: Create DMG\n  #       run: |\n  #         mkdir -p dist\n  #         mkdir -p dist/dmg_contents\n  #         cp -R build/macos/Build/Products/Release/pixes.app dist/dmg_contents/\n  #         ln -s /Applications dist/dmg_contents/Applications\n  #         hdiutil create -volname \"pixes\" -srcfolder dist/dmg_contents -ov -format UDZO \"dist/pixes.dmg\"\n  #\n  #     - name: Add version to filename\n  #       run: |\n  #         APP_VERSION=$(grep \"version:\" pubspec.yaml | cut -d':' -f2 | tr -d ' ')\n  #         mkdir -p result\n  #         mv dist/pixes.dmg result/pixes-$APP_VERSION.dmg\n  #\n  #     # Step 4: Attach and upload artifacts (optional)\n  #     - name: Upload DMG\n  #       uses: actions/upload-artifact@v4\n  #       with:\n  #         name: macos_build\n  #         path: result/\n  Build_IOS:\n    if: github.event_name == 'release' || github.event.inputs.build_ios == 'true'\n    runs-on: macos-26\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version-file: pubspec.yaml\n      - run: sudo xcode-select --switch /Applications/Xcode_26.4.0.app\n      - run: flutter pub get\n      - run: flutter build ios --release --no-codesign\n      - run: |\n          mkdir -p /Users/runner/work/pixes/pixes/build/ios/iphoneos/Payload\n          mv /Users/runner/work/pixes/pixes/build/ios/iphoneos/Runner.app /Users/runner/work/pixes/pixes/build/ios/iphoneos/Payload\n          cd /Users/runner/work/pixes/pixes/build/ios/iphoneos/\n          zip -r pixes-ios.ipa Payload\n      - uses: actions/upload-artifact@v4\n        with:\n          name: app-ios.ipa\n          path: /Users/runner/work/pixes/pixes/build/ios/iphoneos/pixes-ios.ipa\n  Build_Android:\n    if: github.event_name == 'release' || github.event.inputs.build_android == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version-file: pubspec.yaml\n          architecture: x64\n      - name: Decode and install certificate\n        env:\n          STORE_FILE: ${{ secrets.ANDROID_KEYSTORE }}\n          PROPERTY_FILE: ${{ secrets.ANDROID_KEY_PROPERTIES }}\n        run: |\n          echo \"$STORE_FILE\" | base64 --decode > android/keystore.jks\n          echo \"$PROPERTY_FILE\" > android/key.properties\n      - uses: actions/setup-java@v4\n        with:\n          distribution: 'oracle'\n          java-version: '17'\n      - run: flutter pub get\n      - run: flutter build apk --release\n      - uses: actions/upload-artifact@v4\n        with:\n          name: apks\n          path: build/app/outputs/apk/release\n  Build_Windows:\n    if: github.event_name == 'release' || github.event.inputs.build_windows == 'true'\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: install dependencies\n        run: |\n          choco install yq -y\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: \"stable\"\n          flutter-version-file: pubspec.yaml\n          architecture: x64\n      - name: Decode and install certificate\n        shell: bash\n        env:\n          STORE_FILE: ${{ secrets.WINDOWS_KEYSTORE }}\n        run: |\n          echo \"$STORE_FILE\" | base64 --decode > windows/app.pfx\n      - name: build\n        env:\n          CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_KEYSTORE_PASSWORD }}\n        run: |\n          flutter pub get\n          dart run msix:create -p ${{ env.CERTIFICATE_PASSWORD }} --install-certificate false\n      - uses: actions/upload-artifact@v4\n        with:\n          name: windows_build\n          path: build/windows/x64/runner/Release/pixes.msix\n  Build_Linux:\n    if: github.event_name == 'release' || github.event.inputs.build_linux == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: 'stable'\n          flutter-version-file: pubspec.yaml\n          architecture: x64\n      - run: |\n          sudo apt-get update -y\n          sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1\n          dart pub global activate flutter_to_debian\n      - run: python3 debian/build.py x64\n      - run: dart run flutter_to_arch\n      - run: |\n          sudo rm -rf build/linux/arch/app.tar.gz\n          sudo rm -rf build/linux/arch/pkg\n          sudo rm -rf build/linux/arch/src\n          sudo rm -rf build/linux/arch/PKGBUILD\n      - uses: actions/upload-artifact@v4\n        with:\n          name: deb_build\n          path: build/linux/x64/release/debian\n      - uses: actions/upload-artifact@v4\n        with:\n          name: arch_build\n          path: build/linux/arch/\n  Build_Linux_ARM64:\n    if: github.event_name == 'release' || github.event.inputs.build_linux_arm64 == 'true'\n    runs-on: ubuntu-22.04-arm\n    steps:\n      - uses: actions/checkout@v4\n      - uses: subosito/flutter-action@v2\n        with:\n          channel: 'master'\n          flutter-version-file: pubspec.yaml\n      - run: |\n          flutter pub get\n          sudo apt-get update -y\n          sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1\n          dart pub global activate flutter_to_debian\n      - run: python3 debian/build.py arm64\n      - uses: actions/upload-artifact@v4\n        with:\n          name: deb_arm64_build\n          path: build/linux/x64/release/debian # This is a bug related to flutter_to_debian, but it's not a big deal.\n  Release:\n    runs-on: ubuntu-latest\n    needs: [Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64]\n    if: github.event_name == 'release'\n    steps:\n      # macOS artifact: disabled together with Build_MacOS (was macos.zip / DMG from that job).\n      # - uses: actions/download-artifact@v4\n      #   with:\n      #     name: macos.zip\n      #     path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: app-ios.ipa\n          path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: apks\n          path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: windows_build\n          path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: deb_build\n          path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: arch_build\n          path: outputs\n      - uses: actions/download-artifact@v4\n        with:\n          name: deb_arm64_build\n          path: outputs\n      - name: Download public key\n        run: wget https://nyne.dev/files/windows_app.cer\n      - uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ github.ref_name }}\n          files: |\n            outputs/*.ipa\n            outputs/*.dmg\n            outputs/*.apk\n            outputs/*.zip\n            outputs/*.exe\n            outputs/*.deb\n            outputs/*.zst\n            outputs/*.msix\n            windows_app.cer\n          token: ${{ secrets.ACTION_GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.build/\n.buildlog/\n.history\n.svn/\n.swiftpm/\nmigrate_working_dir/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.pub-cache/\n.pub/\n/build/\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\n*.pfx"
  },
  {
    "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: \"54e66469a933b60ddf175f858f82eaeb97e48c8d\"\n  channel: \"stable\"\n\nproject_type: app\n\n# Tracks metadata for the flutter migrate command\nmigration:\n  platforms:\n    - platform: root\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: android\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: ios\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: linux\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: macos\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n    - platform: windows\n      create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n      base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d\n\n  # User provided section\n\n  # List of Local paths (relative to this file) that should be\n  # ignored by the migrate tool.\n  #\n  # Files that are not part of the templates will be ignored by default.\n  unmanaged_files:\n    - 'lib/main.dart'\n    - 'ios/Runner.xcodeproj/project.pbxproj'\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"appdata\",\n        \"Bungo\",\n        \"gjzr\",\n        \"microtask\",\n        \"mypixiv\",\n        \"pawoo\",\n        \"Rorigod\",\n        \"sleepinglife\",\n        \"Ugoira\",\n        \"vocaloidhm\",\n        \"vsync\"\n    ]\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 nyne\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# pixes\n\n[![flutter](https://img.shields.io/badge/flutter-3.32.5-blue)](https://flutter.dev/) \n[![License](https://img.shields.io/github/license/wgh136/pixes)](https://github.com/wgh136/pixes/blob/master/LICENSE)\n[![Download](https://img.shields.io/github/v/release/wgh136/pixes)](https://github.com/wgh136/pixes)\n[![stars](https://img.shields.io/github/stars/wgh136/pixes)](https://github.com/wgh136/pixes/stargazers)\n\nUnofficial Pixiv app, support Windows, Android, iOS, macOS, linux\n\nAll main features are implemented.\n\n## Download\n\nDownload from [Release](https://github.com/wgh136/pixes/releases)\n\n## Build from source\n\n### Install Flutter\n\nView [Flutter Document](https://flutter.dev/docs/get-started/install)\n\n### Build Android\n\nPut your keystore file (`key.jks`, `key.properties`) in `android/`\n\nRun `flutter build apk`\n\n### Build iOS/Windows/macOS\n\nRun `flutter build ios/windows/macos`\n\n### Build Linux\n\nUse`python3 debian/build.py {ARCH}` to build deb package. Replace {ARCH} with `x64` or `arm64`.\n\nFor other linux distributions, you can use `flutter build linux` to build. \nYou must register the `pixiv` scheme in the `.desktop` file, otherwise the login will not work.\n\n## Screenshots\n\n<img src=\"screenshots/1.png\" style=\"width: 400px\">\n<img src=\"screenshots/2.png\" style=\"width: 400px\">\n<img src=\"screenshots/3.png\" style=\"width: 400px\">\n<img src=\"screenshots/4.png\" style=\"width: 400px\">\n"
  },
  {
    "path": "analysis_options.yaml",
    "content": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n# The issues identified by the analyzer are surfaced in the UI of Dart-enabled\n# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be\n# invoked from the command line by running `flutter analyze`.\n\n# The following line activates a set of recommended lints for Flutter apps,\n# packages, and plugins designed to encourage good coding practices.\ninclude: package:flutter_lints/flutter.yaml\n\nlinter:\n  # The lint rules applied to this project can be customized in the\n  # section below to disable rules from the `package:flutter_lints/flutter.yaml`\n  # included above or to enable additional rules. A list of all available lints\n  # and their documentation is published at https://dart.dev/lints.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "android/.gitignore",
    "content": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remember to never publicly share your keystore.\n# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app\nkey.properties\n**/*.keystore\n**/*.jks\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "plugins {\n    id \"com.android.application\"\n    id \"kotlin-android\"\n    id \"dev.flutter.flutter-gradle-plugin\"\n}\n\next.abiCodes = [\"armeabi-v7a\": 1, \"arm64-v8a\": 2, \"x86_64\": 3]\n\ndef 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 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\ndef keystoreProperties = new Properties()\ndef keystorePropertiesFile = rootProject.file('key.properties')\nif (keystorePropertiesFile.exists()) {\n   keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n}\n\nandroid {\n    namespace \"com.github.wgh136.pixes\"\n    compileSdk flutter.compileSdkVersion\n    ndkVersion flutter.ndkVersion\n\n    packaging {\n        jniLibs {\n            useLegacyPackaging true\n        }\n    }\n\n    splits{\n        abi {\n            reset()\n            include 'armeabi-v7a', 'arm64-v8a', 'x86_64'\n            enable true\n            universalApk true\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    kotlinOptions{\n        jvmTarget = JavaVersion.VERSION_17\n    }\n\n    defaultConfig {\n        applicationId \"com.github.wgh136.pixes\"\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 flutter.minSdkVersion\n        targetSdk = flutter.targetSdkVersion\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    signingConfigs {\n        release {\n            keyAlias keystoreProperties['keyAlias']\n            keyPassword keystoreProperties['keyPassword']\n            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null\n            storePassword keystoreProperties['storePassword']\n        }\n        debug {\n            keyAlias keystoreProperties['keyAlias']\n            keyPassword keystoreProperties['keyPassword']\n            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null\n            storePassword keystoreProperties['storePassword']\n        }\n    }\n\n    buildTypes {\n        release {\n            ndk {\n                abiFilters \"armeabi-v7a\", \"arm64-v8a\", \"x86_64\"\n            }\n            signingConfig signingConfigs.release\n            applicationVariants.all { variant ->\n                variant.outputs.all { output ->\n                    def abi = output.getFilter(com.android.build.OutputFile.ABI)\n                    if (abi != null) {\n                        outputFileName = \"pixes-${variant.versionName}-${abi}.apk\"\n                        def abiVersionCode = project.ext.abiCodes.get(abi)\n                        if (abiVersionCode != null) {\n                            versionCodeOverride = variant.versionCode * 10 + abiVersionCode\n                        }\n                    } else {\n                        outputFileName = \"pixes-${variant.versionName}.apk\"\n                        versionCodeOverride = variant.versionCode * 10\n                    }\n                }\n            }\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\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.ACCESS_WIFI_STATE\" />\n    <application\n        android:label=\"pixes\"\n        android:name=\"${applicationName}\"\n        android:enableOnBackInvokedCallback=\"true\"\n        android:icon=\"@mipmap/ic_launcher\">\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/LaunchTheme\"\n            android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\"\n            android:hardwareAccelerated=\"true\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <!-- Specifies an Android theme to apply to this Activity as soon as\n                 the Android process has started. This theme is visible to the user\n                 while the Flutter UI initializes. After that, this theme continues\n                 to determine the Window background behind the Flutter UI. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.NormalTheme\"\n              android:resource=\"@style/NormalTheme\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n            <intent-filter android:label=\"login\">\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                <!-- Accepts URIs that begin with \"example://gizmos” -->\n                <data android:scheme=\"pixiv\"/>\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\" android:host=\"www.pixiv.net\" android:pathPrefix=\"/users\"/>\n                <data android:scheme=\"https\" android:host=\"www.pixiv.net\" android:pathPrefix=\"/novel\"/>\n                <data android:scheme=\"https\" android:host=\"www.pixiv.net\" android:pathPrefix=\"/tags\"/>\n                <data android:scheme=\"https\" android:host=\"www.pixiv.net\" android:pathPrefix=\"/artworks\"/>\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        <meta-data android:name=\"io.flutter.embedding.android.EnableImpeller\" android:value=\"false\"/>\n    </application>\n    <!-- Required to query activities that can process text, see:\n         https://developer.android.com/training/package-visibility?hl=en and\n         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.\n\n         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\"/>\n            <data android:mimeType=\"text/plain\"/>\n        </intent>\n    </queries>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/com/github/wgh136/pixes/MainActivity.kt",
    "content": "package com.github.wgh136.pixes\n\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.plugins.GeneratedPluginRegistrant\nimport io.flutter.plugin.common.MethodChannel\nimport io.flutter.embedding.engine.FlutterEngine\n\nclass MainActivity: FlutterActivity() {\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        GeneratedPluginRegistrant.registerWith(flutterEngine)\n        //获取http代理\n        MethodChannel(\n            flutterEngine.dartExecutor.binaryMessenger,\n            \"pixes/proxy\"\n        ).setMethodCallHandler { _, res ->\n            res.success(getProxy())\n        }\n    }\n\n    private fun getProxy(): String{\n        val host = System.getProperty(\"http.proxyHost\")\n        val port = System.getProperty(\"http.proxyPort\")\n        return if(host!=null&&port!=null){\n            \"$host:$port\"\n        }else{\n            \"No Proxy\"\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  <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</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>\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             the Flutter engine draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n\n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</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    <!-- The INTERNET permission is required for development. Specifically,\n         the Flutter tool needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "allprojects {\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": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.13-all.zip"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4G\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "pluginManagement {\n    def flutterSdkPath = {\n        def properties = new Properties()\n        file(\"local.properties\").withInputStream { properties.load(it) }\n        def flutterSdkPath = properties.getProperty(\"flutter.sdk\")\n        assert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\n        return flutterSdkPath\n    }\n    settings.ext.flutterSdkPath = flutterSdkPath()\n\n    includeBuild(\"${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle\")\n\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n\nplugins {\n    id \"dev.flutter.flutter-plugin-loader\" version \"1.0.0\"\n    id \"com.android.application\" version '8.12.3' apply false\n    id \"org.jetbrains.kotlin.android\" version \"2.1.0\" apply false\n}\n\ninclude \":app\"\n"
  },
  {
    "path": "assets/tr.json",
    "content": "{\n    \"zh_CN\": {\n        \"Search\": \"搜索\",\n        \"Downloading\": \"下载中\",\n        \"Downloaded\": \"已下载\",\n        \"Artwork\": \"插画\",\n        \"Explore\": \"探索\",\n        \"Bookmarks\": \"收藏\",\n        \"Following\": \"关注\",\n        \"History\": \"历史\",\n        \"Ranking\": \"排行\",\n        \"Settings\": \"设置\",\n        \"Artworks\": \"作品\",\n        \"Mangas\": \"漫画\",\n        \"Users\": \"用户\",\n        \"Search artwork\": \"搜索作品\",\n        \"Search Settings\": \"搜索设置\",\n        \"Match\": \"匹配\",\n        \"Favorite number\": \"收藏数\",\n        \"Sort\": \"排序\",\n        \"Age limit\": \"年龄限制\",\n        \"Search novel\": \"搜索小说\",\n        \"Search user\": \"搜索用户\",\n        \"Artwork ID\": \"作品ID\",\n        \"Artist ID\": \"画师ID\",\n        \"Novel ID\": \"小说ID\",\n        \"Search artworks\": \"搜索作品\",\n        \"Speed\": \"速度\",\n        \"View\": \"查看\",\n        \"Info\": \"信息\",\n        \"Delete\": \"删除\",\n        \"Are you sure you want to delete?\": \"确定要删除吗？\",\n        \"Yes\": \"是\",\n        \"Views\": \"浏览数\",\n        \"Favorites\": \"收藏数\",\n        \"Private\": \"私人\",\n        \"Share\": \"分享\",\n        \"Link\": \"链接\",\n        \"Unfollow\": \"取消关注\",\n        \"Favorite\": \"收藏\",\n        \"Comment\": \"评论\",\n        \"Comments\": \"评论\",\n        \"Follows: \": \"关注：\",\n        \"Information\": \"信息\",\n        \"Introduction\": \"简介\",\n        \"Birthday\": \"生日\",\n        \"Job\": \"职业\",\n        \"Gender\": \"性别\",\n        \"Social Network\": \"社交网络\",\n        \"Batch download\": \"批量下载\",\n        \"Maximum number of downloads\": \"最大下载数\",\n        \"Cancel\": \"取消\",\n        \"Continue\": \"继续\",\n        \"Public\": \"公开\",\n        \"All\": \"全部\",\n        \"Daily\": \"每日\",\n        \"Weekly\": \"每周\",\n        \"Monthly\": \"每月\",\n        \"For male\": \"男性向\",\n        \"For female\": \"女性向\",\n        \"Originals\": \"原创\",\n        \"Rookies\": \"新人\",\n        \"Daily Manga\": \"每日漫画\",\n        \"Weekly Manga\": \"每周漫画\",\n        \"Monthly Manga\": \"每月漫画\",\n        \"R18\": \"R18\",\n        \"Account\": \"账号\",\n        \"Logout\": \"登出\",\n        \"Account Settings\": \"账号设置\",\n        \"Edit\": \"编辑\",\n        \"Download\": \"下载\",\n        \"Manage\": \"管理\",\n        \"About\": \"关于\",\n        \"Are you sure you want to logout?\": \"确定要登出吗？\",\n        \"Download Path\": \"下载路径\",\n        \"Confirm\": \"确认\",\n        \"Download subpath\": \"下载子路径\",\n        \"Rule\": \"规则\",\n        \"Weights of the tags\": \"标签权重\",\n        \"Use translated tag name\": \"使用翻译后的标签名\",\n        \"Edit the rule for where to save an image.\": \"编辑保存图片的规则\",\n        \"Note: The rule should include the filename.\": \"注意：规则应包含文件名\",\n        \"Title of the work\": \"作品标题\",\n        \"Name of the author\": \"作者名\",\n        \"Index of the image in the artwork\": \"作品中的图片序号\",\n        \"File extension\": \"文件扩展名\",\n        \"Tags: Tags will be sorted by the \\\"Weights of tags\\\" setting and replaced by the following rule:\": \"标签：标签将按照“标签权重”设置排序，并按照以下规则替换：\",\n        \"The final text will be affected by the \\\"Use translated tag name\\\" setting.\": \"最终文本将受“使用翻译后的标签名”设置影响\",\n        \"The first tag of the artwork\": \"作品的第一个标签\",\n        \"The second tag of the artwork\": \"作品的第二个标签\",\n        \"Follow\": \"关注\",\n        \"Save to\": \"保存到\",\n        \"Filled with tags. The tags should be separated by a space. The tag in front has higher weight.\": \"填写标签, 标签应该用空格分隔, 前面的标签权重更高. \",\n        \"It is required to use the original name instead of the translated name.\": \"必须使用原始名称而不是翻译后的名称\",\n        \"Some keywords will be replaced by the following rule:\": \"一些关键词将按照以下规则替换:\",\n        \"Subpath\": \"子路径\",\n        \"Tags partial match\": \"标签部分匹配\",\n        \"Tags exact match\": \"标签完全匹配\",\n        \"Title or description search\": \"标题或描述搜索\",\n        \"Unlimited\": \"无限制\",\n        \"New to old\": \"新到旧\",\n        \"Old to new\": \"旧到新\",\n        \"Popular\": \"热门\",\n        \"Popular(limited)\": \"热门(受限)\",\n        \"Popular(Male)\": \"热门(男性向)\",\n        \"Popular(Female)\": \"热门(女性向)\",\n        \"Start Time\": \"开始时间\",\n        \"End Time\": \"结束时间\",\n        \"Max parallels\": \"最大并行数\",\n        \"Replace with 'AI' if the work was generated by AI, otherwise replace with blank\": \"替换为'AI'如果作品是由AI生成的, 否则替换为空白\",\n        \"Replace with * if the work have tag *, otherwise replace with blank.\": \"替换为*如果作品包含标签*, 否则替换为空白\",\n        \"Multiple path separators will be automatically replaced with a single\": \"多个路径分隔符将被自动替换为单个\",\n        \"Login\": \"登录\",\n        \"You need to complete the login operation in the browser window that will open.\": \"您需要在打开的浏览器窗口中完成登录操作\",\n        \"Waiting...\" : \"等待中...\",\n        \"Waiting for authentication. Please finished in the browser.\" : \"等待验证. 请在浏览器中完成.\",\n        \"Back\" : \"返回\",\n        \"Logging in\" : \"登录中\",\n        \"Browse\": \"浏览\",\n        \"Proxy\": \"代理\",\n        \"Appearance\": \"外观\",\n        \"Language\": \"语言\",\n        \"Theme\": \"主题\",\n        \"Pause\": \"暂停\",\n        \"Resume\": \"继续\",\n        \"Paused\": \"已暂停\",\n        \"Delete all\": \"删除全部\",\n        \"Related\": \"相关\",\n        \"Related artworks\": \"相关作品\",\n        \"Related users\": \"相关用户\",\n        \"Replace with '-p${index}' if the work have more than one images, otherwise replace with blank.\": \"替换为'-p${index}'如果作品有多张图片, 否则替换为空白\",\n        \"Recommendation\": \"推荐\",\n        \"Novel\": \"小说\",\n        \"Novels\": \"小说\",\n        \"Reading Settings\": \"阅读设置\",\n        \"Font Size\": \"字体大小\",\n        \"Line Height\": \"行高\",\n        \"Paragraph Spacing\": \"段间距\",\n        \"light\": \"浅色\",\n        \"dark\": \"深色\",\n        \"block\": \"屏蔽\",\n        \"Block\": \"屏蔽\",\n        \"Block(Account)\": \"屏蔽(账号)\",\n        \"Block(Local)\": \"屏蔽(本地)\",\n        \"Add\": \"添加\",\n        \"Submit\": \"提交\",\n        \"Local\": \"本地\",\n        \"Both\": \"同时\",\n        \"This artwork is blocked\": \"此作品已被屏蔽\",\n        \"Delete Invalid Items\": \"删除无效项目\",\n        \"Private Favorite\": \"私人收藏\",\n        \"Shortcuts\": \"快捷键\",\n        \"Page down\": \"向下翻页\",\n        \"Page up\": \"向上翻页\",\n        \"Next work\": \"下一作品\",\n        \"Previous work\": \"上一作品\",\n        \"Add to favorites\": \"添加收藏\",\n        \"Follow the artist\": \"关注画师\",\n        \"Manga\": \"漫画\",\n        \"Actions\": \"操作\",\n        \"Current quantity\": \"当前数量\",\n        \"Display the original image on the details page\": \"在详情页显示原图\",\n        \"Open link\": \"打开链接\",\n        \"Read\": \"阅读\",\n        \"Error\": \"错误\",\n        \"Failed to register URL scheme.\": \"注册URL协议失败\",\n        \"Retry\": \"重试\",\n        \"Network\": \"网络\",\n        \"Save to gallery\": \"保存到相册\",\n        \"Choose a way to login\": \"选择登录方式\",\n        \"Use Webview: you cannot sign in with Google.\": \"使用Webview: 无法使用Google登录\",\n        \"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application\": \"使用外部浏览器: 可以使用Google登录. 但是, 一些浏览器可能与应用程序不兼容\",\n        \"External browser\": \"外部浏览器\",\n        \"Show comments\": \"显示评论\",\n        \"Show original image\": \"显示原图\",\n        \"Illustrations\": \"插画\",\n        \"New version available\": \"新版本可用\",\n        \"A new version of Pixes is available. Do you want to update now?\" : \"Pixes有新版本可用. 您要立即更新吗?\",\n        \"Update\": \"更新\",\n        \"Check for updates\": \"检查更新\",\n        \"Check for updates on startup\": \"启动时检查更新\",\n        \"I understand pixes is a free unofficial application.\": \"我了解Pixes是一个免费的非官方应用程序\",\n        \"Related Artworks\": \"相关作品\",\n        \"Emphasize artworks from following artists\": \"强调关注画师的作品\",\n        \"The border of the artworks will be darker\": \"作品的边框将被加深\",\n        \"Initial Page\": \"初始页面\",\n        \"Close the pane to apply the settings\": \"关闭面板以应用设置\",\n        \"No results found\": \"未找到结果\"\n    },\n    \"zh_TW\": {\n        \"Search\": \"搜索\",\n        \"Downloading\": \"下載中\",\n        \"Downloaded\": \"已下載\",\n        \"Artwork\": \"作品\",\n        \"Explore\": \"探索\",\n        \"Bookmarks\": \"收藏\",\n        \"Following\": \"關注\",\n        \"History\": \"歷史\",\n        \"Ranking\": \"排行\",\n        \"Settings\": \"設置\",\n        \"Artworks\": \"作品\",\n        \"Mangas\": \"漫畫\",\n        \"Users\": \"用戶\",\n        \"Search artwork\": \"搜索作品\",\n        \"Search Settings\": \"搜索設置\",\n        \"Match\": \"匹配\",\n        \"Favorite number\": \"收藏數\",\n        \"Sort\": \"排序\",\n        \"Age limit\": \"年齡限制\",\n        \"Search novel\": \"搜索小說\",\n        \"Search user\": \"搜索用戶\",\n        \"Artwork ID\": \"作品ID\",\n        \"Artist ID\": \"畫師ID\",\n        \"Novel ID\": \"小說ID\",\n        \"Search artworks\": \"搜索作品\",\n        \"Speed\": \"速度\",\n        \"View\": \"查看\",\n        \"Info\": \"信息\",\n        \"Delete\": \"刪除\",\n        \"Are you sure you want to delete?\": \"確定要刪除嗎？\",\n        \"Yes\": \"是\",\n        \"Views\": \"瀏覽數\",\n        \"Favorites\": \"收藏數\",\n        \"Private\": \"私人\",\n        \"Share\": \"分享\",\n        \"Link\": \"鏈接\",\n        \"Unfollow\": \"取消關注\",\n        \"Favorite\": \"收藏\",\n        \"Comment\": \"評論\",\n        \"Comments\": \"評論\",\n        \"Follows: \": \"關注：\",\n        \"Information\": \"信息\",\n        \"Introduction\": \"簡介\",\n        \"Birthday\": \"生日\",\n        \"Job\": \"職業\",\n        \"Gender\": \"性別\",\n        \"Social Network\": \"社交網絡\",\n        \"Batch download\": \"批量下載\",\n        \"Maximum number of downloads\": \"最大下載數\",\n        \"Cancel\": \"取消\",\n        \"Continue\": \"繼續\",\n        \"Public\": \"公開\",\n        \"All\": \"全部\",\n        \"Daily\": \"每日\",\n        \"Weekly\": \"每周\",\n        \"Monthly\": \"每月\",\n        \"For male\": \"男性向\",\n        \"For female\": \"女性向\",\n        \"Originals\": \"原創\",\n        \"Rookies\": \"新人\",\n        \"Daily Manga\": \"每日漫畫\",\n        \"Weekly Manga\": \"每周漫畫\",\n        \"Monthly Manga\": \"每月漫畫\",\n        \"R18\": \"R18\",\n        \"Account\": \"賬戶\",\n        \"Logout\": \"登出\",\n        \"Account Settings\": \"賬戶設置\",\n        \"Edit\": \"編輯\",\n        \"Download\": \"下載\",\n        \"Manage\": \"管理\",\n        \"About\": \"關於\",\n        \"Are you sure you want to logout?\": \"確定要登出嗎？\",\n        \"Download Path\": \"下載路徑\",\n        \"Confirm\": \"確認\",\n        \"Download subpath\": \"下載子路徑\",\n        \"Rule\": \"規則\",\n        \"Weights of the tags\": \"標籤權重\",\n        \"Use translated tag name\": \"使用翻譯後的標籤名\",\n        \"Edit the rule for where to save an image.\": \"編輯保存圖片的規則\",\n        \"Note: The rule should include the filename.\": \"注意：規則應包含文件名\",\n        \"Title of the work\": \"作品標題\",\n        \"Name of the author\": \"作者名\",\n        \"Index of the image in the artwork\": \"作品中的圖片序號\",\n        \"File extension\": \"文件擴展名\",\n        \"Tags: Tags will be sorted by the \\\"Weights of tags\\\" setting and replaced by the following rule:\": \"標籤：標籤將按照“標籤權重”設置排序，並按照以下規則替換：\",\n        \"The final text will be affected by the \\\"Use translated tag name\\\" setting.\": \"最終文本將受“使用翻譯後的標籤名”設置影響\",\n        \"The first tag of the artwork\": \"作品的第一個標籤\",\n        \"The second tag of the artwork\": \"作品的第二個標籤\",\n        \"Follow\": \"關注\",\n        \"Save to\": \"保存到\",\n        \"Filled with tags. The tags should be separated by a space. The tag in front has higher weight.\": \"填寫標籤, 標籤應該用空格分隔, 前面的標籤權重更高. \",\n        \"It is required to use the original name instead of the translated name.\": \"必須使用原始名稱而不是翻譯後的名稱\",\n        \"Some keywords will be replaced by the following rule:\": \"一些關鍵詞將按照以下規則替換:\",\n        \"Subpath\": \"子路徑\",\n        \"Tags partial match\": \"標籤部分匹配\",\n        \"Tags exact match\": \"標籤完全匹配\",\n        \"Title or description search\": \"標題或描述搜索\",\n        \"Unlimited\": \"無限制\",\n        \"New to old\": \"新到舊\",\n        \"Old to new\": \"舊到新\",\n        \"Popular\": \"熱門\",\n        \"Popular(limited)\": \"熱門(受限)\",\n        \"Popular(Male)\": \"熱門(男性)\",\n        \"Popular(Female)\": \"熱門(女性)\",\n        \"Start Time\": \"開始時間\",\n        \"End Time\": \"結束時間\",\n        \"Max parallels\": \"最大並行數\",\n        \"Replace with 'AI' if the work was generated by AI, otherwise replace with blank\": \"替換為'AI'如果作品是由AI生成的, 否則替換為空白\",\n        \"Replace with * if the work have tag *, otherwise replace with blank.\": \"替換為*如果作品包含標籤*, 否則替換為空白\",\n        \"Multiple path separators will be automatically replaced with a single\": \"多個路徑分隔符號將自動替換為單一\",\n        \"Login\": \"登錄\",\n        \"You need to complete the login operation in the browser window that will open.\": \"您需要在打開的瀏覽器窗口中完成登錄操作\",\n        \"Waiting...\" : \"等待中...\",\n        \"Waiting for authentication. Please finished in the browser.\" : \"等待驗證. 請在瀏覽器中完成.\",\n        \"Back\" : \"返回\",\n        \"Logging in\" : \"登錄中\",\n        \"Browse\": \"瀏覽\",\n        \"Proxy\": \"代理\",\n        \"Appearance\": \"外觀\",\n        \"Language\": \"語言\",\n        \"Theme\": \"主題\",\n        \"Pause\": \"暫停\",\n        \"Resume\": \"繼續\",\n        \"Paused\": \"已暫停\",\n        \"Delete all\": \"刪除全部\",\n        \"Related\": \"相關\",\n        \"Related artworks\": \"相關作品\",\n        \"Related users\": \"相關用戶\",\n        \"Replace with '-p${index}' if the work have more than one images, otherwise replace with blank.\": \"替換為'-p${index}'如果作品有多張圖片, 否則替換為空白\",\n        \"Recommendation\": \"推薦\",\n        \"Novel\": \"小說\",\n        \"Novels\": \"小說\",\n        \"Reading Settings\": \"閱讀設置\",\n        \"Font Size\": \"字體大小\",\n        \"Line Height\": \"行高\",\n        \"Paragraph Spacing\": \"段間距\",\n        \"light\": \"淺色\",\n        \"dark\": \"深色\",\n        \"block\": \"屏蔽\",\n        \"Block\": \"屏蔽\",\n        \"Block(Account)\": \"屏蔽(賬戶)\",\n        \"Block(Local)\": \"屏蔽(本地)\",\n        \"Add\": \"添加\",\n        \"Submit\": \"提交\",\n        \"Local\": \"本地\",\n        \"Both\": \"同時\",\n        \"This artwork is blocked\": \"此作品已被屏蔽\",\n        \"Delete Invalid Items\": \"刪除無效項目\",\n        \"Private Favorite\": \"私人收藏\",\n        \"Shortcuts\": \"快捷鍵\",\n        \"Page down\": \"向下翻頁\",\n        \"Page up\": \"向上翻頁\",\n        \"Next work\": \"下一作品\",\n        \"Previous work\": \"上一作品\",\n        \"Add to favorites\": \"添加收藏\",\n        \"Follow the artist\": \"關注畫師\",\n        \"Manga\": \"漫畫\",\n        \"Actions\": \"操作\",\n        \"Current quantity\": \"當前數量\",\n        \"Display the original image on the details page\": \"在詳情頁顯示原圖\",\n        \"Open link\": \"打開鏈接\",\n        \"Read\": \"閱讀\",\n        \"Error\": \"錯誤\",\n        \"Failed to register URL scheme.\": \"註冊URL協議失敗\",\n        \"Retry\": \"重試\",\n        \"Network\": \"網絡\",\n        \"Save to gallery\": \"保存到相冊\",\n        \"Choose a way to login\": \"選擇登錄方式\",\n        \"Use Webview: you cannot sign in with Google.\": \"使用Webview: 無法使用Google登錄\",\n        \"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application\": \"使用外部瀏覽器: 可以使用Google登錄. 但是, 一些瀏覽器可能與應用程序不兼容\",\n        \"External browser\": \"外部瀏覽器\",\n        \"Show comments\": \"顯示評論\",\n        \"Show original image\": \"顯示原圖\",\n        \"Illustrations\": \"插畫\",\n        \"New version available\": \"新版本可用\",\n        \"A new version of Pixes is available. Do you want to update now?\" : \"Pixes有新版本可用. 您要立即更新嗎?\",\n        \"Update\": \"更新\",\n        \"Check for updates\": \"檢查更新\",\n        \"Check for updates on startup\": \"啟動時檢查更新\",\n        \"I understand pixes is a free unofficial application.\": \"我了解Pixes是一個免費的非官方應用程序\",\n        \"Related Artworks\": \"相關作品\",\n        \"Emphasize artworks from following artists\": \"強調關注畫師的作品\",\n        \"The border of the artworks will be darker\": \"作品的邊框將被加深\",\n        \"Initial Page\": \"初始頁面\",\n        \"Close the pane to apply the settings\": \"關閉面板以應用設置\",\n        \"No results found\": \"未找到結果\"\n    },\n    \"ko_KR\": {\n        \"Search\": \"검색\",\n        \"Downloading\": \"다운로드 중\",\n        \"Downloaded\": \"다운로드 완료\",\n        \"Artwork\": \"작품\",\n        \"Explore\": \"탐색\",\n        \"Bookmarks\": \"북마크\",\n        \"Following\": \"팔로잉\",\n        \"History\": \"기록\",\n        \"Ranking\": \"랭킹\",\n        \"Settings\": \"설정\",\n        \"Artworks\": \"작품\",\n        \"Mangas\": \"만화\",\n        \"Users\": \"사용자\",\n        \"Search artwork\": \"작품 검색\",\n        \"Search Settings\": \"검색 설정\",\n        \"Match\": \"일치\",\n        \"Favorite number\": \"북마크 수\",\n        \"Sort\": \"정렬\",\n        \"Age limit\": \"연령 제한\",\n        \"Search novel\": \"소설 검색\",\n        \"Search user\": \"사용자 검색\",\n        \"Artwork ID\": \"작품 ID\",\n        \"Artist ID\": \"작가 ID\",\n        \"Novel ID\": \"소설 ID\",\n        \"Search artworks\": \"작품 검색\",\n        \"Speed\": \"속도\",\n        \"View\": \"보기\",\n        \"Info\": \"정보\",\n        \"Delete\": \"삭제\",\n        \"Are you sure you want to delete?\": \"정말 삭제하시겠습니까?\",\n        \"Yes\": \"예\",\n        \"Views\": \"조회수\",\n        \"Favorites\": \"북마크 수\",\n        \"Private\": \"비공개\",\n        \"Share\": \"공유\",\n        \"Link\": \"링크\",\n        \"Unfollow\": \"언팔로우\",\n        \"Favorite\": \"북마크\",\n        \"Comment\": \"댓글\",\n        \"Comments\": \"댓글\",\n        \"Follows: \": \"팔로우: \",\n        \"Information\": \"정보\",\n        \"Introduction\": \"소개\",\n        \"Birthday\": \"생일\",\n        \"Job\": \"직업\",\n        \"Gender\": \"성별\",\n        \"Social Network\": \"소셜 네트워크\",\n        \"Batch download\": \"일괄 다운로드\",\n        \"Maximum number of downloads\": \"최대 다운로드 수\",\n        \"Cancel\": \"취소\",\n        \"Continue\": \"계속\",\n        \"Public\": \"공개\",\n        \"All\": \"전체\",\n        \"Daily\": \"일간\",\n        \"Weekly\": \"주간\",\n        \"Monthly\": \"월간\",\n        \"For male\": \"남성향\",\n        \"For female\": \"여성향\",\n        \"Originals\": \"오리지널\",\n        \"Rookies\": \"루키\",\n        \"Daily Manga\": \"일간 만화\",\n        \"Weekly Manga\": \"주간 만화\",\n        \"Monthly Manga\": \"월간 만화\",\n        \"R18\": \"R18\",\n        \"Account\": \"계정\",\n        \"Logout\": \"로그아웃\",\n        \"Account Settings\": \"계정 설정\",\n        \"Edit\": \"편집\",\n        \"Download\": \"다운로드\",\n        \"Manage\": \"관리\",\n        \"About\": \"정보\",\n        \"Are you sure you want to logout?\": \"정말 로그아웃하시겠습니까?\",\n        \"Download Path\": \"다운로드 경로\",\n        \"Confirm\": \"확인\",\n        \"Download subpath\": \"다운로드 하위 경로\",\n        \"Rule\": \"규칙\",\n        \"Weights of the tags\": \"태그 가중치\",\n        \"Use translated tag name\": \"번역된 태그 이름 사용\",\n        \"Edit the rule for where to save an image.\": \"이미지를 저장할 위치 규칙을 편집합니다.\",\n        \"Note: The rule should include the filename.\": \"참고: 규칙에 파일 이름이 포함되어야 합니다.\",\n        \"Title of the work\": \"작품 제목\",\n        \"Name of the author\": \"작가 이름\",\n        \"Index of the image in the artwork\": \"작품 내 이미지 인덱스\",\n        \"File extension\": \"파일 확장자\",\n        \"Tags: Tags will be sorted by the \\\"Weights of tags\\\" setting and replaced by the following rule:\": \"태그: 태그는 \\\"태그 가중치\\\" 설정에 따라 정렬되며 다음 규칙으로 대체됩니다:\",\n        \"The final text will be affected by the \\\"Use translated tag name\\\" setting.\": \"최종 텍스트는 \\\"번역된 태그 이름 사용\\\" 설정의 영향을 받습니다.\",\n        \"The first tag of the artwork\": \"작품의 첫 번째 태그\",\n        \"The second tag of the artwork\": \"작품의 두 번째 태그\",\n        \"Follow\": \"팔로우\",\n        \"Save to\": \"저장 위치\",\n        \"Filled with tags. The tags should be separated by a space. The tag in front has higher weight.\": \"태그를 입력하세요. 태그는 공백으로 구분해야 하며, 앞의 태그가 가중치가 더 높습니다.\",\n        \"It is required to use the original name instead of the translated name.\": \"번역된 이름 대신 원본 이름을 사용해야 합니다.\",\n        \"Some keywords will be replaced by the following rule:\": \"일부 키워드는 다음 규칙으로 대체됩니다:\",\n        \"Subpath\": \"하위 경로\",\n        \"Tags partial match\": \"태그 부분 일치\",\n        \"Tags exact match\": \"태그 완전 일치\",\n        \"Title or description search\": \"제목 또는 설명 검색\",\n        \"Unlimited\": \"무제한\",\n        \"New to old\": \"최신순\",\n        \"Old to new\": \"오래된 순\",\n        \"Popular\": \"인기\",\n        \"Popular(limited)\": \"인기(제한됨)\",\n        \"Popular(Male)\": \"인기(남성향)\",\n        \"Popular(Female)\": \"인기(여성향)\",\n        \"Start Time\": \"시작 시간\",\n        \"End Time\": \"종료 시간\",\n        \"Max parallels\": \"최대 동시 작업 수\",\n        \"Replace with 'AI' if the work was generated by AI, otherwise replace with blank\": \"AI가 생성한 작품인 경우 'AI'로 대체하고, 그렇지 않으면 공백으로 둡니다.\",\n        \"Replace with * if the work have tag *, otherwise replace with blank.\": \"작품에 * 태그가 있는 경우 *로 대체하고, 그렇지 않으면 공백으로 둡니다.\",\n        \"Multiple path separators will be automatically replaced with a single\": \"다중 경로 구분 기호는 자동으로 하나로 대체됩니다\",\n        \"Login\": \"로그인\",\n        \"You need to complete the login operation in the browser window that will open.\": \"열리는 브라우저 창에서 로그인 작업을 완료해야 합니다.\",\n        \"Waiting...\" : \"대기 중...\",\n        \"Waiting for authentication. Please finished in the browser.\" : \"인증 대기 중입니다. 브라우저에서 완료해 주세요.\",\n        \"Back\" : \"뒤로\",\n        \"Logging in\" : \"로그인 중\",\n        \"Browse\": \"둘러보기\",\n        \"Proxy\": \"프록시\",\n        \"Appearance\": \"모양\",\n        \"Language\": \"언어\",\n        \"Theme\": \"테마\",\n        \"Pause\": \"일시 정지\",\n        \"Resume\": \"재개\",\n        \"Paused\": \"일시 정지됨\",\n        \"Delete all\": \"모두 삭제\",\n        \"Related\": \"관련\",\n        \"Related artworks\": \"관련 작품\",\n        \"Related users\": \"관련 사용자\",\n        \"Replace with '-p${index}' if the work have more than one images, otherwise replace with blank.\": \"작품에 이미지가 한 장 이상인 경우 '-p${index}'로 대체하고, 그렇지 않으면 공백으로 둡니다.\",\n        \"Recommendation\": \"추천\",\n        \"Novel\": \"소설\",\n        \"Novels\": \"소설\",\n        \"Reading Settings\": \"읽기 설정\",\n        \"Font Size\": \"글꼴 크기\",\n        \"Line Height\": \"줄 간격\",\n        \"Paragraph Spacing\": \"문단 간격\",\n        \"light\": \"라이트\",\n        \"dark\": \"다크\",\n        \"block\": \"차단\",\n        \"Block\": \"차단\",\n        \"Block(Account)\": \"차단(계정)\",\n        \"Block(Local)\": \"차단(로컬)\",\n        \"Add\": \"추가\",\n        \"Submit\": \"제출\",\n        \"Local\": \"로컬\",\n        \"Both\": \"모두\",\n        \"This artwork is blocked\": \"이 작품은 차단되었습니다\",\n        \"Delete Invalid Items\": \"잘못된 항목 삭제\",\n        \"Private Favorite\": \"비공개 북마크\",\n        \"Shortcuts\": \"단축키\",\n        \"Page down\": \"페이지 다운\",\n        \"Page up\": \"페이지 업\",\n        \"Next work\": \"다음 작품\",\n        \"Previous work\": \"이전 작품\",\n        \"Add to favorites\": \"북마크에 추가\",\n        \"Follow the artist\": \"작가 팔로우\",\n        \"Manga\": \"만화\",\n        \"Actions\": \"작업\",\n        \"Current quantity\": \"현재 수량\",\n        \"Display the original image on the details page\": \"상세 페이지에 원본 이미지 표시\",\n        \"Open link\": \"링크 열기\",\n        \"Read\": \"읽기\",\n        \"Error\": \"오류\",\n        \"Failed to register URL scheme.\": \"URL 스킴 등록에 실패했습니다.\",\n        \"Retry\": \"다시 시도\",\n        \"Network\": \"네트워크\",\n        \"Save to gallery\": \"갤러리에 저장\",\n        \"Choose a way to login\": \"로그인 방식을 선택하세요\",\n        \"Use Webview: you cannot sign in with Google.\": \"Webview 사용: Google로 로그인할 수 없습니다.\",\n        \"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application\": \"외부 브라우저 사용: Google 계정으로 로그인할 수 있습니다. 단, 일부 브라우저는 어플리케이션과 호환되지 않을 수 있습니다.\",\n        \"External browser\": \"외부 브라우저\",\n        \"Show comments\": \"댓글 표시\",\n        \"Show original image\": \"원본 이미지 표시\",\n        \"Illustrations\": \"일러스트\",\n        \"New version available\": \"새 버전 사용 가능\",\n        \"A new version of Pixes is available. Do you want to update now?\" : \"Pixes의 새 버전이 출시되었습니다. 지금 업데이트하시겠습니까?\",\n        \"Update\": \"업데이트\",\n        \"Check for updates\": \"업데이트 확인\",\n        \"Check for updates on startup\": \"시작 시 업데이트 확인\",\n        \"I understand pixes is a free unofficial application.\": \"Pixes가 무료 비공식 앱임을 이해합니다.\",\n        \"Related Artworks\": \"관련 작품\",\n        \"Emphasize artworks from following artists\": \"팔로우한 작가의 작품 강조\",\n        \"The border of the artworks will be darker\": \"해당 작품들의 테두리가 더 어두워집니다.\",\n        \"Initial Page\": \"시작 페이지\",\n        \"Close the pane to apply the settings\": \"설정을 적용하려면 창을 닫으세요\",\n        \"No results found\": \"결과를 찾을 수 없음\"\n    }\n}\n"
  },
  {
    "path": "debian/build.py",
    "content": "import subprocess\nimport sys\n\narch = sys.argv[1]\ndebianContent = ''\ndesktopContent = ''\nversion = ''\n\nwith open('debian/debian.yaml', 'r') as f:\n    debianContent = f.read()\nwith open('debian/gui/pixes.desktop', 'r') as f:\n    desktopContent = f.read()\nwith open('pubspec.yaml', 'r') as f:\n    version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]\n\nwith open('debian/debian.yaml', 'w') as f:\n    content = debianContent.replace('{{Version}}', version)\n    if arch == 'x64':\n        content = content.replace('{{Arch}}', 'x64')\n        content = content.replace('{{Architecture}}', 'amd64')\n    elif arch == 'arm64':\n        content = content.replace('{{Arch}}', 'arm64')\n        content = content.replace('{{Architecture}}', 'arm64')\n    f.write(content)\nwith open('debian/gui/pixes.desktop', 'w') as f:\n    f.write(desktopContent.replace('{{Version}}', version))\n\nsubprocess.run([\"flutter\", \"build\", \"linux\"])\n\nsubprocess.run([\"$HOME/.pub-cache/bin/flutter_to_debian\"], shell=True)\n\nwith open('debian/debian.yaml', 'w') as f:\n    f.write(debianContent)\nwith open('debian/gui/pixes.desktop', 'w') as f:\n    f.write(desktopContent)\n"
  },
  {
    "path": "debian/debian.yaml",
    "content": "flutter_app: \n  command: pixes\n  arch: {{Arch}}\n  parent: /usr/local/lib\n  nonInteractive: true\n  execFieldCodes: u\n\ncontrol:\n  Package: pixes\n  Version: {{Version}}\n  Architecture: {{Architecture}}\n  Priority: optional\n  Depends: libwebkit2gtk-4.1-0, libgtk-3-0\n  Maintainer: nyne\n  Description: Unofficial pixiv application\n  \n#options:\n#  exec_out_dir: debian/packages\n"
  },
  {
    "path": "debian/gui/pixes.desktop",
    "content": "[Desktop Entry]\nName=Pixes\nGenericName=Pixes\nComment=Unofficial pixiv application\nTerminal=false\nType=Application\nCategories=Utility\nKeywords=Flutter;share;images;\nMimeType=x-scheme-handler/pixiv;\nIcon=pixes"
  },
  {
    "path": "ios/.gitignore",
    "content": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/DerivedData/\nIcon?\n**/Pods/\n**/.symlinks/\nprofile\nxcuserdata\n**/.generated/\nFlutter/App.framework\nFlutter/Flutter.framework\nFlutter/Flutter.podspec\nFlutter/Generated.xcconfig\nFlutter/ephemeral/\nFlutter/app.flx\nFlutter/app.zip\nFlutter/flutter_assets/\nFlutter/flutter_export_environment.sh\nServiceDefinitions.json\nRunner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!default.mode1v3\n!default.mode2v3\n!default.pbxuser\n!default.perspectivev3\n"
  },
  {
    "path": "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, '13.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\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\n  target 'RunnerTests' do\n    inherit! :search_paths\n  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    let controller: FlutterViewController = window?.rootViewController as! FlutterViewController\n    let methodChannel = FlutterMethodChannel(name: \"pixes/proxy\", binaryMessenger: controller.binaryMessenger)\n    methodChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in\n        if let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as NSDictionary?,\n           let dict = proxySettings.object(forKey: kCFNetworkProxiesHTTPProxy) as? NSDictionary,\n           let host = dict.object(forKey: kCFNetworkProxiesHTTPProxy) as? String,\n           let port = dict.object(forKey: kCFNetworkProxiesHTTPPort) as? Int {\n            let proxyConfig = \"\\(host):\\(port)\"\n            result(proxyConfig)\n        } else {\n            result(\"no proxy\")\n        }\n    }\n\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"filename\": \"AppIcon@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"1x\",\n      \"size\": \"76x76\"\n    },\n    {\n      \"filename\": \"AppIcon@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"76x76\"\n    },\n    {\n      \"filename\": \"AppIcon-83.5@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"83.5x83.5\"\n    },\n    {\n      \"filename\": \"AppIcon-40@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-40@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-40~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"1x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-40@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"40x40\"\n    },\n    {\n      \"filename\": \"AppIcon-20@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-20@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-20~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"1x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-20@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"20x20\"\n    },\n    {\n      \"filename\": \"AppIcon-29.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"1x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29@2x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"2x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29@3x.png\",\n      \"idiom\": \"iphone\",\n      \"scale\": \"3x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"1x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-29@2x~ipad.png\",\n      \"idiom\": \"ipad\",\n      \"scale\": \"2x\",\n      \"size\": \"29x29\"\n    },\n    {\n      \"filename\": \"AppIcon-60@2x~car.png\",\n      \"idiom\": \"car\",\n      \"scale\": \"2x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon-60@3x~car.png\",\n      \"idiom\": \"car\",\n      \"scale\": \"3x\",\n      \"size\": \"60x60\"\n    },\n    {\n      \"filename\": \"AppIcon~ios-marketing.png\",\n      \"idiom\": \"ios-marketing\",\n      \"scale\": \"1x\",\n      \"size\": \"1024x1024\"\n    }\n  ],\n  \"info\": {\n    \"author\": \"iconkitchen\",\n    \"version\": 1\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>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Pixes</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>pixes</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSupportsIndirectInputEvents</key>\n\t<true/>\n\t<key>CFBundleURLTypes</key>\n    <array>\n        <dict>\n            <key>CFBundleURLSchemes</key>\n            <array>\n                <string>pixiv</string>\n            </array>\n            <key>CFBundleURLName</key>\n            <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n        </dict>\n    </array>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string>photo</string>\n\t<key>NSPhotoLibraryUsageDescription</key>\n\t<string>photo</string>\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\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 97C146E61CF9000F007C117D /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 97C146ED1CF9000F007C117D;\n\t\t\tremoteInfo = Runner;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t331C8082294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C807B294A618700263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t331C8082294A63A400263BE5 /* RunnerTests */,\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\t331C8081294A63A400263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t331C8080294A63A400263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C807D294A63A400263BE5 /* Sources */,\n\t\t\t\t331C807F294A63A400263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C8080294A63A400263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 97C146ED1CF9000F007C117D;\n\t\t\t\t\t};\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\t331C8080294A63A400263BE5 /* RunnerTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C807F294A63A400263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\",\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\talwaysOutOfDate = 1;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t331C807D294A63A400263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\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 PBXTargetDependency section */\n\t\t331C8086294A63A400263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 97C146ED1CF9000F007C117D /* Runner */;\n\t\t\ttargetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency 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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t249021D4217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t331C8088294A63A400263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C8089294A63A400263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C808A294A63A400263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner\";\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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\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\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\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\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 12.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSUPPORTED_PLATFORMS = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t97C147061CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147071CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = \"$(FLUTTER_BUILD_NUMBER)\";\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C8088294A63A400263BE5 /* Debug */,\n\t\t\t\t331C8089294A63A400263BE5 /* Release */,\n\t\t\t\t331C808A294A63A400263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\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 = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C8080294A63A400263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "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": "ios/RunnerTests/RunnerTests.swift",
    "content": "import Flutter\nimport UIKit\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "lib/appdata.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:path_provider/path_provider.dart';\nimport 'package:pixes/utils/io.dart';\n\nimport 'foundation/app.dart';\nimport 'foundation/log.dart';\nimport 'network/models.dart';\n\nclass _Appdata {\n  static const MethodChannel _macosDownloadPathChannel =\n      MethodChannel(\"pixes/macos/download_path\");\n\n  Account? account;\n\n  var searchOptions = SearchOptions();\n\n  Map<String, dynamic> settings = {\n    \"downloadPath\": null,\n    \"downloadSubPath\": r\"/${id}-p${index}.${ext}\",\n    \"maxParallels\": 3,\n    \"proxy\": \"\",\n    \"darkMode\": \"System\",\n    \"language\": \"System\",\n    \"readingFontSize\": 16.0,\n    \"readingLineHeight\": 1.5,\n    \"readingParagraphSpacing\": 8.0,\n    \"blockTags\": [],\n    \"shortcuts\": <int>[\n      LogicalKeyboardKey.arrowDown.keyId,\n      LogicalKeyboardKey.arrowUp.keyId,\n      LogicalKeyboardKey.arrowRight.keyId,\n      LogicalKeyboardKey.arrowLeft.keyId,\n      LogicalKeyboardKey.enter.keyId,\n      LogicalKeyboardKey.keyD.keyId,\n      LogicalKeyboardKey.keyF.keyId,\n      LogicalKeyboardKey.keyC.keyId,\n      LogicalKeyboardKey.keyG.keyId,\n    ],\n    \"showOriginalImage\": false,\n    \"checkUpdate\": true,\n    \"emphasizeArtworksFromFollowingArtists\": true,\n    \"initialPage\": 4,\n  };\n\n  bool lock = false;\n\n  void writeData() async {\n    while (lock) {\n      await Future.delayed(const Duration(milliseconds: 20));\n    }\n    lock = true;\n    await File(\"${App.dataPath}/account.json\")\n        .writeAsString(jsonEncode(account));\n    await File(\"${App.dataPath}/settings.json\")\n        .writeAsString(jsonEncode(settings));\n    lock = false;\n  }\n\n  void writeSettings() async {\n    while (lock) {\n      await Future.delayed(const Duration(milliseconds: 20));\n    }\n    lock = true;\n    await File(\"${App.dataPath}/settings.json\")\n        .writeAsString(jsonEncode(settings));\n    lock = false;\n  }\n\n  Future<void> readData() async {\n    final file = File(\"${App.dataPath}/account.json\");\n    if (file.existsSync()) {\n      var json = jsonDecode(await file.readAsString());\n      if (json != null) {\n        account = Account.fromJson(json);\n      }\n    }\n    final settingsFile = File(\"${App.dataPath}/settings.json\");\n    if (settingsFile.existsSync()) {\n      var json = jsonDecode(await settingsFile.readAsString());\n      for (var key in json.keys) {\n        if (json[key] != null) {\n          if (json[key] is List && settings[key] is List) {\n            for (int i = 0;\n                i < json[key].length && i < settings[key].length;\n                i++) {\n              settings[key][i] = json[key][i];\n            }\n          } else {\n            settings[key] = json[key];\n          }\n        }\n      }\n    }\n    settings[\"downloadPath\"] ??= await _defaultDownloadPath;\n    if (App.isMacOS) {\n      await _ensureMacOSDownloadPathPermission();\n    }\n  }\n\n  Future<void> _ensureMacOSDownloadPathPermission() async {\n    final defaultPath = await _defaultDownloadPath;\n    final currentPath = settings[\"downloadPath\"] as String? ?? defaultPath;\n    if (_normalizePath(currentPath) == _normalizePath(defaultPath)) {\n      settings[\"downloadPath\"] = defaultPath;\n      return;\n    }\n\n    final restoredPath = await _restoreMacOSDownloadPathAccess(currentPath);\n    if (restoredPath != null) {\n      settings[\"downloadPath\"] = restoredPath;\n      Log.info(\n          \"DownloadPath\", \"Restored macOS directory access: $restoredPath\");\n      return;\n    }\n\n    Log.warning(\n      \"DownloadPath\",\n      \"Failed to restore macOS directory access for $currentPath, requesting permission again.\",\n    );\n    final selectedPath = await _requestMacOSDownloadPathAccess(currentPath);\n    if (selectedPath != null) {\n      settings[\"downloadPath\"] = selectedPath;\n      writeSettings();\n      Log.info(\n        \"DownloadPath\",\n        \"Re-authorized macOS directory access: $selectedPath\",\n      );\n      return;\n    }\n\n    settings[\"downloadPath\"] = defaultPath;\n    writeSettings();\n    Log.warning(\n      \"DownloadPath\",\n      \"macOS directory permission denied or canceled, fallback to default path: $defaultPath\",\n    );\n  }\n\n  Future<String?> _restoreMacOSDownloadPathAccess(String? expectedPath) async {\n    try {\n      return await _macosDownloadPathChannel.invokeMethod<String>(\n        \"restoreDownloadDirectoryAccess\",\n        {\"path\": expectedPath},\n      );\n    } catch (e) {\n      Log.warning(\"DownloadPath\", \"restoreDownloadDirectoryAccess failed: $e\");\n      return null;\n    }\n  }\n\n  Future<String?> _requestMacOSDownloadPathAccess(String? initialPath) async {\n    try {\n      return await _macosDownloadPathChannel.invokeMethod<String>(\n        \"selectDownloadDirectory\",\n        {\"initialPath\": initialPath},\n      );\n    } catch (e) {\n      Log.warning(\"DownloadPath\", \"selectDownloadDirectory failed: $e\");\n      return null;\n    }\n  }\n\n  String _normalizePath(String path) {\n    if (!App.isMacOS) {\n      return path;\n    }\n    final home = Platform.environment[\"HOME\"];\n    if (home != null && path.startsWith(\"~\")) {\n      return path.replaceFirst(\"~\", home);\n    }\n    return path;\n  }\n\n  Future<String> get _defaultDownloadPath async {\n    if (App.isAndroid) {\n      String? downloadPath = \"/storage/emulated/0/download\";\n      if (!Directory(downloadPath).havePermission()) {\n        downloadPath = null;\n      }\n      var res = downloadPath;\n      res ??= (await getExternalStorageDirectory())!.path;\n      return \"$res/pixes\";\n    } else if (App.isWindows) {\n      var res =\n          await const MethodChannel(\"pixes/picture_folder\").invokeMethod(\"\");\n      if (res != \"error\") {\n        return res + \"/pixes\";\n      }\n    } else if (App.isLinux) {\n      var downloadPath = (await getDownloadsDirectory())?.path;\n      if (downloadPath != null && Directory(downloadPath).havePermission()) {\n        return \"$downloadPath/pixes\";\n      }\n    } else if (App.isMacOS) {\n      if (Directory(\"~/Pictures\").havePermission()) {\n        return \"~/Pictures/pixes\";\n      }\n      try {\n        Directory(\"~/Downloads/pixes\").createSync(recursive: true);\n        return \"~/Downloads/pixes\";\n      } catch (e) {\n        return \"${App.dataPath}/download\";\n      }\n    }\n\n    return \"${App.dataPath}/download\";\n  }\n}\n\nfinal appdata = _Appdata();\n"
  },
  {
    "path": "lib/components/animated_image.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:flutter/scheduler.dart';\n\nclass AnimatedImage extends StatefulWidget {\n  /// show animation when loading is complete.\n  AnimatedImage({\n    required ImageProvider image,\n    super.key,\n    double scale = 1.0,\n    this.semanticLabel,\n    this.excludeFromSemantics = false,\n    this.width,\n    this.height,\n    this.color,\n    this.opacity,\n    this.colorBlendMode,\n    this.fit,\n    this.alignment = Alignment.center,\n    this.repeat = ImageRepeat.noRepeat,\n    this.centerSlice,\n    this.matchTextDirection = false,\n    this.gaplessPlayback = false,\n    this.filterQuality = FilterQuality.low,\n    this.isAntiAlias = false,\n    Map<String, String>? headers,\n    int? cacheWidth,\n    int? cacheHeight,\n  }\n      ): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),\n        assert(cacheWidth == null || cacheWidth > 0),\n        assert(cacheHeight == null || cacheHeight > 0);\n\n  final ImageProvider image;\n\n  final String? semanticLabel;\n\n  final bool excludeFromSemantics;\n\n  final double? width;\n\n  final double? height;\n\n  final bool gaplessPlayback;\n\n  final bool matchTextDirection;\n\n  final Rect? centerSlice;\n\n  final ImageRepeat repeat;\n\n  final AlignmentGeometry alignment;\n\n  final BoxFit? fit;\n\n  final BlendMode? colorBlendMode;\n\n  final FilterQuality filterQuality;\n\n  final Animation<double>? opacity;\n\n  final Color? color;\n\n  final bool isAntiAlias;\n\n  static void clear() => _AnimatedImageState.clear();\n\n  @override\n  State<AnimatedImage> createState() => _AnimatedImageState();\n}\n\nclass _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {\n  ImageStream? _imageStream;\n  ImageInfo? _imageInfo;\n  ImageChunkEvent? _loadingProgress;\n  bool _isListeningToStream = false;\n  late bool _invertColors;\n  int? _frameNumber;\n  bool _wasSynchronouslyLoaded = false;\n  late DisposableBuildContext<State<AnimatedImage>> _scrollAwareContext;\n  Object? _lastException;\n  ImageStreamCompleterHandle? _completerHandle;\n\n  static final Map<int, Size> _cache = {};\n\n  static clear() => _cache.clear();\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _scrollAwareContext = DisposableBuildContext<State<AnimatedImage>>(this);\n  }\n\n  @override\n  void dispose() {\n    assert(_imageStream != null);\n    WidgetsBinding.instance.removeObserver(this);\n    _stopListeningToStream();\n    _completerHandle?.dispose();\n    _scrollAwareContext.dispose();\n    _replaceImage(info: null);\n    super.dispose();\n  }\n\n  @override\n  void didChangeDependencies() {\n    _updateInvertColors();\n    _resolveImage();\n\n    if (TickerMode.of(context)) {\n      _listenToStream();\n    } else {\n      _stopListeningToStream(keepStreamAlive: true);\n    }\n\n    super.didChangeDependencies();\n  }\n\n  @override\n  void didUpdateWidget(AnimatedImage oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    if (widget.image != oldWidget.image) {\n      _resolveImage();\n    }\n  }\n\n  @override\n  void didChangeAccessibilityFeatures() {\n    super.didChangeAccessibilityFeatures();\n    setState(() {\n      _updateInvertColors();\n    });\n  }\n\n  @override\n  void reassemble() {\n    _resolveImage(); // in case the image cache was flushed\n    super.reassemble();\n  }\n\n  void _updateInvertColors() {\n    _invertColors = MediaQuery.maybeInvertColorsOf(context)\n        ?? SemanticsBinding.instance.accessibilityFeatures.invertColors;\n  }\n\n  void _resolveImage() {\n    final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(\n      context: _scrollAwareContext,\n      imageProvider: widget.image,\n    );\n    final ImageStream newStream =\n    provider.resolve(createLocalImageConfiguration(\n      context,\n      size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,\n    ));\n    _updateSourceStream(newStream);\n  }\n\n  ImageStreamListener? _imageStreamListener;\n  ImageStreamListener _getListener({bool recreateListener = false}) {\n    if(_imageStreamListener == null || recreateListener) {\n      _lastException = null;\n      _imageStreamListener = ImageStreamListener(\n        _handleImageFrame,\n        onChunk: _handleImageChunk,\n        onError: (Object error, StackTrace? stackTrace) {\n          setState(() {\n            _lastException = error;\n          });\n        },\n      );\n    }\n    return _imageStreamListener!;\n  }\n\n  void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {\n    setState(() {\n      _replaceImage(info: imageInfo);\n      _loadingProgress = null;\n      _lastException = null;\n      _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;\n      _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;\n    });\n  }\n\n  void _handleImageChunk(ImageChunkEvent event) {\n    setState(() {\n      _loadingProgress = event;\n      _lastException = null;\n    });\n  }\n\n  void _replaceImage({required ImageInfo? info}) {\n    final ImageInfo? oldImageInfo = _imageInfo;\n    SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());\n    _imageInfo = info;\n  }\n\n  // Updates _imageStream to newStream, and moves the stream listener\n  // registration from the old stream to the new stream (if a listener was\n  // registered).\n  void _updateSourceStream(ImageStream newStream) {\n    if (_imageStream?.key == newStream.key) {\n      return;\n    }\n\n    if (_isListeningToStream) {\n      _imageStream!.removeListener(_getListener());\n    }\n\n    if (!widget.gaplessPlayback) {\n      setState(() { _replaceImage(info: null); });\n    }\n\n    setState(() {\n      _loadingProgress = null;\n      _frameNumber = null;\n      _wasSynchronouslyLoaded = false;\n    });\n\n    _imageStream = newStream;\n    if (_isListeningToStream) {\n      _imageStream!.addListener(_getListener());\n    }\n  }\n\n  void _listenToStream() {\n    if (_isListeningToStream) {\n      return;\n    }\n\n    _imageStream!.addListener(_getListener());\n    _completerHandle?.dispose();\n    _completerHandle = null;\n\n    _isListeningToStream = true;\n  }\n\n  /// Stops listening to the image stream, if this state object has attached a\n  /// listener.\n  ///\n  /// If the listener from this state is the last listener on the stream, the\n  /// stream will be disposed. To keep the stream alive, set `keepStreamAlive`\n  /// to true, which create [ImageStreamCompleterHandle] to keep the completer\n  /// alive and is compatible with the [TickerMode] being off.\n  void _stopListeningToStream({bool keepStreamAlive = false}) {\n    if (!_isListeningToStream) {\n      return;\n    }\n\n    if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {\n      _completerHandle = _imageStream!.completer!.keepAlive();\n    }\n\n    _imageStream!.removeListener(_getListener());\n    _isListeningToStream = false;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget result;\n\n    if(_imageInfo != null){\n      // build image\n      result = RawImage(\n        // Do not clone the image, because RawImage is a stateless wrapper.\n        // The image will be disposed by this state object when it is not needed\n        // anymore, such as when it is unmounted or when the image stream pushes\n        // a new image.\n        image: _imageInfo?.image,\n        width: widget.width,\n        height: widget.height,\n        debugImageLabel: _imageInfo?.debugLabel,\n        scale: _imageInfo?.scale ?? 1.0,\n        color: widget.color,\n        opacity: widget.opacity,\n        colorBlendMode: widget.colorBlendMode,\n        alignment: widget.alignment,\n        repeat: widget.repeat,\n        centerSlice: widget.centerSlice,\n        matchTextDirection: widget.matchTextDirection,\n        invertColors: _invertColors,\n        isAntiAlias: widget.isAntiAlias,\n        filterQuality: widget.filterQuality,\n        fit: widget.fit,\n      );\n    } else if (_lastException != null) {\n      result = const Center(\n        child: Icon(FluentIcons.error),\n      );\n\n      if (!widget.excludeFromSemantics) {\n        result = Semantics(\n          container: widget.semanticLabel != null,\n          image: true,\n          label: widget.semanticLabel ?? '',\n          child: result,\n        );\n      }\n    } else{\n      result = const Center();\n    }\n\n    return AnimatedSwitcher(\n      duration: const Duration(milliseconds: 200),\n      reverseDuration: const Duration(milliseconds: 200),\n      child: result,\n    );\n  }\n\n  @override\n  void debugFillProperties(DiagnosticPropertiesBuilder description) {\n    super.debugFillProperties(description);\n    description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));\n    description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));\n    description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));\n    description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));\n    description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));\n  }\n}\n"
  },
  {
    "path": "lib/components/batch_download.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/message.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/download.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../network/network.dart';\n\nclass BatchDownloadButton extends StatelessWidget {\n  const BatchDownloadButton({super.key, required this.request});\n\n  final Future<Res<List<Illust>>> Function() request;\n\n  @override\n  Widget build(BuildContext context) {\n    return Button(\n      child: const Icon(\n        MdIcons.download,\n        size: 20,\n      ),\n      onPressed: () {\n        showDialog(\n            context: context,\n            barrierDismissible: false,\n            barrierColor: Colors.transparent,\n            useRootNavigator: false,\n            builder: (context) => _DownloadDialog(request));\n      },\n    );\n  }\n}\n\nclass _DownloadDialog extends StatefulWidget {\n  const _DownloadDialog(this.request);\n\n  final Future<Res<List<Illust>>> Function() request;\n\n  @override\n  State<_DownloadDialog> createState() => _DownloadDialogState();\n}\n\nclass _DownloadDialogState extends State<_DownloadDialog> {\n  int maxCount = 30;\n\n  int currentCount = 0;\n\n  bool loading = false;\n\n  bool cancel = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return ContentDialog(\n      title: Text(\"Batch download\".tl),\n      content: SizedBox(\n        child: Column(\n          mainAxisSize: MainAxisSize.min,\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (!loading) Text('${\"Maximum number of downloads\".tl}:'),\n            if (loading) Text(\"${\"Current quantity\".tl}: $currentCount\"),\n            const SizedBox(\n              height: 16,\n            ),\n            SizedBox(\n              height: 42,\n              width: 196,\n              child: NumberBox(\n                value: maxCount,\n                onChanged: (value) {\n                  if (!loading) {\n                    setState(() => maxCount = value ?? maxCount);\n                  }\n                },\n                allowExpressions: true,\n                mode: SpinButtonPlacementMode.inline,\n                smallChange: 10,\n                largeChange: 30,\n                clearButton: false,\n              ),\n            ),\n          ],\n        ).paddingVertical(8),\n      ),\n      actions: [\n        Button(\n            child: Text(\"Cancel\".tl),\n            onPressed: () {\n              cancel = true;\n              context.pop();\n            }),\n        if (!loading)\n          FilledButton(onPressed: load, child: Text(\"Continue\".tl))\n        else\n          FilledButton(\n              onPressed: () {},\n              child: const SizedBox(\n                height: 20,\n                width: 64,\n                child: Center(\n                  child: SizedBox.square(\n                    dimension: 18,\n                    child: ProgressRing(\n                      strokeWidth: 1.6,\n                    ),\n                  ),\n                ),\n              ))\n      ],\n    );\n  }\n\n  void load() async {\n    setState(() {\n      loading = true;\n    });\n\n    var request = widget.request();\n\n    List<Illust> all = [];\n    String? nextUrl;\n    int retryCount = 0;\n    while (nextUrl != \"end\" && all.length < maxCount) {\n      if (nextUrl != null) {\n        request = Network().getIllustsWithNextUrl(nextUrl);\n      }\n      var res = await request;\n      if (cancel || !mounted) {\n        return;\n      }\n      if (res.error) {\n        retryCount++;\n        if (retryCount > 3) {\n          setState(() {\n            loading = false;\n          });\n          showToast(context, message: \"Error\".tl);\n          return;\n        }\n        await Future.delayed(Duration(seconds: 1 << retryCount));\n        continue;\n      }\n      all.addAll(res.data);\n      setState(() {\n        currentCount = all.length;\n      });\n      nextUrl = res.subData ?? \"end\";\n    }\n    int i = 0;\n    for (var illust in all) {\n      if (i > maxCount) break;\n      DownloadManager().addDownloadingTask(illust);\n      i++;\n    }\n    context.pop();\n  }\n}\n"
  },
  {
    "path": "lib/components/button.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\n\nabstract class BaseButton extends StatelessWidget {\n  const BaseButton({this.enabled = true, this.isLoading = false, super.key});\n\n  final bool enabled;\n\n  final bool isLoading;\n\n  Widget buildNormal(BuildContext context);\n\n  Widget buildLoading(BuildContext context);\n\n  Widget buildDisabled(BuildContext context);\n\n  @override\n  Widget build(BuildContext context) {\n    if (isLoading) {\n      return buildLoading(context);\n    } else if (enabled) {\n      return buildNormal(context);\n    } else {\n      return buildDisabled(context);\n    }\n  }\n}\n\nclass FluentButton extends BaseButton {\n  const FluentButton({\n    required this.onPressed,\n    required this.child,\n    this.width,\n    super.enabled,\n    super.isLoading,\n    super.key,\n  });\n\n  final void Function() onPressed;\n\n  final Widget child;\n\n  final double? width;\n\n  static const _kFluentButtonPadding = 12.0;\n\n  @override\n  Widget buildNormal(BuildContext context) {\n    Widget child = this.child;\n    if (width != null) {\n      child = child.fixWidth(width! - _kFluentButtonPadding * 2);\n    }\n    return FilledButton(\n      onPressed: onPressed,\n      child: child,\n    );\n  }\n\n  @override\n  Widget buildLoading(BuildContext context) {\n    Widget child = Center(\n      widthFactor: 1,\n      heightFactor: 1,\n      child: const ProgressRing(\n        strokeWidth: 1.6,\n      ).fixWidth(14).fixHeight(14),\n    );\n    if (width != null) {\n      child = child.fixWidth(width! - _kFluentButtonPadding * 2);\n    }\n    return Container(\n      padding: const EdgeInsets.symmetric(\n          horizontal: _kFluentButtonPadding, vertical: 6.5),\n      decoration: BoxDecoration(\n          color: FluentTheme.of(context).inactiveBackgroundColor,\n          borderRadius: BorderRadius.circular(4)),\n      child: child,\n    );\n  }\n\n  @override\n  Widget buildDisabled(BuildContext context) {\n    Widget child = Center(\n      widthFactor: 1,\n      heightFactor: 1,\n      child: this.child,\n    );\n    if (width != null) {\n      child = child.fixWidth(width! - _kFluentButtonPadding * 2);\n    }\n    return Container(\n      padding: const EdgeInsets.symmetric(\n          horizontal: _kFluentButtonPadding, vertical: 6.5),\n      decoration: BoxDecoration(\n          color: FluentTheme.of(context).inactiveBackgroundColor,\n          borderRadius: BorderRadius.circular(4)),\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/grid.dart",
    "content": "import 'package:flutter/rendering.dart';\nimport 'package:flutter/widgets.dart';\nimport 'package:pixes/foundation/app.dart';\n\nclass SliverGridViewWithFixedItemHeight extends StatelessWidget {\n  const SliverGridViewWithFixedItemHeight(\n      {required this.delegate,\n      this.maxCrossAxisExtent = double.infinity,\n      this.minCrossAxisExtent = 0,\n      required this.itemHeight,\n      super.key});\n\n  final SliverChildDelegate delegate;\n\n  final double maxCrossAxisExtent;\n\n  final double minCrossAxisExtent;\n\n  final double itemHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverGrid(\n      delegate: delegate,\n      gridDelegate: SliverGridDelegateWithFixedHeight(\n          itemHeight: itemHeight,\n          maxCrossAxisExtent: maxCrossAxisExtent,\n          minCrossAxisExtent: minCrossAxisExtent),\n    ).sliverPadding(EdgeInsets.only(bottom: context.padding.bottom));\n  }\n}\n\nclass GridViewWithFixedItemHeight extends StatelessWidget {\n  const GridViewWithFixedItemHeight(\n      {required this.builder,\n      required this.itemCount,\n      this.maxCrossAxisExtent = double.infinity,\n      this.minCrossAxisExtent = 0,\n      required this.itemHeight,\n      super.key});\n\n  final Widget Function(BuildContext, int) builder;\n\n  final int itemCount;\n\n  final double maxCrossAxisExtent;\n\n  final double minCrossAxisExtent;\n\n  final double itemHeight;\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n        builder: ((context, constraints) => GridView.builder(\n              gridDelegate: SliverGridDelegateWithFixedHeight(\n                  itemHeight: itemHeight,\n                  maxCrossAxisExtent: maxCrossAxisExtent,\n                  minCrossAxisExtent: minCrossAxisExtent),\n              itemBuilder: builder,\n              itemCount: itemCount,\n              padding: EdgeInsets.only(bottom: context.padding.bottom),\n            )));\n  }\n}\n\nclass SliverGridDelegateWithFixedHeight extends SliverGridDelegate {\n  const SliverGridDelegateWithFixedHeight({\n    this.maxCrossAxisExtent = double.infinity,\n    this.minCrossAxisExtent = 0,\n    required this.itemHeight,\n  });\n\n  final double maxCrossAxisExtent;\n\n  final double minCrossAxisExtent;\n\n  final double itemHeight;\n\n  @override\n  SliverGridLayout getLayout(SliverConstraints constraints) {\n    var crossItemsCount = calcCrossItemsCount(constraints.crossAxisExtent);\n    return SliverGridRegularTileLayout(\n        crossAxisCount: crossItemsCount,\n        mainAxisStride: itemHeight,\n        childMainAxisExtent: itemHeight,\n        crossAxisStride: constraints.crossAxisExtent / crossItemsCount,\n        childCrossAxisExtent: constraints.crossAxisExtent / crossItemsCount,\n        reverseCrossAxis: false);\n  }\n\n  int calcCrossItemsCount(double width) {\n    int count = 20;\n    var itemWidth = width / 20;\n\n    if(minCrossAxisExtent == 0) {\n      count = 1;\n      itemWidth = width;\n      while(itemWidth > maxCrossAxisExtent) {\n        count++;\n        itemWidth = width / count;\n      }\n      return count;\n    }\n\n    while (\n        !(itemWidth > minCrossAxisExtent && itemWidth < maxCrossAxisExtent)) {\n      count--;\n      itemWidth = width / count;\n      if (count == 1) {\n        return 1;\n      }\n    }\n    return count;\n  }\n\n  @override\n  bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {\n    return oldDelegate is! SliverGridDelegateWithFixedHeight ||\n        oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||\n        oldDelegate.minCrossAxisExtent != minCrossAxisExtent ||\n        oldDelegate.itemHeight != itemHeight;\n  }\n}\n"
  },
  {
    "path": "lib/components/illust_widget.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/history.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/download.dart';\nimport 'package:pixes/pages/related_page.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../network/network.dart';\nimport '../pages/illust_page.dart';\nimport 'md.dart';\n\ntypedef UpdateFavoriteFunc = void Function(bool v);\n\nclass IllustWidget extends StatefulWidget {\n  const IllustWidget(this.illust, {this.onTap, super.key});\n\n  final Illust illust;\n\n  final void Function()? onTap;\n\n  static Map<String, UpdateFavoriteFunc> favoriteCallbacks = {};\n\n  @override\n  State<IllustWidget> createState() => _IllustWidgetState();\n}\n\nclass _IllustWidgetState extends State<IllustWidget> {\n  bool isBookmarking = false;\n\n  final contextController = FlyoutController();\n  final contextAttachKey = GlobalKey();\n\n  @override\n  void initState() {\n    IllustWidget.favoriteCallbacks[widget.illust.id.toString()] = (v) {\n      setState(() {\n        widget.illust.isBookmarked = v;\n      });\n    };\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    IllustWidget.favoriteCallbacks.remove(widget.illust.id.toString());\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(builder: (context, constrains) {\n      final width = constrains.maxWidth;\n      final height = widget.illust.height * width / widget.illust.width;\n      return FlyoutTarget(\n        controller: contextController,\n        child: SizedBox(\n          key: contextAttachKey,\n          width: width,\n          height: height,\n          child: Stack(\n            children: [\n              Positioned.fill(\n                child: Container(\n                  width: width,\n                  height: height,\n                  padding: const EdgeInsets.symmetric(\n                      horizontal: 8.0, vertical: 8.0),\n                  child: Container(\n                    width: double.infinity,\n                    height: double.infinity,\n                    padding: EdgeInsets.zero,\n                    decoration: BoxDecoration(\n                      borderRadius: BorderRadius.circular(4.0),\n                      color: FluentTheme.of(context).cardColor,\n                      border: () {\n                        var emphasis = widget.illust.author.isFollowed &&\n                            appdata.settings[\n                                'emphasizeArtworksFromFollowingArtists'];\n                        var color = emphasis\n                            ? ColorScheme.of(context).primary\n                            : ColorScheme.of(context)\n                                .outlineVariant\n                                .toOpacity(0.64);\n                        var width = emphasis ? 1.6 : 1.0;\n                        return Border.all(color: color, width: width);\n                      }(),\n                    ),\n                    margin: EdgeInsets.zero,\n                    child: GestureDetector(\n                      onTap: widget.onTap ??\n                          () {\n                            context.to(() => IllustPage(widget.illust));\n                          },\n                      onSecondaryTapUp: showMenu,\n                      onLongPress: showMenu,\n                      child: ClipRRect(\n                        borderRadius: BorderRadius.circular(4.0),\n                        child: AnimatedImage(\n                          image: CachedImageProvider(\n                              widget.illust.images.first.medium),\n                          fit: BoxFit.cover,\n                          width: width - 16.0,\n                          height: height - 16.0,\n                        ),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n              if (widget.illust.images.length > 1)\n                Positioned(\n                  top: 12,\n                  left: 12,\n                  child: Container(\n                      width: 28,\n                      height: 20,\n                      decoration: BoxDecoration(\n                        color: FluentTheme.of(context)\n                            .micaBackgroundColor\n                            .toOpacity(0.72),\n                        borderRadius: BorderRadius.circular(4),\n                        border: Border.all(\n                            color: ColorScheme.of(context).outlineVariant,\n                            width: 0.6),\n                      ),\n                      child: Center(\n                        child: Text(\n                          \"${widget.illust.images.length}P\",\n                          style: const TextStyle(fontSize: 12),\n                        ),\n                      )),\n                ),\n              if (widget.illust.isAi)\n                Positioned(\n                  bottom: 12,\n                  left: 12,\n                  child: Container(\n                      width: 28,\n                      height: 20,\n                      decoration: BoxDecoration(\n                        color: ColorScheme.of(context)\n                            .errorContainer\n                            .toOpacity(0.8),\n                        borderRadius: BorderRadius.circular(4),\n                        border: Border.all(\n                            color: ColorScheme.of(context).outlineVariant,\n                            width: 0.6),\n                      ),\n                      child: const Center(\n                        child: Text(\n                          \"AI\",\n                          style: TextStyle(fontSize: 12),\n                        ),\n                      )),\n                ),\n              if (widget.illust.isUgoira)\n                Positioned(\n                  bottom: 12,\n                  left: 12,\n                  child: Container(\n                      width: 28,\n                      height: 20,\n                      decoration: BoxDecoration(\n                        color: ColorScheme.of(context)\n                            .primaryContainer\n                            .toOpacity(0.8),\n                        borderRadius: BorderRadius.circular(4),\n                        border: Border.all(\n                            color: ColorScheme.of(context).outlineVariant,\n                            width: 0.6),\n                      ),\n                      child: const Center(\n                        child: Text(\n                          \"GIF\",\n                          style: TextStyle(fontSize: 12),\n                        ),\n                      )),\n                ),\n              if (widget.illust.isR18)\n                Positioned(\n                  bottom: 12,\n                  right: 12,\n                  child: Container(\n                      width: 28,\n                      height: 20,\n                      decoration: BoxDecoration(\n                        color: ColorScheme.of(context).errorContainer,\n                        borderRadius: BorderRadius.circular(4),\n                        border: Border.all(\n                            color: ColorScheme.of(context).outlineVariant,\n                            width: 0.6),\n                      ),\n                      child: const Center(\n                        child: Text(\n                          \"R18\",\n                          style: TextStyle(fontSize: 12),\n                        ),\n                      )),\n                ),\n              if (widget.illust.isR18G)\n                Positioned(\n                  bottom: 12,\n                  right: 12,\n                  child: Container(\n                      width: 28,\n                      height: 20,\n                      decoration: BoxDecoration(\n                        color: ColorScheme.of(context).errorContainer,\n                        borderRadius: BorderRadius.circular(4),\n                        border: Border.all(\n                            color: ColorScheme.of(context).outlineVariant,\n                            width: 0.6),\n                      ),\n                      child: const Center(\n                        child: Text(\n                          \"R18G\",\n                          style: TextStyle(fontSize: 12),\n                        ),\n                      )),\n                ),\n              Positioned(\n                top: 16,\n                right: 16,\n                child: buildButton(),\n              )\n            ],\n          ),\n        ),\n      );\n    });\n  }\n\n  void showMenu([TapUpDetails? details]) {\n    // This calculates the position of the flyout according to the parent navigator\n    final targetContext = contextAttachKey.currentContext;\n    if (targetContext == null) return;\n    final box = targetContext.findRenderObject() as RenderBox;\n    Offset? position = box.localToGlobal(\n      details?.localPosition ?? box.size.center(Offset.zero),\n      ancestor: Navigator.of(context).context.findRenderObject(),\n    );\n\n    contextController.showFlyout(\n      barrierColor: Colors.transparent,\n      position: position,\n      builder: (context) {\n        return MenuFlyout(\n          items: [\n            MenuFlyoutItem(\n                text: Text(\"View\".tl),\n                onPressed: () {\n                  context.to(() => IllustPage(widget.illust));\n                }),\n            MenuFlyoutItem(\n                text: Text(\"Private Favorite\".tl),\n                onPressed: () {\n                  favorite(\"private\");\n                }),\n            MenuFlyoutItem(\n                text: Text(\"Download\".tl),\n                onPressed: () {\n                  context.showToast(message: \"Added\");\n                  DownloadManager().addDownloadingTask(widget.illust);\n                }),\n            MenuFlyoutItem(\n                text: Text(\"Related Artworks\".tl),\n                onPressed: () {\n                  context.to(\n                      () => RelatedIllustsPage(widget.illust.id.toString()));\n                }),\n          ],\n        );\n      },\n    );\n  }\n\n  void favorite([String type = \"public\"]) async {\n    if (isBookmarking) return;\n    setState(() {\n      isBookmarking = true;\n    });\n    var method = widget.illust.isBookmarked ? \"delete\" : \"add\";\n    var res =\n        await Network().addBookmark(widget.illust.id.toString(), method, type);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      widget.illust.isBookmarked = !widget.illust.isBookmarked;\n    }\n    setState(() {\n      isBookmarking = false;\n    });\n  }\n\n  Widget buildButton() {\n    Widget child;\n    if (isBookmarking) {\n      child = const SizedBox(\n        width: 14,\n        height: 14,\n        child: ProgressRing(\n          strokeWidth: 1.6,\n        ),\n      );\n    } else if (widget.illust.isBookmarked) {\n      child = Icon(\n        MdIcons.favorite,\n        color: Colors.red,\n        size: 22,\n      );\n    } else {\n      child = Icon(\n        MdIcons.favorite,\n        color: ColorScheme.of(context).outline,\n        size: 22,\n      );\n    }\n\n    return SizedBox(\n      height: 24,\n      width: 24,\n      child: MouseRegion(\n        cursor: SystemMouseCursors.click,\n        child: GestureDetector(\n          onTap: favorite,\n          child: Center(\n            child: child,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass IllustHistoryWidget extends StatelessWidget {\n  const IllustHistoryWidget(this.illust, {super.key});\n\n  final IllustHistory illust;\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(builder: (context, constrains) {\n      final width = constrains.maxWidth;\n      final height = illust.height * width / illust.width;\n      return SizedBox(\n        width: width,\n        height: height,\n        child: Stack(\n          children: [\n            Positioned.fill(\n                child: Container(\n              width: width,\n              height: height,\n              padding:\n                  const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),\n              child: Card(\n                padding: EdgeInsets.zero,\n                margin: EdgeInsets.zero,\n                child: GestureDetector(\n                  onTap: () {\n                    context.to(() => IllustPageWithId(illust.id.toString()));\n                  },\n                  child: ClipRRect(\n                    borderRadius: BorderRadius.circular(4.0),\n                    child: AnimatedImage(\n                      image: CachedImageProvider(illust.imgPath),\n                      fit: BoxFit.cover,\n                      width: width - 16.0,\n                      height: height - 16.0,\n                    ),\n                  ),\n                ),\n              ),\n            )),\n            if (illust.imageCount > 1)\n              Positioned(\n                top: 12,\n                left: 12,\n                child: Container(\n                    width: 28,\n                    height: 20,\n                    decoration: BoxDecoration(\n                      color: FluentTheme.of(context)\n                          .micaBackgroundColor\n                          .toOpacity(0.72),\n                      borderRadius: BorderRadius.circular(4),\n                      border: Border.all(\n                          color: ColorScheme.of(context).outlineVariant,\n                          width: 0.6),\n                    ),\n                    child: Center(\n                      child: Text(\n                        \"${illust.imageCount}P\",\n                        style: const TextStyle(fontSize: 12),\n                      ),\n                    )),\n              ),\n            if (illust.isAi)\n              Positioned(\n                bottom: 12,\n                left: 12,\n                child: Container(\n                    width: 28,\n                    height: 20,\n                    decoration: BoxDecoration(\n                      color: ColorScheme.of(context)\n                          .errorContainer\n                          .toOpacity(0.8),\n                      borderRadius: BorderRadius.circular(4),\n                      border: Border.all(\n                          color: ColorScheme.of(context).outlineVariant,\n                          width: 0.6),\n                    ),\n                    child: const Center(\n                      child: Text(\n                        \"AI\",\n                        style: TextStyle(fontSize: 12),\n                      ),\n                    )),\n              ),\n            if (illust.isGif)\n              Positioned(\n                bottom: 12,\n                left: 12,\n                child: Container(\n                    width: 28,\n                    height: 20,\n                    decoration: BoxDecoration(\n                      color: ColorScheme.of(context)\n                          .primaryContainer\n                          .toOpacity(0.8),\n                      borderRadius: BorderRadius.circular(4),\n                      border: Border.all(\n                          color: ColorScheme.of(context).outlineVariant,\n                          width: 0.6),\n                    ),\n                    child: const Center(\n                      child: Text(\n                        \"GIF\",\n                        style: TextStyle(fontSize: 12),\n                      ),\n                    )),\n              ),\n            if (illust.isR18)\n              Positioned(\n                bottom: 12,\n                right: 12,\n                child: Container(\n                    width: 28,\n                    height: 20,\n                    decoration: BoxDecoration(\n                      color: ColorScheme.of(context).errorContainer,\n                      borderRadius: BorderRadius.circular(4),\n                      border: Border.all(\n                          color: ColorScheme.of(context).outlineVariant,\n                          width: 0.6),\n                    ),\n                    child: const Center(\n                      child: Text(\n                        \"R18\",\n                        style: TextStyle(fontSize: 12),\n                      ),\n                    )),\n              ),\n            if (illust.isR18G)\n              Positioned(\n                bottom: 12,\n                right: 12,\n                child: Container(\n                    width: 28,\n                    height: 20,\n                    decoration: BoxDecoration(\n                      color: ColorScheme.of(context).errorContainer,\n                      borderRadius: BorderRadius.circular(4),\n                      border: Border.all(\n                          color: ColorScheme.of(context).outlineVariant,\n                          width: 0.6),\n                    ),\n                    child: const Center(\n                      child: Text(\n                        \"R18G\",\n                        style: TextStyle(fontSize: 12),\n                      ),\n                    )),\n              ),\n          ],\n        ),\n      );\n    });\n  }\n}\n"
  },
  {
    "path": "lib/components/keyboard.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pixes/foundation/app.dart';\n\ntypedef KeyEventHandler = void Function(LogicalKeyboardKey key);\n\nclass KeyEventListener extends StatefulWidget {\n  const KeyEventListener({required this.child, super.key});\n\n  final Widget child;\n\n  static KeyEventListenerState? of(BuildContext context) {\n    return context.findAncestorStateOfType<KeyEventListenerState>();\n  }\n\n  @override\n  State<KeyEventListener> createState() => KeyEventListenerState();\n}\n\nclass KeyEventListenerState extends State<KeyEventListener> {\n  final focusNode = FocusNode();\n\n  final List<KeyEventHandler> _handlers = [];\n\n  void addHandler(KeyEventHandler handler) {\n    _handlers.add(handler);\n  }\n\n  void removeHandler(KeyEventHandler handler) {\n    _handlers.remove(handler);\n  }\n\n  void removeAll() {\n    _handlers.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Focus(\n      focusNode: focusNode,\n      autofocus: true,\n      onKeyEvent: (node, event) {\n        if (event is! KeyUpEvent) return KeyEventResult.ignored;\n        if (event.logicalKey == LogicalKeyboardKey.escape) {\n          if (App.rootNavigatorKey.currentState?.canPop() ?? false) {\n            App.rootNavigatorKey.currentState?.pop();\n          } else if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {\n            App.mainNavigatorKey?.currentState?.pop();\n          }\n          return KeyEventResult.handled;\n        }\n        for (var handler in _handlers) {\n          handler(event.logicalKey);\n        }\n        return KeyEventResult.ignored;\n      },\n      child: widget.child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/loading.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/res.dart';\n\nabstract class LoadingState<T extends StatefulWidget, S extends Object> extends State<T>{\n  bool isLoading = false;\n\n  S? data;\n\n  String? error;\n\n  Future<Res<S>> loadData();\n\n  Widget buildContent(BuildContext context, S data);\n\n  Widget? buildFrame(BuildContext context, Widget child) => null;\n\n  Widget buildLoading() {\n    return const Center(\n      child: ProgressRing(),\n    );\n  }\n\n  void retry() {\n    setState(() {\n      isLoading = true;\n      error = null;\n    });\n    loadData().then((value) {\n      if(value.success) {\n        setState(() {\n          isLoading = false;\n          data = value.data;\n        });\n      } else {\n        setState(() {\n          isLoading = false;\n          error = value.errorMessage!;\n        });\n      }\n    });\n  }\n\n  Widget buildError() {\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text(error!),\n          const SizedBox(height: 12),\n          Button(\n            onPressed: retry,\n            child: const Text(\"Retry\"),\n          )\n        ],\n      ),\n    ).paddingHorizontal(16);\n  }\n\n  @override\n  @mustCallSuper\n  void initState() {\n    isLoading = true;\n    loadData().then((value) {\n      if(value.success) {\n        setState(() {\n          isLoading = false;\n          data = value.data;\n        });\n      } else {\n        setState(() {\n          isLoading = false;\n          error = value.errorMessage!;\n        });\n      }\n    });\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child;\n\n    if(isLoading){\n      child = buildLoading();\n    } else if (error != null){\n      child = buildError();\n    } else {\n      child = buildContent(context, data!);\n    }\n\n    return buildFrame(context, child) ?? child;\n  }\n}\n\nabstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object> extends State<T>{\n  bool _isFirstLoading = true;\n\n  bool _isLoading = false;\n\n  List<S>? _data;\n\n  String? _error;\n\n  int _page = 1;\n\n  Future<Res<List<S>>> loadData(int page);\n\n  Widget? buildFrame(BuildContext context, Widget child) => null;\n\n  Widget buildContent(BuildContext context, List<S> data);\n\n  bool get isLoading => _isLoading || _isFirstLoading;\n\n  bool get isFirstLoading => _isFirstLoading;\n\n  void nextPage() {\n    if(_isLoading) return;\n    _isLoading = true;\n    loadData(_page).then((value) {\n      _isLoading = false;\n      if(value.success) {\n        _page++;\n        setState(() {\n          _data!.addAll(value.data);\n        });\n      } else {\n        var message = value.errorMessage ?? \"Network Error\";\n        if(message == \"No more data\") {\n          return;\n        }\n        if(message.length > 20) {\n          message = \"${message.substring(0, 20)}...\";\n        }\n        if (mounted) {\n          context.showToast(message: message);\n        }\n      }\n    });\n  }\n\n  void reset() {\n    setState(() {\n      _isFirstLoading = true;\n      _isLoading = false;\n      _data = null;\n      _error = null;\n      _page = 1;\n    });\n    firstLoad();\n  }\n\n  void firstLoad() {\n    loadData(_page).then((value) {\n      if (!mounted) return;\n      if(value.success) {\n        _page++;\n        setState(() {\n          _isFirstLoading = false;\n          _data = value.data;\n        });\n      } else {\n        setState(() {\n          _isFirstLoading = false;\n          _error = value.errorMessage!;\n        });\n      }\n    });\n  }\n\n  @override\n  void initState() {\n    firstLoad();\n    super.initState();\n  }\n\n  Widget buildLoading(BuildContext context) {\n    return const Center(\n      child: ProgressRing(),\n    );\n  }\n\n  Widget buildError(BuildContext context, String error) {\n    return Center(\n      child: Column(\n        mainAxisSize: MainAxisSize.min,\n        children: [\n          Text(error),\n          const SizedBox(height: 12),\n          Button(\n            onPressed: () {\n              reset();\n            },\n            child: const Text(\"Retry\"),\n          )\n        ],\n      ),\n    ).paddingHorizontal(16);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    Widget child;\n\n    if(_isFirstLoading){\n      child = buildLoading(context);\n    } else if (_error != null){\n      child = buildError(context, _error!);\n    } else {\n      child = buildContent(context, _data!);\n    }\n\n    return buildFrame(context, child) ?? child;\n  }\n}\n"
  },
  {
    "path": "lib/components/md.dart",
    "content": "import 'package:flutter/material.dart' as md;\n\ntypedef MdIcons = md.Icons;\ntypedef MdTheme = md.Theme;\ntypedef MdThemeData = md.ThemeData;\ntypedef MdColorScheme = md.ColorScheme;\ntypedef TextField = md.TextField;\n\nclass ColorScheme {\n  static md.ColorScheme of(md.BuildContext context) {\n    return md.Theme.of(context).colorScheme;\n  }\n}"
  },
  {
    "path": "lib/components/message.dart",
    "content": "import 'dart:async';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/foundation/app.dart';\n\nvoid showToast(BuildContext context, {required String message, IconData? icon}) {\n  var newEntry = OverlayEntry(\n      builder: (context) => ToastOverlay(message: message, icon: icon));\n\n  var overlay = OverlayWidget.of(context);\n\n  overlay?.addOverlay(newEntry);\n\n  Timer(const Duration(seconds: 2), () => overlay?.remove(newEntry));\n}\n\nclass ToastOverlay extends StatelessWidget {\n  const ToastOverlay({required this.message, this.icon, super.key});\n\n  final String message;\n\n  final IconData? icon;\n\n  @override\n  Widget build(BuildContext context) {\n    return Positioned(\n      bottom: 24 + MediaQuery.of(context).viewInsets.bottom,\n      left: 0,\n      right: 0,\n      child: Align(\n        alignment: Alignment.bottomCenter,\n        child: PhysicalModel(\n          color: ColorScheme.of(context).surface.toOpacity(1),\n          borderRadius: BorderRadius.circular(4),\n          elevation: 1,\n          child: Container(\n            padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                if (icon != null) Icon(icon),\n                if (icon != null)\n                  const SizedBox(\n                    width: 8,\n                  ),\n                Text(\n                  message,\n                  style: TextStyle(\n                      fontSize: 16,\n                      fontWeight: FontWeight.w500,\n                      color: ColorScheme.of(context).onSurface\n                  ),\n                  maxLines: 3,\n                ),\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass OverlayWidget extends StatefulWidget {\n  const OverlayWidget(this.child, {super.key});\n\n  final Widget child;\n\n  static OverlayWidgetState? of(BuildContext context) {\n    return LookupBoundary.findAncestorStateOfType<OverlayWidgetState>(context);\n  }\n\n  @override\n  State<OverlayWidget> createState() => OverlayWidgetState();\n}\n\nclass OverlayWidgetState extends State<OverlayWidget> {\n  var overlayKey = GlobalKey<OverlayState>();\n\n  var entries = <OverlayEntry>[];\n\n  void addOverlay(OverlayEntry entry) {\n    if (overlayKey.currentState != null) {\n      overlayKey.currentState!.insert(entry);\n      entries.add(entry);\n    }\n  }\n\n  void remove(OverlayEntry entry) {\n    if (entries.remove(entry)) {\n      entry.remove();\n    }\n  }\n\n  void removeAll() {\n    for (var entry in entries) {\n      entry.remove();\n    }\n    entries.clear();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Overlay(\n      key: overlayKey,\n      initialEntries: [OverlayEntry(builder: (context) => widget.child)],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/novel.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/novel_page.dart';\n\nclass NovelWidget extends StatefulWidget {\n  const NovelWidget(this.novel, {super.key});\n\n  final Novel novel;\n\n  @override\n  State<NovelWidget> createState() => _NovelWidgetState();\n}\n\nclass _NovelWidgetState extends State<NovelWidget> {\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n      child: GestureDetector(\n        onTap: () {\n          context.to(() => NovelPage(widget.novel));\n        },\n        behavior: HitTestBehavior.opaque,\n        child: Row(\n          children: [\n            Container(\n              width: 96,\n              height: double.infinity,\n              decoration: BoxDecoration(\n                color: ColorScheme.of(context).secondaryContainer,\n                borderRadius: BorderRadius.circular(4),\n              ),\n              clipBehavior: Clip.antiAlias,\n              child: AnimatedImage(\n                fit: BoxFit.cover,\n                filterQuality: FilterQuality.medium,\n                width: double.infinity,\n                height: double.infinity,\n                image: CachedImageProvider(widget.novel.image),\n              ),\n            ),\n            const SizedBox(\n              width: 12,\n            ),\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                children: [\n                  Text(\n                    widget.novel.title,\n                    maxLines: 2,\n                    style: const TextStyle(\n                        fontSize: 16, fontWeight: FontWeight.bold),\n                  ),\n                  const SizedBox(\n                    height: 4,\n                  ),\n                  Expanded(\n                    child: Text(\n                      widget.novel.caption.trim().replaceAll('<br />', '\\n'),\n                      maxLines: 3,\n                      overflow: TextOverflow.ellipsis,\n                    ),\n                  ),\n                  const SizedBox(\n                    height: 4,\n                  ),\n                  Text(\n                    widget.novel.author.name,\n                    style: const TextStyle(fontSize: 12),\n                  )\n                ],\n              ),\n            )\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/page_route.dart",
    "content": "import 'dart:math';\nimport 'dart:ui';\nimport 'package:flutter/gestures.dart';\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/material.dart' show PredictiveBackPageTransitionsBuilder;\nimport 'package:pixes/foundation/app.dart';\n\nconst double _kBackGestureWidth = 20.0;\nconst int _kMaxDroppedSwipePageForwardAnimationTime = 800;\nconst int _kMaxPageBackAnimationTime = 300;\nconst double _kMinFlingVelocity = 1.0;\n\nclass AppPageRoute<T> extends PageRoute<T> with _AppRouteTransitionMixin {\n  /// Construct a MaterialPageRoute whose contents are defined by [builder].\n  AppPageRoute({\n    required this.builder,\n    super.settings,\n    this.maintainState = true,\n    super.fullscreenDialog,\n    super.allowSnapshotting = true,\n    super.barrierDismissible = false,\n    this.enableIOSGesture = true,\n    this.preventRebuild = true,\n    this.isRoot = false,\n  }) {\n    assert(opaque);\n  }\n\n  /// Builds the primary contents of the route.\n  final WidgetBuilder builder;\n\n  @override\n  Widget buildContent(BuildContext context) {\n    return builder(context);\n  }\n\n  @override\n  final bool maintainState;\n\n  @override\n  String get debugLabel => '${super.debugLabel}(${settings.name})';\n\n  @override\n  final bool enableIOSGesture;\n\n  @override\n  final bool preventRebuild;\n\n  @override\n  final bool isRoot;\n\n  static void updateBackButton() {\n    Future.delayed(const Duration(milliseconds: 300), () {\n      StateController.findOrNull(tag: \"back_button\")?.update();\n    });\n  }\n}\n\nmixin _AppRouteTransitionMixin<T> on PageRoute<T> {\n  /// Builds the primary contents of the route.\n  @protected\n  Widget buildContent(BuildContext context);\n\n  @override\n  Duration get transitionDuration => const Duration(milliseconds: 300);\n\n  @override\n  Color? get barrierColor => null;\n\n  @override\n  String? get barrierLabel => null;\n\n  @override\n  bool canTransitionTo(TransitionRoute<dynamic> nextRoute) {\n    // Don't perform outgoing animation if the next route is a fullscreen dialog.\n    return nextRoute is PageRoute && !nextRoute.fullscreenDialog;\n  }\n\n  bool get enableIOSGesture;\n\n  bool get preventRebuild;\n\n  Widget? _child;\n\n  bool get isRoot;\n\n  @override\n  Widget buildPage(\n    BuildContext context,\n    Animation<double> animation,\n    Animation<double> secondaryAnimation,\n  ) {\n    Widget result;\n\n    if (preventRebuild) {\n      result = _child ?? (_child = buildContent(context));\n    } else {\n      result = buildContent(context);\n    }\n\n    return Semantics(\n      scopesRoute: true,\n      explicitChildNodes: true,\n      child: result,\n    );\n  }\n\n  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {\n    if (route.isFirst ||\n        route.willHandlePopInternally ||\n        route.popDisposition == RoutePopDisposition.doNotPop ||\n        route.fullscreenDialog ||\n        route.animation!.status != AnimationStatus.completed ||\n        route.secondaryAnimation!.status != AnimationStatus.dismissed ||\n        route.navigator!.userGestureInProgress) {\n      return false;\n    }\n\n    return true;\n  }\n\n  @override\n  Widget buildTransitions(BuildContext context, Animation<double> animation,\n      Animation<double> secondaryAnimation, Widget child) {\n    child = ColoredBox(\n      color: FluentTheme.of(context).micaBackgroundColor,\n      child: child,\n    );\n\n    if (isRoot) {\n      child = EntrancePageTransition(\n        animation: CurvedAnimation(\n          parent: animation,\n          curve: FluentTheme.of(context).animationCurve,\n        ),\n        child: enableIOSGesture && App.isIOS\n            ? IOSBackGestureDetector(\n                gestureWidth: _kBackGestureWidth,\n                enabledCallback: () => _isPopGestureEnabled<T>(this),\n                onStartPopGesture: () => _startPopGesture(this),\n                child: child)\n            : child,\n      );\n    } else {\n      if (App.isAndroid) {\n        child = const PredictiveBackPageTransitionsBuilder().buildTransitions(\n          this, \n          context, \n          animation, \n          secondaryAnimation, \n          child,\n        );\n      } else {\n        child = DrillInPageTransition(\n          animation: CurvedAnimation(\n            parent: animation,\n            curve: FluentTheme\n                .of(context)\n                .animationCurve,\n          ),\n          child: enableIOSGesture && App.isIOS\n              ? IOSBackGestureDetector(\n              gestureWidth: _kBackGestureWidth,\n              enabledCallback: () => _isPopGestureEnabled<T>(this),\n              onStartPopGesture: () => _startPopGesture(this),\n              child: child)\n              : child,\n        );\n      }\n    }\n\n    return child;\n  }\n\n  IOSBackGestureController _startPopGesture(PageRoute<T> route) {\n    return IOSBackGestureController(route.controller!, route.navigator!);\n  }\n}\n\nclass IOSBackGestureController {\n  final AnimationController controller;\n\n  final NavigatorState navigator;\n\n  IOSBackGestureController(this.controller, this.navigator) {\n    navigator.didStartUserGesture();\n  }\n\n  void dragEnd(double velocity) {\n    const Curve animationCurve = Curves.fastLinearToSlowEaseIn;\n    final bool animateForward;\n\n    if (velocity.abs() >= _kMinFlingVelocity) {\n      animateForward = velocity <= 0;\n    } else {\n      animateForward = controller.value > 0.5;\n    }\n\n    if (animateForward) {\n      final droppedPageForwardAnimationTime = min(\n        lerpDouble(\n                _kMaxDroppedSwipePageForwardAnimationTime, 0, controller.value)!\n            .floor(),\n        _kMaxPageBackAnimationTime,\n      );\n      controller.animateTo(1.0,\n          duration: Duration(milliseconds: droppedPageForwardAnimationTime),\n          curve: animationCurve);\n    } else {\n      navigator.pop();\n      if (controller.isAnimating) {\n        final droppedPageBackAnimationTime = lerpDouble(\n                0, _kMaxDroppedSwipePageForwardAnimationTime, controller.value)!\n            .floor();\n        controller.animateBack(0.0,\n            duration: Duration(milliseconds: droppedPageBackAnimationTime),\n            curve: animationCurve);\n      }\n    }\n\n    if (controller.isAnimating) {\n      late AnimationStatusListener animationStatusCallback;\n      animationStatusCallback = (status) {\n        navigator.didStopUserGesture();\n        controller.removeStatusListener(animationStatusCallback);\n      };\n      controller.addStatusListener(animationStatusCallback);\n    } else {\n      navigator.didStopUserGesture();\n    }\n  }\n\n  void dragUpdate(double delta) {\n    controller.value -= delta;\n  }\n}\n\nclass IOSBackGestureDetector extends StatefulWidget {\n  const IOSBackGestureDetector(\n      {required this.enabledCallback,\n      required this.child,\n      required this.gestureWidth,\n      required this.onStartPopGesture,\n      super.key});\n\n  final double gestureWidth;\n\n  final bool Function() enabledCallback;\n\n  final IOSBackGestureController Function() onStartPopGesture;\n\n  final Widget child;\n\n  @override\n  State<IOSBackGestureDetector> createState() => _IOSBackGestureDetectorState();\n}\n\nclass _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {\n  IOSBackGestureController? _backGestureController;\n\n  late HorizontalDragGestureRecognizer _recognizer;\n\n  @override\n  void dispose() {\n    _recognizer.dispose();\n    super.dispose();\n  }\n\n  @override\n  void initState() {\n    super.initState();\n    _recognizer = HorizontalDragGestureRecognizer(debugOwner: this)\n      ..onStart = _handleDragStart\n      ..onUpdate = _handleDragUpdate\n      ..onEnd = _handleDragEnd\n      ..onCancel = _handleDragCancel;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var dragAreaWidth = Directionality.of(context) == TextDirection.ltr\n        ? MediaQuery.of(context).padding.left\n        : MediaQuery.of(context).padding.right;\n    dragAreaWidth = max(dragAreaWidth, widget.gestureWidth);\n    return Stack(\n      fit: StackFit.passthrough,\n      children: <Widget>[\n        widget.child,\n        Positioned(\n          width: dragAreaWidth,\n          top: 0.0,\n          bottom: 0.0,\n          left: 0,\n          child: Listener(\n            onPointerDown: _handlePointerDown,\n            behavior: HitTestBehavior.translucent,\n          ),\n        ),\n      ],\n    );\n  }\n\n  void _handlePointerDown(PointerDownEvent event) {\n    if (widget.enabledCallback()) _recognizer.addPointer(event);\n  }\n\n  void _handleDragCancel() {\n    assert(mounted);\n    _backGestureController?.dragEnd(0.0);\n    _backGestureController = null;\n  }\n\n  double _convertToLogical(double value) {\n    switch (Directionality.of(context)) {\n      case TextDirection.rtl:\n        return -value;\n      case TextDirection.ltr:\n        return value;\n    }\n  }\n\n  void _handleDragEnd(DragEndDetails details) {\n    assert(mounted);\n    assert(_backGestureController != null);\n    _backGestureController!.dragEnd(_convertToLogical(\n        details.velocity.pixelsPerSecond.dx / context.size!.width));\n    _backGestureController = null;\n  }\n\n  void _handleDragStart(DragStartDetails details) {\n    assert(mounted);\n    assert(_backGestureController == null);\n    _backGestureController = widget.onStartPopGesture();\n  }\n\n  void _handleDragUpdate(DragUpdateDetails details) {\n    assert(mounted);\n    assert(_backGestureController != null);\n    _backGestureController!.dragUpdate(\n        _convertToLogical(details.primaryDelta! / context.size!.width));\n  }\n}\n\nconst _kSideBarWidth = 420.0;\n\nclass SideBarRoute<T> extends PopupRoute<T> {\n  SideBarRoute(this.child);\n\n  final Widget child;\n\n  @override\n  Color? get barrierColor => Colors.transparent;\n\n  @override\n  bool get barrierDismissible => true;\n\n  @override\n  String? get barrierLabel => \"side bar\";\n\n  @override\n  Widget buildPage(BuildContext context, Animation<double> animation,\n      Animation<double> secondaryAnimation) {\n    return Align(\n      alignment: Alignment.centerRight,\n      child: Stack(\n        children: [\n          Positioned(\n            right: 0,\n            top: 0,\n            bottom: 0,\n            child: Container(\n              decoration: BoxDecoration(\n                  color: FluentTheme.of(context)\n                      .micaBackgroundColor\n                      .toOpacity(0.98),\n                  borderRadius: const BorderRadius.only(\n                      topLeft: Radius.circular(4),\n                      bottomLeft: Radius.circular(4))),\n              constraints: BoxConstraints(\n                  maxWidth:\n                      min(_kSideBarWidth, MediaQuery.of(context).size.width)),\n              width: double.infinity,\n              child: child,\n            ),\n          )\n        ],\n      ),\n    );\n  }\n\n  @override\n  Duration get transitionDuration => const Duration(milliseconds: 200);\n\n  static bool _isPopGestureEnabled<T>(PopupRoute<T> route) {\n    if (route.isFirst ||\n        route.willHandlePopInternally ||\n        route.popDisposition == RoutePopDisposition.doNotPop ||\n        route.animation!.status != AnimationStatus.completed ||\n        route.secondaryAnimation!.status != AnimationStatus.dismissed ||\n        route.navigator!.userGestureInProgress) {\n      return false;\n    }\n\n    return true;\n  }\n\n  bool get enableIOSGesture => true;\n\n  @override\n  Widget buildTransitions(BuildContext context, Animation<double> animation,\n      Animation<double> secondaryAnimation, Widget child) {\n    var offset =\n        Tween<Offset>(begin: const Offset(1, 0), end: const Offset(0, 0));\n    return SlideTransition(\n      position: offset.animate(CurvedAnimation(\n        parent: animation,\n        curve: Curves.fastOutSlowIn,\n      )),\n      child: enableIOSGesture\n          ? IOSBackGestureDetector(\n              gestureWidth: _kBackGestureWidth,\n              enabledCallback: () => _isPopGestureEnabled<T>(this),\n              onStartPopGesture: () => _startPopGesture(this),\n              child: child)\n          : child,\n    );\n  }\n\n  IOSBackGestureController _startPopGesture(PopupRoute<T> route) {\n    return IOSBackGestureController(route.controller!, route.navigator!);\n  }\n}\n\nclass EntrancePageTransition extends StatelessWidget {\n  /// Creates an entrance page transition\n  const EntrancePageTransition({\n    super.key,\n    required this.child,\n    required this.animation,\n  });\n\n  /// The widget to be animated\n  final Widget child;\n\n  /// The animation to drive this transition\n  final Animation<double> animation;\n\n  @override\n  Widget build(BuildContext context) {\n    return SlideTransition(\n      position: Tween<Offset>(\n        begin: const Offset(0, 0.1),\n        end: Offset.zero,\n      ).animate(animation),\n      child: FadeTransition(\n        opacity: animation,\n        child: child,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/search_field.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\n\nclass AutoCompleteItem {\n  final String title;\n  final String? subtitle;\n  final VoidCallback onTap;\n\n  const AutoCompleteItem({\n    required this.title,\n    this.subtitle,\n    required this.onTap,\n  });\n}\n\nclass AutoCompleteData {\n  final List<AutoCompleteItem> items;\n  final bool isLoading;\n\n  const AutoCompleteData({\n    this.items = const <AutoCompleteItem>[],\n    this.isLoading = false,\n  });\n}\n\nclass SearchField extends StatefulWidget {\n  const SearchField({\n    super.key,\n    this.autoCompleteItems = const <AutoCompleteItem>[],\n    this.isLoadingAutoCompleteItems = false,\n    this.enableAutoComplete = true,\n    this.textEditingController,\n    this.placeholder,\n    this.leading,\n    this.trailing,\n    this.foregroundDecoration,\n    this.onChanged,\n    this.onSubmitted,\n    this.padding,\n    this.focusNode,\n    this.autoCompleteNoResultsText,\n  });\n\n  final List<AutoCompleteItem> autoCompleteItems;\n\n  final bool isLoadingAutoCompleteItems;\n\n  final bool enableAutoComplete;\n\n  final TextEditingController? textEditingController;\n\n  final String? placeholder;\n\n  final Widget? leading;\n\n  final Widget? trailing;\n\n  final WidgetStatePropertyAll<BoxDecoration>? foregroundDecoration;\n\n  final void Function(String)? onChanged;\n\n  final void Function(String)? onSubmitted;\n\n  final EdgeInsets? padding;\n\n  final FocusNode? focusNode;\n\n  final String? autoCompleteNoResultsText;\n\n  @override\n  State<SearchField> createState() => _SearchFieldState();\n}\n\nclass _SearchFieldState extends State<SearchField> with TickerProviderStateMixin {\n  late final ValueNotifier<AutoCompleteData> autoCompleteItems;\n\n  late final FocusNode focusNode;\n\n  final boxKey = GlobalKey();\n\n  OverlayEntry? _overlayEntry;\n\n  AnimationController? _animationController;\n  Animation<double>? _fadeAnimation;\n\n  @override\n  void initState() {\n    autoCompleteItems = ValueNotifier(AutoCompleteData(\n      items: widget.autoCompleteItems,\n      isLoading: widget.isLoadingAutoCompleteItems,\n    ));\n    focusNode = widget.focusNode ?? FocusNode();\n    focusNode.addListener(onfocusChange);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _animationController?.dispose();\n    focusNode.removeListener(onfocusChange);\n    if (widget.focusNode == null) {\n      focusNode.dispose();\n    }\n    super.dispose();\n  }\n\n  @override\n  void didUpdateWidget(covariant SearchField oldWidget) {\n    if (widget.autoCompleteItems != oldWidget.autoCompleteItems ||\n        widget.isLoadingAutoCompleteItems !=\n            oldWidget.isLoadingAutoCompleteItems) {\n      Future.microtask(() {\n        autoCompleteItems.value = AutoCompleteData(\n          items: widget.autoCompleteItems,\n          isLoading: widget.isLoadingAutoCompleteItems,\n        );\n      });\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  void onfocusChange() {\n    if (focusNode.hasFocus && widget.enableAutoComplete) {\n      final box = context.findRenderObject() as RenderBox?;\n      if (box == null) return;\n      final overlay = Overlay.of(context);\n      final position = box.localToGlobal(\n        Offset.zero,\n        ancestor: overlay.context.findRenderObject(),\n      );\n\n      if (_overlayEntry != null) {\n        _removeOverlayWithAnimation();\n      }\n\n      _animationController = AnimationController(\n        duration: const Duration(milliseconds: 200),\n        vsync: this,\n      );\n\n      _fadeAnimation = Tween<double>(\n        begin: 0.0,\n        end: 1.0,\n      ).animate(CurvedAnimation(\n        parent: _animationController!,\n        curve: Curves.easeOut,\n      ));\n\n      _overlayEntry = OverlayEntry(\n        builder: (context) {\n          return Positioned(\n            left: position.dx,\n            width: box.size.width,\n            top: position.dy + box.size.height,\n            child: _AnimatedOverlayWrapper(\n              animation: _fadeAnimation!,\n              child: _AutoCompleteOverlay(\n                data: autoCompleteItems,\n                noResultsText: widget.autoCompleteNoResultsText,\n              ),\n            ),\n          );\n        },\n      );\n\n      overlay.insert(_overlayEntry!);\n      _animationController!.forward();\n    } else {\n      _removeOverlayWithAnimation();\n    }\n  }\n\n  void _removeOverlayWithAnimation() {\n    if (_overlayEntry != null && _animationController != null) {\n      _animationController!.reverse().then((_) {\n        _overlayEntry?.remove();\n        _overlayEntry = null;\n        _animationController?.dispose();\n        _animationController = null;\n        _fadeAnimation = null;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextBox(\n      controller: widget.textEditingController,\n      key: boxKey,\n      focusNode: focusNode,\n      padding: const EdgeInsets.symmetric(horizontal: 12),\n      placeholder: widget.placeholder,\n      onChanged: widget.onChanged,\n      onSubmitted: widget.onSubmitted,\n      foregroundDecoration: widget.foregroundDecoration,\n      prefix: widget.leading,\n      suffix: widget.trailing,\n    );\n  }\n}\n\nclass _AutoCompleteOverlay extends StatefulWidget {\n  const _AutoCompleteOverlay({required this.data, this.noResultsText});\n\n  final ValueNotifier<AutoCompleteData> data;\n\n  final String? noResultsText;\n\n  @override\n  State<_AutoCompleteOverlay> createState() => _AutoCompleteOverlayState();\n}\n\nclass _AutoCompleteOverlayState extends State<_AutoCompleteOverlay> {\n  late final notifier = widget.data;\n\n  var items = <AutoCompleteItem>[];\n\n  var isLoading = false;\n\n  @override\n  void initState() {\n    items = notifier.value.items;\n    isLoading = notifier.value.isLoading;\n    notifier.addListener(onItemsChanged);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    notifier.removeListener(onItemsChanged);\n    super.dispose();\n  }\n\n  void onItemsChanged() {\n    setState(() {\n      items = notifier.value.items;\n      isLoading = notifier.value.isLoading;\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var items = List<AutoCompleteItem>.from(this.items);\n\n    Widget? content;\n\n    if (isLoading) {\n      content = SizedBox(\n        height: 44,\n        child: Center(\n          child: ProgressRing(\n            activeColor: FluentTheme.of(context).accentColor,\n            strokeWidth: 2,\n          ).fixWidth(24).fixHeight(24),\n        ),\n      );\n    } else if (items.isEmpty) {\n      content = ListTile(\n        title: Text(widget.noResultsText ?? 'No results found'),\n        onPressed: () {},\n      );\n    } else {\n      if (items.length > 8) {\n        items = items.sublist(0, 8);\n      }\n      content = Column(\n        mainAxisSize: MainAxisSize.min,\n        children: items.map((item) {\n          return ListTile(\n            title: Text(item.title),\n            subtitle: item.subtitle != null ? Text(item.subtitle!) : null,\n            onPressed: item.onTap,\n          );\n        }).toList(),\n      );\n    }\n\n    return Card(\n      backgroundColor: FluentTheme.of(context).micaBackgroundColor,\n      child: AnimatedSize(\n        alignment: Alignment.topCenter,\n        duration: const Duration(milliseconds: 160),\n        child: content,\n      ),\n    );\n  }\n}\n\nclass _AnimatedOverlayWrapper extends StatelessWidget {\n  const _AnimatedOverlayWrapper({\n    required this.animation,\n    required this.child,\n  });\n\n  final Animation<double> animation;\n  final Widget child;\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: animation,\n      builder: (context, child) {\n        return FadeTransition(\n          opacity: animation,\n          child: Transform.scale(\n            scale: 0.9 + (0.1 * animation.value),\n            alignment: Alignment.topCenter,\n            child: child,\n          ),\n        );\n      },\n      child: child,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/segmented_button.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\n\nimport 'md.dart';\n\nclass SegmentedButton<T> extends StatelessWidget {\n  const SegmentedButton(\n      {required this.options,\n      required this.value,\n      required this.onPressed,\n      super.key});\n\n  final List<SegmentedButtonOption<T>> options;\n\n  final T value;\n\n  final void Function(T key) onPressed;\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n      alignment: Alignment.centerLeft,\n      child: Card(\n        padding: EdgeInsets.zero,\n        child: SizedBox(\n          height: 28,\n          child: Row(\n            mainAxisSize: MainAxisSize.min,\n            children: options.map((e) => buildButton(e)).toList(),\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildButton(SegmentedButtonOption<T> e) {\n    bool active = value == e.key;\n    return HoverButton(\n        cursor: active ? MouseCursor.defer : SystemMouseCursors.click,\n        onPressed: () => onPressed(e.key),\n        builder: (context, states) {\n          var textColor = active ? null : ColorScheme.of(context).outline;\n          var backgroundColor = active ? null : WidgetStateProperty.resolveWith((states) {\n            return ButtonThemeData.buttonColor(context, states);\n          }).resolve(states);\n\n          return Container(\n            decoration: BoxDecoration(\n                color: backgroundColor,\n                border: e != options.last\n                    ? Border(\n                        right: BorderSide(\n                            width: 0.6,\n                            color: ColorScheme.of(context).outlineVariant))\n                    : null),\n            child: Center(\n              child: Text(e.text,\n                      style: TextStyle(\n                          color: textColor, fontWeight: FontWeight.w500))\n                  .paddingHorizontal(12),\n            ),\n          );\n        });\n  }\n}\n\nclass SegmentedButtonOption<T> {\n  final T key;\n  final String text;\n\n  const SegmentedButtonOption(this.key, this.text);\n}\n"
  },
  {
    "path": "lib/components/title_bar.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\n\nclass TitleBar extends StatelessWidget {\n  const TitleBar({required this.title, this.action, super.key});\n\n  final String title;\n\n  final Widget? action;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      child: Row(\n        children: [\n          Text(title,\n            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),\n          const Spacer(),\n          if(action != null)\n            action!\n        ],\n      ).paddingHorizontal(16).paddingVertical(8),\n    );\n  }\n}\n\nclass SliverTitleBar extends StatelessWidget {\n  const SliverTitleBar({required this.title, this.action, super.key});\n\n  final String title;\n\n  final Widget? action;\n\n\n  @override\n  Widget build(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        child: Row(\n          children: [\n            Text(title,\n              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),),\n            const Spacer(),\n            if(action != null)\n              action!\n          ],\n        ).paddingHorizontal(16).paddingVertical(8),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/components/ugoira.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:intl/intl.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/network/network.dart';\n\nimport '../foundation/cache_manager.dart';\nimport '../network/app_dio.dart';\nimport 'dart:ui' as ui;\n\nclass UgoiraWidget extends StatefulWidget {\n  const UgoiraWidget({super.key, required this.id, required this.previewImage,\n    required this.width, required this.height});\n\n  final String id;\n\n  final ImageProvider previewImage;\n\n  final double width;\n\n  final double height;\n\n  @override\n  State<UgoiraWidget> createState() => _UgoiraWidgetState();\n}\n\nclass _UgoiraWidgetState extends State<UgoiraWidget> {\n  _UgoiraMetadata? _metadata;\n\n  bool _loading = false;\n\n  bool _finished = false;\n\n  bool _error = false;\n\n  int expectedBytes = 1;\n\n  int receivedBytes = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return SizedBox(\n      width: widget.width,\n      height: widget.height,\n      child: !_finished\n        ? buildPreview()\n        : _UgoiraAnimation(metadata: _metadata!, key: Key(widget.id),),\n    );\n  }\n\n  Widget buildPreview() {\n    return Stack(\n      children: [\n        Positioned.fill(\n          child: ClipRRect(\n            borderRadius: BorderRadius.circular(4),\n            child: Image(\n              image: widget.previewImage,\n              fit: BoxFit.cover,\n            ),\n          ),\n        ),\n        if(_error)\n          const Positioned.fill(\n            child: Center(\n              child: Icon(\n                MdIcons.error_outline,\n                size: 36,\n              ),\n            )),\n        if(!_loading)\n          Positioned.fill(\n            child: GestureDetector(\n              onTap: load,\n              child: const Center(\n                child: Icon(\n                  MdIcons.play_circle_outline,\n                  size: 36,\n                ),\n              ),\n            ),\n          )\n        else\n          Center(\n            child: ProgressRing(value: (receivedBytes / expectedBytes) * 100,),\n          ),\n      ],\n    );\n  }\n\n  void load() async {\n    setState(() {\n      _loading = true;\n    });\n    var res0 = await Network().apiGet('/v1/ugoira/metadata?illust_id=${widget.id}');\n    if(res0.error) {\n      setState(() {\n        _error = true;\n        _loading = false;\n      });\n      return;\n    }\n    var json = res0.data;\n    _metadata = _UgoiraMetadata(\n      url: json[\"ugoira_metadata\"][\"zip_urls\"][\"medium\"],\n      frames: (json[\"ugoira_metadata\"][\"frames\"] as List).map<_UgoiraFrame>((e) => _UgoiraFrame(\n        delay: e[\"delay\"],\n        fileName: e[\"file\"],\n      )).toList(),\n    );\n    try {\n      var key = \"ugoira_${widget.id}\";\n      var cached = await CacheManager().findCache(key);\n      if(cached != null) {\n        await extract(cached);\n        return;\n      }\n      var dio = AppDio();\n      final time = DateFormat(\"yyyy-MM-dd'T'HH:mm:ss'+00:00'\").format(DateTime.now());\n      final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();\n      var res = await dio.get<ResponseBody>(\n          _metadata!.url,\n          options: Options(\n              responseType: ResponseType.stream,\n              validateStatus: (status) => status != null && status < 500,\n              headers: {\n                \"referer\": \"https://app-api.pixiv.net/\",\n                \"user-agent\": \"PixivAndroidApp/5.0.234 (Android 14; Pixes)\",\n                \"x-client-time\": time,\n                \"x-client-hash\": hash,\n                \"accept-enconding\": \"gzip\",\n              }\n          )\n      );\n      if(res.statusCode != 200) {\n        throw \"Failed to load image: ${res.statusCode}\";\n      }\n      expectedBytes = int.parse(res.headers.value(\"content-length\") ?? \"1\");\n      var cachingFile = await CacheManager().openWrite(key);\n      await for (var chunk in res.data!.stream) {\n        await cachingFile.writeBytes(chunk);\n        setState(() {\n          receivedBytes += chunk.length;\n          if(receivedBytes > expectedBytes) {\n            expectedBytes = receivedBytes + 1;\n          }\n        });\n      }\n      await cachingFile.close();\n      await extract(cachingFile.file.path);\n    }\n    catch(e) {\n      setState(() {\n        _error = true;\n        _loading = false;\n      });\n      return;\n    }\n  }\n\n  Future<void> extract(String filePath) async{\n    var zip = ZipDecoder().decodeBytes(await File(filePath).readAsBytes());\n    for(var file in zip) {\n      if(file.isFile) {\n        var frame = _metadata!.frames.firstWhere((element) => element.fileName == file.name);\n        frame.data = await decodeImageFromList(file.content);\n      }\n    }\n    zip.clear();\n    setState(() {\n      _loading = false;\n      _finished = true;\n    });\n  }\n}\n\n\nclass _UgoiraAnimation extends StatefulWidget {\n  const _UgoiraAnimation({super.key, required this.metadata});\n\n  final _UgoiraMetadata metadata;\n\n  @override\n  State<_UgoiraAnimation> createState() => _UgoiraAnimationState();\n}\n\nclass _UgoiraAnimationState extends State<_UgoiraAnimation> with SingleTickerProviderStateMixin {\n  late AnimationController _controller;\n\n  @override\n  void initState() {\n    super.initState();\n    final totalDuration = widget.metadata.frames.fold<int>(\n        0, (previousValue, element) => previousValue + element.delay);\n    _controller = AnimationController(\n      vsync: this,\n      duration: Duration(milliseconds: totalDuration),\n      value: 0,\n      lowerBound: 0,\n      upperBound: widget.metadata.frames.length.toDouble(),\n    );\n    _controller.repeat();\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: _controller,\n      builder: (context, child) {\n        final frame = widget.metadata.frames[_controller.value.toInt()];\n        return CustomPaint(\n          painter: _ImagePainter(frame.data!),\n        );\n      },\n    );\n  }\n}\n\nclass _UgoiraMetadata {\n  final String url;\n  final List<_UgoiraFrame> frames;\n\n  _UgoiraMetadata({required this.url, required this.frames});\n}\n\nclass _UgoiraFrame {\n  final int delay;\n  final String fileName;\n  ui.Image? data;\n\n  _UgoiraFrame({required this.delay, required this.fileName});\n}\n\nclass _ImagePainter extends CustomPainter {\n  final ui.Image data;\n\n  _ImagePainter(this.data);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    // 覆盖整个画布\n    Rect rect = Offset.zero & size;\n    canvas.drawImageRect(\n      data,\n      Rect.fromLTRB(0, 0, data.width.toDouble(), data.height.toDouble()),\n      rect,\n      Paint()..filterQuality = FilterQuality.medium\n    );\n  }\n\n  @override\n  bool shouldRepaint(covariant CustomPainter oldDelegate) {\n    return data != (oldDelegate as _ImagePainter).data;\n  }\n}\n"
  },
  {
    "path": "lib/components/user_preview.dart",
    "content": "import 'dart:math';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../network/network.dart';\nimport 'md.dart';\n\ntypedef UpdateFollowCallback = void Function(bool isFollowed);\n\nclass UserPreviewWidget extends StatefulWidget {\n  const UserPreviewWidget(this.user, {super.key});\n\n  final UserPreview user;\n\n  static Map<String, UpdateFollowCallback> followCallbacks = {};\n\n  @override\n  State<UserPreviewWidget> createState() => _UserPreviewWidgetState();\n}\n\nclass _UserPreviewWidgetState extends State<UserPreviewWidget> {\n  @override\n  void initState() {\n    UserPreviewWidget.followCallbacks[widget.user.id.toString()] = (v) {\n      setState(() {\n        widget.user.isFollowed = v;\n      });\n    };\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    UserPreviewWidget.followCallbacks.remove(widget.user.id.toString());\n    super.dispose();\n  }\n\n  bool isFollowing = false;\n\n  void follow() async {\n    if (isFollowing) return;\n    setState(() {\n      isFollowing = true;\n    });\n    var method = widget.user.isFollowed ? \"delete\" : \"add\";\n    var res = await Network().follow(widget.user.id.toString(), method);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      widget.user.isFollowed = !widget.user.isFollowed;\n    }\n    setState(() {\n      isFollowing = false;\n    });\n    UserInfoPage.followCallbacks[widget.user.id.toString()]\n        ?.call(widget.user.isFollowed);\n    IllustPage.updateFollow(widget.user.id.toString(), widget.user.isFollowed);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Card(\n      margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n      child: GestureDetector(\n        onTap: () => context.to(() => UserInfoPage(widget.user.id.toString())),\n        behavior: HitTestBehavior.translucent,\n        child: SizedBox.expand(\n          child: Row(\n            children: [\n              SizedBox(\n                width: 64,\n                height: 64,\n                child: ClipRRect(\n                  borderRadius: BorderRadius.circular(64),\n                  child: ColoredBox(\n                    color: ColorScheme.of(context).secondaryContainer,\n                    child: AnimatedImage(\n                      image: CachedImageProvider(widget.user.avatar),\n                      fit: BoxFit.cover,\n                      filterQuality: FilterQuality.medium,\n                    ),\n                  ),\n                ),\n              ),\n              const SizedBox(\n                width: 12,\n              ),\n              SizedBox(\n                width: 96,\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    const Spacer(),\n                    Text(widget.user.name,\n                        maxLines: 1,\n                        style: const TextStyle(\n                            fontSize: 16, fontWeight: FontWeight.bold)),\n                    const SizedBox(\n                      height: 12,\n                    ),\n                    Row(\n                      children: [\n                        if (isFollowing)\n                          Button(\n                              onPressed: follow,\n                              child: const SizedBox(\n                                width: 42,\n                                height: 24,\n                                child: Center(\n                                  child: SizedBox.square(\n                                    dimension: 18,\n                                    child: ProgressRing(\n                                      strokeWidth: 2,\n                                    ),\n                                  ),\n                                ),\n                              ))\n                        else if (!widget.user.isFollowed)\n                          Button(onPressed: follow, child: Text(\"Follow\".tl))\n                        else\n                          Button(\n                            onPressed: follow,\n                            child: Text(\n                              \"Unfollow\".tl,\n                              style: TextStyle(\n                                  color: ColorScheme.of(context).error),\n                            ),\n                          ),\n                      ],\n                    ),\n                    const Spacer(),\n                  ],\n                ),\n              ),\n              Expanded(\n                child: LayoutBuilder(\n                  builder: (context, constraints) {\n                    var count = constraints.maxWidth.toInt() ~/ 96;\n                    var images = List.generate(\n                        min(count, widget.user.artworks.length),\n                        (index) => buildIllust(widget.user.artworks[index]));\n                    return Row(\n                      children: images,\n                    );\n                  },\n                ),\n              ),\n              const Icon(\n                FluentIcons.chevron_right,\n                size: 14,\n              )\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  Widget buildIllust(Illust illust) {\n    return SizedBox(\n      width: 96,\n      height: double.infinity,\n      child: Padding(\n        padding: const EdgeInsets.symmetric(horizontal: 8),\n        child: ClipRRect(\n          borderRadius: BorderRadius.circular(4),\n          child: ColoredBox(\n            color: ColorScheme.of(context).secondaryContainer,\n            child: AnimatedImage(\n              width: double.infinity,\n              height: double.infinity,\n              fit: BoxFit.cover,\n              filterQuality: FilterQuality.medium,\n              image: CachedImageProvider(illust.images.first.medium),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/foundation/app.dart",
    "content": "import 'dart:io';\nimport 'dart:ui';\n\nimport 'package:device_info_plus/device_info_plus.dart';\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:path_provider/path_provider.dart';\n\nimport '../appdata.dart';\n\nexport \"widget_utils.dart\";\nexport \"state_controller.dart\";\nexport \"navigation.dart\";\n\nclass _App {\n  final version = \"1.2.1\";\n\n  bool get isAndroid => Platform.isAndroid;\n  bool get isIOS => Platform.isIOS;\n  bool get isWindows => Platform.isWindows;\n  int? _windowsVersion;\n  int get windowsVersion => _windowsVersion!;\n  bool get isLinux => Platform.isLinux;\n  bool get isMacOS => Platform.isMacOS;\n  bool get isDesktop =>\n      Platform.isWindows || Platform.isLinux || Platform.isMacOS;\n  bool get isMobile => Platform.isAndroid || Platform.isIOS;\n\n  Locale get locale {\n    if (appdata.settings[\"language\"] != \"System\") {\n      return switch (appdata.settings[\"language\"]) {\n        \"English\" => const Locale(\"en\"),\n        \"简体中文\" => const Locale(\"zh\", \"CN\"),\n        \"繁體中文\" => const Locale(\"zh\", \"TW\"),\n        _ => const Locale(\"en\"),\n      };\n    }\n    Locale deviceLocale = PlatformDispatcher.instance.locale;\n    if (deviceLocale.languageCode == \"zh\" &&\n        deviceLocale.scriptCode == \"Hant\") {\n      deviceLocale = const Locale(\"zh\", \"TW\");\n    }\n    return deviceLocale;\n  }\n\n  late String dataPath;\n  late String cachePath;\n\n  init() async {\n    cachePath = (await getApplicationCacheDirectory()).path;\n    dataPath = (await getApplicationSupportDirectory()).path;\n    if (App.isWindows) {\n      final deviceInfoPlugin = DeviceInfoPlugin();\n      final deviceInfo = await deviceInfoPlugin.windowsInfo;\n      if (deviceInfo.majorVersion <= 6) {\n        if (deviceInfo.minorVersion < 2) {\n          _windowsVersion = 7;\n        } else {\n          _windowsVersion = 8;\n        }\n      } else if (deviceInfo.buildNumber < 22000) {\n        _windowsVersion = 10;\n      } else {\n        _windowsVersion = 11;\n      }\n    }\n  }\n\n  final rootNavigatorKey = GlobalKey<NavigatorState>();\n\n  GlobalKey<NavigatorState>? mainNavigatorKey;\n}\n\n// ignore: non_constant_identifier_names\nfinal App = _App();\n"
  },
  {
    "path": "lib/foundation/cache_manager.dart",
    "content": "import 'dart:io';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:pixes/utils/io.dart';\nimport 'package:sqlite3/sqlite3.dart';\n\nimport 'app.dart';\n\nclass CacheManager {\n  static String get cachePath => '${App.cachePath}/cache';\n\n  static CacheManager? instance;\n\n  late Database _db;\n\n  int? _currentSize;\n\n  /// size in bytes\n  int get currentSize => _currentSize ?? 0;\n\n  int dir = 0;\n\n  int _limitSize = 2 * 1024 * 1024 * 1024;\n\n  CacheManager._create(){\n    Directory(cachePath).createSync(recursive: true);\n    _db = sqlite3.open('${App.dataPath}/cache.db');\n    _db.execute('''\n      CREATE TABLE IF NOT EXISTS cache (\n        key TEXT PRIMARY KEY NOT NULL,\n        dir TEXT NOT NULL,\n        name TEXT NOT NULL,\n        expires INTEGER NOT NULL\n      )\n    ''');\n    compute((path) => Directory(path).size, cachePath)\n        .then((value) => _currentSize = value);\n  }\n\n  factory CacheManager() => instance ??= CacheManager._create();\n\n  /// set cache size limit in bytes\n  void setLimitSize(int size){\n    _limitSize = size;\n  }\n\n  Future<void> writeCache(String key, Uint8List data, [int duration = 7 * 24 * 60 * 60 * 1000]) async{\n    this.dir++;\n    this.dir %= 100;\n    var dir = this.dir;\n    var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();\n    var file = File('$cachePath/$dir/$name');\n    while(await file.exists()){\n      name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();\n      file = File('$cachePath/$dir/$name');\n    }\n    await file.create(recursive: true);\n    await file.writeAsBytes(data);\n    var expires = DateTime.now().millisecondsSinceEpoch + duration;\n    _db.execute('''\n      INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)\n    ''', [key, dir.toString(), name, expires]);\n    if(_currentSize != null) {\n      _currentSize = _currentSize! + data.length;\n    }\n    if(_currentSize != null && _currentSize! > _limitSize){\n      await checkCache();\n    }\n  }\n\n  Future<CachingFile> openWrite(String key) async{\n    this.dir++;\n    this.dir %= 100;\n    var dir = this.dir;\n    var name = md5.convert(Uint8List.fromList(key.codeUnits)).toString();\n    var file = File('$cachePath/$dir/$name');\n    while(await file.exists()){\n      name = md5.convert(Uint8List.fromList(name.codeUnits)).toString();\n      file = File('$cachePath/$dir/$name');\n    }\n    await file.create(recursive: true);\n    return CachingFile._(key, dir.toString(), name, file);\n  }\n\n  Future<String?> findCache(String key) async{\n    var res = _db.select('''\n      SELECT * FROM cache\n      WHERE key = ?\n    ''', [key]);\n    if(res.isEmpty){\n      return null;\n    }\n    var row = res.first;\n    var dir = row.values[1] as String;\n    var name = row.values[2] as String;\n    var file = File('$cachePath/$dir/$name');\n    if(await file.exists()){\n      return file.path;\n    }\n    return null;\n  }\n\n  bool _isChecking = false;\n\n  Future<void> checkCache() async{\n    if(_isChecking){\n      return;\n    }\n    _isChecking = true;\n    var res = _db.select('''\n      SELECT * FROM cache\n      WHERE expires < ?\n    ''', [DateTime.now().millisecondsSinceEpoch]);\n    for(var row in res){\n      var dir = row.values[1] as int;\n      var name = row.values[2] as String;\n      var file = File('$cachePath/$dir/$name');\n      if(await file.exists()){\n        await file.delete();\n      }\n    }\n    _db.execute('''\n      DELETE FROM cache\n      WHERE expires < ?\n    ''', [DateTime.now().millisecondsSinceEpoch]);\n\n    while(_currentSize != null && _currentSize! > _limitSize){\n      var res = _db.select('''\n        SELECT * FROM cache\n        ORDER BY time ASC\n        limit 10\n      ''');\n      for(var row in res){\n        var key = row.values[0] as String;\n        var dir = row.values[1] as int;\n        var name = row.values[2] as String;\n        var file = File('$cachePath/$dir/$name');\n        if(await file.exists()){\n          var size = await file.length();\n          await file.delete();\n          _db.execute('''\n            DELETE FROM cache\n            WHERE key = ?\n          ''', [key]);\n          _currentSize = _currentSize! - size;\n          if(_currentSize! <= _limitSize){\n            break;\n          }\n        } else {\n          _db.execute('''\n            DELETE FROM cache\n            WHERE key = ?\n          ''', [key]);\n        }\n      }\n    }\n    _isChecking = false;\n  }\n\n  Future<void> delete(String key) async{\n    var res = _db.select('''\n      SELECT * FROM cache\n      WHERE key = ?\n    ''', [key]);\n    if(res.isEmpty){\n      return;\n    }\n    var row = res.first;\n    var dir = row.values[1] as String;\n    var name = row.values[2] as String;\n    var file = File('$cachePath/$dir/$name');\n    var fileSize = 0;\n    if(await file.exists()){\n      fileSize = await file.length();\n      await file.delete();\n    }\n    _db.execute('''\n      DELETE FROM cache\n      WHERE key = ?\n    ''', [key]);\n    if(_currentSize != null) {\n      _currentSize = _currentSize! - fileSize;\n    }\n  }\n\n  Future<void> clear() async {\n    await Directory(cachePath).delete(recursive: true);\n    Directory(cachePath).createSync(recursive: true);\n    _db.execute('''\n      DELETE FROM cache\n    ''');\n    _currentSize = 0;\n  }\n\n  Future<void> deleteKeyword(String keyword) async{\n    var res = _db.select('''\n      SELECT * FROM cache\n      WHERE key LIKE ?\n    ''', ['%$keyword%']);\n    for(var row in res){\n      var key = row.values[0] as String;\n      var dir = row.values[1] as String;\n      var name = row.values[2] as String;\n      var file = File('$cachePath/$dir/$name');\n      var fileSize = 0;\n      if(await file.exists()){\n        fileSize = await file.length();\n        await file.delete();\n      }\n      _db.execute('''\n        DELETE FROM cache\n        WHERE key = ?\n      ''', [key]);\n      if(_currentSize != null) {\n        _currentSize = _currentSize! - fileSize;\n      }\n    }\n  }\n}\n\nclass CachingFile{\n  CachingFile._(this.key, this.dir, this.name, this.file);\n\n  final String key;\n\n  final String dir;\n\n  final String name;\n\n  final File file;\n\n  final List<int> _buffer = [];\n\n  Future<void> writeBytes(List<int> data) async{\n    _buffer.addAll(data);\n    if(_buffer.length > 1024 * 1024){\n      await file.writeAsBytes(_buffer, mode: FileMode.append);\n      _buffer.clear();\n    }\n  }\n\n  Future<void> close() async{\n    if(_buffer.isNotEmpty){\n      await file.writeAsBytes(_buffer, mode: FileMode.append);\n    }\n    CacheManager()._db.execute('''\n      INSERT OR REPLACE INTO cache (key, dir, name, expires) VALUES (?, ?, ?, ?)\n    ''', [key, dir, name, DateTime.now().millisecondsSinceEpoch + 7 * 24 * 60 * 60 * 1000]);\n  }\n\n  Future<void> cancel() async{\n    await file.deleteIfExists();\n  }\n}"
  },
  {
    "path": "lib/foundation/history.dart",
    "content": "import 'package:pixes/foundation/app.dart';\nimport 'package:sqlite3/sqlite3.dart';\nimport 'package:pixes/network/models.dart';\n\nclass IllustHistory {\n  final int id;\n  final String imgPath;\n  final DateTime time;\n  final int imageCount;\n  final bool isR18;\n  final bool isR18G;\n  final bool isAi;\n  final bool isGif;\n  final int width;\n  final int height;\n\n  IllustHistory(this.id, this.imgPath, this.time, this.imageCount, this.isR18,\n      this.isR18G, this.isAi, this.isGif, this.width, this.height);\n}\n\nclass HistoryManager {\n  static HistoryManager? instance;\n\n  factory HistoryManager() => instance ??= HistoryManager._create();\n\n  HistoryManager._create();\n\n  late Database _db;\n\n  init() {\n    _db = sqlite3.open(\"${App.dataPath}/history.db\");\n    _db.execute('''\n      create table if not exists history (\n        id integer primary key not null,\n        imgPath text not null,\n        time integer not null,\n        imageCount integer not null,\n        isR18 integer not null,\n        isR18g integer not null,\n        isAi integer not null,\n        isGif integer not null,\n        width integer not null,\n        height integer not null\n      )\n    ''');\n  }\n\n  void addHistory(Illust illust) {\n    var time = DateTime.now();\n    _db.execute('''\n      insert or replace into history (id, imgPath, time, imageCount, isR18, isR18g, isAi, isGif, width, height)\n      values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n    ''', [\n      illust.id,\n      illust.images.first.medium,\n      time.millisecondsSinceEpoch,\n      illust.pageCount,\n      illust.isR18 ? 1 : 0,\n      illust.isR18G ? 1 : 0,\n      illust.isAi ? 1 : 0,\n      illust.isUgoira ? 1 : 0,\n      illust.width,\n      illust.height\n    ]);\n    if(length > 1000) {\n      _db.execute('''\n        delete from history where id in (\n          select id from history order by time asc limit 100\n        )\n      ''');\n    }\n  }\n\n  List<IllustHistory> getHistories(int page) {\n    var rows = _db.select('''\n      select * from history order by time desc\n      limit 20 offset ? \n    ''', [(page - 1) * 20]);\n    List<IllustHistory> res = [];\n    for (var row in rows) {\n      res.add(IllustHistory(\n          row['id'],\n          row['imgPath'],\n          DateTime.fromMillisecondsSinceEpoch(row['time']),\n          row['imageCount'],\n          row['isR18'] == 1,\n          row['isR18g'] == 1,\n          row['isAi'] == 1,\n          row['isGif'] == 1,\n          row['width'],\n          row['height']));\n    }\n    return res;\n  }\n\n  int get length {\n    var rows = _db.select('''\n      select count(*) from history\n    ''');\n    return rows.first.values.first! as int;\n  }\n}\n"
  },
  {
    "path": "lib/foundation/image_provider.dart",
    "content": "import 'dart:async' show Future, StreamController, scheduleMicrotask;\nimport 'dart:convert';\nimport 'dart:io';\nimport 'dart:ui' as ui show Codec;\nimport 'dart:ui';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/material.dart';\nimport 'package:intl/intl.dart';\nimport 'package:pixes/network/app_dio.dart';\nimport 'package:pixes/network/network.dart';\n\nimport 'cache_manager.dart';\n\nclass BadRequestException implements Exception {\n  final String message;\n\n  BadRequestException(this.message);\n\n  @override\n  String toString() {\n    return message;\n  }\n}\n\nabstract class BaseImageProvider<T extends BaseImageProvider<T>>\n    extends ImageProvider<T> {\n  const BaseImageProvider();\n\n  @override\n  ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {\n    final chunkEvents = StreamController<ImageChunkEvent>();\n    return MultiFrameImageStreamCompleter(\n      codec: _loadBufferAsync(key, chunkEvents, decode),\n      chunkEvents: chunkEvents.stream,\n      scale: 1.0,\n      informationCollector: () sync* {\n        yield DiagnosticsProperty<ImageProvider>(\n          'Image provider: $this \\n Image key: $key',\n          this,\n          style: DiagnosticsTreeStyle.errorProperty,\n        );\n      },\n    );\n  }\n\n  Future<ui.Codec> _loadBufferAsync(\n    T key,\n    StreamController<ImageChunkEvent> chunkEvents,\n    ImageDecoderCallback decode,\n  ) async {\n    try {\n      int retryTime = 1;\n\n      bool stop = false;\n\n      chunkEvents.onCancel = () {\n        stop = true;\n      };\n\n      Uint8List? data;\n\n      while (data == null && !stop) {\n        try {\n          data = await load(chunkEvents);\n        } catch (e) {\n          if (e.toString().contains(\"Your IP address\")) {\n            rethrow;\n          }\n          if (e is BadRequestException) {\n            rethrow;\n          }\n          if (e.toString().contains(\"handshake\")) {\n            if (retryTime < 5) {\n              retryTime = 5;\n            }\n          }\n          retryTime <<= 1;\n          if (retryTime > (2 << 3) || stop) {\n            rethrow;\n          }\n          await Future.delayed(Duration(seconds: retryTime));\n        }\n      }\n\n      if (stop) {\n        throw Exception(\"Image loading is stopped\");\n      }\n\n      if (data!.isEmpty) {\n        throw Exception(\"Empty image data\");\n      }\n\n      try {\n        final buffer = await ImmutableBuffer.fromUint8List(data);\n        return await decode(buffer);\n      } catch (e) {\n        await CacheManager().delete(this.key);\n        Object error = e;\n        if (data.length < 200) {\n          // data is too short, it's likely that the data is text, not image\n          try {\n            var text = utf8.decoder.convert(data);\n            error = Exception(\"Expected image data, but got text: $text\");\n          } catch (e) {\n            // ignore\n          }\n        }\n        throw error;\n      }\n    } catch (e) {\n      scheduleMicrotask(() {\n        PaintingBinding.instance.imageCache.evict(key);\n      });\n      rethrow;\n    } finally {\n      chunkEvents.close();\n    }\n  }\n\n  Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);\n\n  String get key;\n\n  @override\n  bool operator ==(Object other) {\n    return other is BaseImageProvider<T> && key == other.key;\n  }\n\n  @override\n  int get hashCode => key.hashCode;\n\n  @override\n  String toString() {\n    return \"$runtimeType($key)\";\n  }\n}\n\ntypedef FileDecoderCallback = Future<ui.Codec> Function(Uint8List);\n\nclass CachedImageProvider extends BaseImageProvider<CachedImageProvider> {\n  final String url;\n\n  CachedImageProvider(this.url);\n\n  @override\n  String get key => url;\n\n  @override\n  Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {\n    chunkEvents.add(const ImageChunkEvent(\n      cumulativeBytesLoaded: 0,\n      expectedTotalBytes: 1,\n    ));\n    var cached = await CacheManager().findCache(key);\n    if (cached != null) {\n      chunkEvents.add(const ImageChunkEvent(\n        cumulativeBytesLoaded: 1,\n        expectedTotalBytes: 1,\n      ));\n      return await File(cached).readAsBytes();\n    }\n    var dio = AppDio();\n    final time =\n        DateFormat(\"yyyy-MM-dd'T'HH:mm:ss'+00:00'\").format(DateTime.now());\n    final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();\n    var res = await dio.get<ResponseBody>(url,\n        options: Options(\n            responseType: ResponseType.stream,\n            validateStatus: (status) => status != null && status < 500,\n            headers: {\n              \"referer\": \"https://app-api.pixiv.net/\",\n              \"user-agent\": \"PixivAndroidApp/5.0.234 (Android 14; Pixes)\",\n              \"x-client-time\": time,\n              \"x-client-hash\": hash,\n              \"accept-enconding\": \"gzip\",\n            }));\n    if (res.statusCode != 200) {\n      throw BadRequestException(\"Failed to load image: ${res.statusCode}\");\n    }\n    var data = <int>[];\n    var cachingFile = await CacheManager().openWrite(key);\n    await for (var chunk in res.data!.stream) {\n      var length = res.data!.contentLength + 1;\n      if (length < data.length) {\n        length = data.length + 1;\n      }\n      data.addAll(chunk);\n      await cachingFile.writeBytes(chunk);\n      chunkEvents.add(ImageChunkEvent(\n        cumulativeBytesLoaded: data.length,\n        expectedTotalBytes: length,\n      ));\n    }\n    await cachingFile.close();\n    return Uint8List.fromList(data);\n  }\n\n  @override\n  Future<CachedImageProvider> obtainKey(ImageConfiguration configuration) {\n    return SynchronousFuture<CachedImageProvider>(this);\n  }\n}\n\nclass CachedNovelImageProvider\n    extends BaseImageProvider<CachedNovelImageProvider> {\n  final String novelId;\n  final String imageId;\n\n  CachedNovelImageProvider(this.novelId, this.imageId);\n\n  @override\n  String get key => \"$novelId/$imageId\";\n\n  @override\n  Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {\n    chunkEvents.add(const ImageChunkEvent(\n      cumulativeBytesLoaded: 0,\n      expectedTotalBytes: 1,\n    ));\n    var cached = await CacheManager().findCache(key);\n    if (cached != null) {\n      chunkEvents.add(const ImageChunkEvent(\n        cumulativeBytesLoaded: 1,\n        expectedTotalBytes: 1,\n      ));\n      return await File(cached).readAsBytes();\n    }\n    var urlRes = await Network().getNovelImage(novelId, imageId);\n    var url = urlRes.data;\n    var dio = AppDio();\n    final time =\n        DateFormat(\"yyyy-MM-dd'T'HH:mm:ss'+00:00'\").format(DateTime.now());\n    final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();\n    var res = await dio.get<ResponseBody>(url,\n        options: Options(\n            responseType: ResponseType.stream,\n            validateStatus: (status) => status != null && status < 500,\n            headers: {\n              \"referer\": \"https://app-api.pixiv.net/\",\n              \"user-agent\": \"PixivAndroidApp/5.0.234 (Android 14; Pixes)\",\n              \"x-client-time\": time,\n              \"x-client-hash\": hash,\n              \"accept-enconding\": \"gzip\",\n            }));\n    if (res.statusCode != 200) {\n      throw BadRequestException(\"Failed to load image: ${res.statusCode}\");\n    }\n    var data = <int>[];\n    var cachingFile = await CacheManager().openWrite(key);\n    await for (var chunk in res.data!.stream) {\n      var length = res.data!.contentLength + 1;\n      if (length < data.length) {\n        length = data.length + 1;\n      }\n      data.addAll(chunk);\n      await cachingFile.writeBytes(chunk);\n      chunkEvents.add(ImageChunkEvent(\n        cumulativeBytesLoaded: data.length,\n        expectedTotalBytes: length,\n      ));\n    }\n    await cachingFile.close();\n    return Uint8List.fromList(data);\n  }\n\n  @override\n  Future<CachedNovelImageProvider> obtainKey(ImageConfiguration configuration) {\n    return SynchronousFuture<CachedNovelImageProvider>(this);\n  }\n}\n"
  },
  {
    "path": "lib/foundation/log.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/foundation.dart';\nimport 'package:pixes/utils/ext.dart';\n\nclass LogItem {\n  final LogLevel level;\n  final String title;\n  final String content;\n  final DateTime time = DateTime.now();\n\n  @override\n  toString() => \"${level.name} $title $time \\n$content\\n\\n\";\n\n  LogItem(this.level, this.title, this.content);\n}\n\nenum LogLevel { error, warning, info }\n\nclass Log {\n  static final List<LogItem> _logs = <LogItem>[];\n\n  static List<LogItem> get logs => _logs;\n\n  static const maxLogLength = 3000;\n\n  static const maxLogNumber = 500;\n\n  static bool ignoreLimitation = false;\n\n  /// only for debug\n  static const String? logFile = null;\n\n  static void printWarning(String text) {\n    debugPrint('\\x1B[33m$text\\x1B[0m');\n  }\n\n  static void printError(String text) {\n    debugPrint('\\x1B[31m$text\\x1B[0m');\n  }\n\n  static void addLog(LogLevel level, String title, String content) {\n    if (!ignoreLimitation && content.length > maxLogLength) {\n      content = \"${content.substring(0, maxLogLength)}...\";\n    }\n\n    switch (level) {\n      case LogLevel.error:\n        printError(content);\n      case LogLevel.warning:\n        printWarning(content);\n      case LogLevel.info:\n        debugPrint(content);\n    }\n\n    var newLog = LogItem(level, title, content);\n\n    if (newLog == _logs.lastOrNull) {\n      return;\n    }\n\n    _logs.add(newLog);\n    if(logFile != null) {\n      File(logFile!).writeAsString(newLog.toString(), mode: FileMode.append);\n    }\n    if (_logs.length > maxLogNumber) {\n      var res = _logs.remove(\n          _logs.firstWhereOrNull((element) => element.level == LogLevel.info));\n      if (!res) {\n        _logs.removeAt(0);\n      }\n    }\n  }\n\n  static info(String title, String content) {\n    addLog(LogLevel.info, title, content);\n  }\n\n  static warning(String title, String content) {\n    addLog(LogLevel.warning, title, content);\n  }\n\n  static error(String title, String content) {\n    addLog(LogLevel.error, title, content);\n  }\n\n  static void clear() => _logs.clear();\n\n  @override\n  String toString() {\n    var res = \"Logs\\n\\n\";\n    for (var log in _logs) {\n      res += log.toString();\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/foundation/navigation.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\n\nimport '../components/message.dart' as overlay;\nimport '../components/page_route.dart';\n\nextension Navigation on BuildContext {\n  void pop<T>([T? result]) {\n    Navigator.of(this).pop(result);\n  }\n\n  Future<T?> to<T>(Widget Function() builder) {\n    return Navigator.of(this)\n        .push<T>(AppPageRoute(builder: (context) => builder()));\n  }\n\n  void showToast({required String message, IconData? icon}) {\n    overlay.showToast(this, message: message, icon: icon);\n  }\n\n  Size get size => MediaQuery.of(this).size;\n\n  EdgeInsets get padding => MediaQuery.of(this).padding;\n\n  EdgeInsets get viewInsets => MediaQuery.of(this).viewInsets;\n}\n"
  },
  {
    "path": "lib/foundation/pair.dart",
    "content": "class Pair<M, V>{\n  M left;\n  V right;\n\n  Pair(this.left, this.right);\n\n  Pair.fromMap(Map<M, V> map, M key): left = key, right = map[key]\n      ?? (throw Exception(\"Pair not found\"));\n}"
  },
  {
    "path": "lib/foundation/state_controller.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'pair.dart';\n\nclass SimpleController extends StateController{\n  final void Function()? refresh_;\n\n  SimpleController({this.refresh_});\n\n  @override\n  void refresh() {\n    (refresh_ ?? super.refresh)();\n  }\n}\n\nabstract class StateController{\n  static final _controllers = <StateControllerWrapped>[];\n\n  static T put<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){\n    _controllers.add(StateControllerWrapped(controller, autoRemove, tag));\n    return controller;\n  }\n\n  static T putIfNotExists<T extends StateController>(T controller, {Object? tag, bool autoRemove = false}){\n    return findOrNull<T>(tag: tag) ?? put(controller, tag: tag, autoRemove: autoRemove);\n  }\n\n  static T find<T extends StateController>({Object? tag}){\n    try {\n      return _controllers.lastWhere((element) =>\n      element.controller is T\n          && (tag == null || tag == element.tag)).controller as T;\n    }\n    catch(e){\n      throw StateError(\"${T.runtimeType} with tag $tag Not Found\");\n    }\n  }\n\n  static T? findOrNull<T extends StateController>({Object? tag}){\n    try {\n      return _controllers.lastWhere((element) =>\n      element.controller is T\n          && (tag == null || tag == element.tag)).controller as T;\n    }\n    catch(e){\n      return null;\n    }\n  }\n\n  static void remove<T>([Object? tag, bool check = false]){\n    for(int i=_controllers.length-1; i>=0; i--){\n      var element = _controllers[i];\n      if(element.controller is T && (tag == null || tag == element.tag)){\n        if(check && !element.autoRemove){\n          continue;\n        }\n        _controllers.removeAt(i);\n        return;\n      }\n    }\n  }\n\n  static SimpleController putSimpleController(void Function() onUpdate, Object? tag, {void Function()? refresh}){\n    var controller = SimpleController(refresh_: refresh);\n    controller.stateUpdaters.add(Pair(null, onUpdate));\n    _controllers.add(StateControllerWrapped(controller, false, tag));\n    return controller;\n  }\n\n  List<Pair<Object?, void Function()>> stateUpdaters = [];\n\n  void update([List<Object>? ids]){\n    if(ids == null){\n      for(var element in stateUpdaters){\n        element.right();\n      }\n    }else{\n      for(var element in stateUpdaters){\n        if(ids.contains(element.left)) {\n          element.right();\n        }\n      }\n    }\n  }\n\n  void dispose(){\n    _controllers.removeWhere((element) => element.controller == this);\n  }\n\n  void refresh(){\n    update();\n  }\n}\n\nclass StateControllerWrapped{\n  StateController controller;\n  bool autoRemove;\n  Object? tag;\n\n  StateControllerWrapped(this.controller, this.autoRemove, this.tag);\n}\n\n\nclass StateBuilder<T extends StateController> extends StatefulWidget {\n  const StateBuilder({super.key, this.init, this.dispose, this.initState, this.tag,\n    required this.builder, this.id});\n\n  final T? init;\n\n  final void Function(T controller)? dispose;\n\n  final void Function(T controller)? initState;\n\n  final Object? tag;\n\n  final Widget Function(T controller) builder;\n\n  Widget builderWrapped(StateController controller){\n    return builder(controller as T);\n  }\n\n  void initStateWrapped(StateController controller){\n    return initState?.call(controller as T);\n  }\n\n  void disposeWrapped(StateController controller){\n    return dispose?.call(controller as T);\n  }\n\n  final Object? id;\n\n  @override\n  State<StateBuilder> createState() => _StateBuilderState<T>();\n}\n\nclass _StateBuilderState<T extends StateController> extends State<StateBuilder> {\n  late T controller;\n\n  @override\n  void initState() {\n    if(widget.init != null) {\n      StateController.put(widget.init!, tag: widget.tag, autoRemove: true);\n    }\n    try {\n      controller = StateController.find<T>(tag: widget.tag);\n    }\n    catch(e){\n      throw \"Controller Not Found\";\n    }\n    controller.stateUpdaters.add(Pair(widget.id, () {\n      if(mounted){\n        setState(() {});\n      }\n    }));\n    widget.initStateWrapped(controller);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    widget.disposeWrapped(controller);\n    StateController.remove<T>(widget.tag, true);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.builderWrapped(controller);\n}\n\nabstract class StateWithController<T extends StatefulWidget> extends State<T>{\n  late final SimpleController _controller;\n\n  void refresh(){\n    _controller.update();\n  }\n\n  @override\n  @mustCallSuper\n  void initState() {\n    _controller = StateController.putSimpleController(() => setState(() {}), tag, refresh: refresh);\n    super.initState();\n  }\n\n  @override\n  @mustCallSuper\n  void dispose() {\n    _controller.dispose();\n    super.dispose();\n  }\n\n  void update(){\n    _controller.update();\n  }\n\n  Object? get tag;\n}"
  },
  {
    "path": "lib/foundation/widget_utils.dart",
    "content": "import 'package:flutter/widgets.dart';\n\nextension WidgetExtension on Widget{\n  Widget padding(EdgeInsetsGeometry padding){\n    return Padding(padding: padding, child: this);\n  }\n\n  Widget paddingLeft(double padding){\n    return Padding(padding: EdgeInsets.only(left: padding), child: this);\n  }\n\n  Widget paddingRight(double padding){\n    return Padding(padding: EdgeInsets.only(right: padding), child: this);\n  }\n\n  Widget paddingTop(double padding){\n    return Padding(padding: EdgeInsets.only(top: padding), child: this);\n  }\n\n  Widget paddingBottom(double padding){\n    return Padding(padding: EdgeInsets.only(bottom: padding), child: this);\n  }\n\n  Widget paddingVertical(double padding){\n    return Padding(padding: EdgeInsets.symmetric(vertical: padding), child: this);\n  }\n\n  Widget paddingHorizontal(double padding){\n    return Padding(padding: EdgeInsets.symmetric(horizontal: padding), child: this);\n  }\n\n  Widget paddingAll(double padding){\n    return Padding(padding: EdgeInsets.all(padding), child: this);\n  }\n\n  Widget toCenter(){\n    return Center(child: this);\n  }\n\n  Widget toAlign(AlignmentGeometry alignment){\n    return Align(alignment: alignment, child: this);\n  }\n\n  Widget sliverPadding(EdgeInsetsGeometry padding){\n    return SliverPadding(padding: padding, sliver: this);\n  }\n\n  Widget sliverPaddingAll(double padding){\n    return SliverPadding(padding: EdgeInsets.all(padding), sliver: this);\n  }\n\n  Widget sliverPaddingVertical(double padding){\n    return SliverPadding(padding: EdgeInsets.symmetric(vertical: padding), sliver: this);\n  }\n\n  Widget sliverPaddingHorizontal(double padding){\n    return SliverPadding(padding: EdgeInsets.symmetric(horizontal: padding), sliver: this);\n  }\n\n  Widget fixWidth(double width){\n    return SizedBox(width: width, child: this);\n  }\n\n  Widget fixHeight(double height){\n    return SizedBox(height: height, child: this);\n  }\n}\n\nextension ColorExt on Color {\n  Color toOpacity(double opacity){\n    return withValues(alpha: opacity);\n  }\n}"
  },
  {
    "path": "lib/main.dart",
    "content": "import \"dart:async\";\nimport \"dart:ui\";\n\nimport \"package:dynamic_color/dynamic_color.dart\";\nimport \"package:fluent_ui/fluent_ui.dart\";\nimport \"package:flutter/foundation.dart\";\nimport \"package:flutter/material.dart\" as md;\nimport \"package:flutter/services.dart\";\nimport \"package:pixes/appdata.dart\";\nimport \"package:pixes/components/keyboard.dart\";\nimport \"package:pixes/components/md.dart\";\nimport \"package:pixes/components/message.dart\";\nimport \"package:pixes/foundation/app.dart\";\nimport \"package:pixes/foundation/history.dart\";\nimport \"package:pixes/foundation/log.dart\";\nimport \"package:pixes/network/app_dio.dart\";\nimport \"package:pixes/pages/main_page.dart\";\nimport \"package:pixes/utils/app_links.dart\";\nimport \"package:pixes/utils/loop.dart\";\nimport \"package:pixes/utils/translation.dart\";\nimport \"package:pixes/utils/update.dart\";\nimport \"package:pixes/utils/window.dart\";\nimport \"package:window_manager/window_manager.dart\";\n\nvoid main() {\n  runZonedGuarded(() async {\n    Future.delayed(const Duration(seconds: 3), checkUpdate);\n    WidgetsFlutterBinding.ensureInitialized();\n    FlutterError.onError = (details) {\n      Log.error(\"Unhandled\", \"${details.exception}\\n${details.stack}\");\n    };\n    setSystemProxy();\n    await App.init();\n    await appdata.readData();\n    await Translation.init();\n    HistoryManager().init();\n    handleLinks();\n    if (App.isDesktop) {\n      await WindowManager.instance.ensureInitialized();\n      windowManager.waitUntilReadyToShow().then((_) async {\n        await windowManager.setTitleBarStyle(\n          TitleBarStyle.hidden,\n          windowButtonVisibility: App.isMacOS,\n        );\n        if (App.isLinux) {\n          // https://github.com/leanflutter/window_manager/issues/460\n          return;\n        }\n       \n        if (App.isMacOS) {\n          await windowManager.setMinimumSize(const Size(650, 600));\n        } else {\n          await windowManager.setMinimumSize(const Size(500, 600));\n        }\n        var placement = await WindowPlacement.loadFromFile();\n        await placement.applyToWindow();\n        await windowManager.show();\n        Loop.register(WindowPlacement.loop);\n      });\n    }\n    Loop.start();\n    Log.info(\"APP\", \"Application started\");\n    runApp(const MyApp());\n  }, (error, stack) {\n    Log.error(\"Unhandled Exception\", \"$error\\n$stack\");\n  });\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);\n    return StateBuilder<SimpleController>(\n        init: SimpleController(),\n        tag: \"MyApp\",\n        builder: (controller) {\n          Brightness brightness =\n              PlatformDispatcher.instance.platformBrightness;\n\n          if (appdata.settings[\"theme\"] == \"Dark\") {\n            brightness = Brightness.dark;\n          } else if (appdata.settings[\"theme\"] == \"Light\") {\n            brightness = Brightness.light;\n          }\n\n          return AnnotatedRegion<SystemUiOverlayStyle>(\n            value: SystemUiOverlayStyle(\n              systemNavigationBarColor: Colors.transparent,\n              statusBarColor: Colors.transparent,\n              statusBarIconBrightness: brightness.opposite,\n              systemNavigationBarIconBrightness: brightness.opposite,\n            ),\n            child: DynamicColorBuilder(\n              builder: (light, dark) {\n                final colorScheme =\n                    (brightness == Brightness.light ? light : dark) ??\n                        md.ColorScheme.fromSeed(\n                            seedColor: Colors.blue, brightness: brightness);\n                return FluentApp(\n                    navigatorKey: App.rootNavigatorKey,\n                    debugShowCheckedModeBanner: false,\n                    title: 'pixes',\n                    theme: FluentThemeData(\n                        brightness: brightness,\n                        fontFamily: App.isWindows ? \"Microsoft YaHei UI\" : null,\n                        accentColor: AccentColor.swatch({\n                          'darkest': darken(colorScheme.primary, 30),\n                          'darker': darken(colorScheme.primary, 20),\n                          'dark': darken(colorScheme.primary, 10),\n                          'normal': colorScheme.primary,\n                          'light': lighten(colorScheme.primary, 10),\n                          'lighter': lighten(colorScheme.primary, 20),\n                          'lightest': lighten(colorScheme.primary, 30)\n                        }),\n                        focusTheme: const FocusThemeData(\n                          primaryBorder: BorderSide.none,\n                          secondaryBorder: BorderSide.none,\n                        )),\n                    home: const MainPage(),\n                    builder: (context, child) {\n                      ErrorWidget.builder = (details) {\n                        if (details.exception\n                            .toString()\n                            .contains(\"RenderFlex overflowed\")) {\n                          return const SizedBox.shrink();\n                        }\n                        Log.error(\n                            \"UI\", \"${details.exception}\\n${details.stack}\");\n                        return Text(details.exception.toString());\n                      };\n                      if (child == null) {\n                        throw \"widget is null\";\n                      }\n\n                      String? font;\n                      List<String>? fallback;\n                      if (App.isLinux || App.isWindows) {\n                        font = 'Noto Sans CJK';\n                        fallback = [\n                          'Segoe UI',\n                          'Noto Sans SC',\n                          'Noto Sans TC',\n                          'Noto Sans',\n                          'Microsoft YaHei',\n                          'PingFang SC',\n                          'Arial',\n                          'sans-serif'\n                        ];\n                      }\n\n                      Widget widget = MdTheme(\n                        data: MdThemeData.from(\n                            colorScheme: colorScheme, useMaterial3: true),\n                        child: DefaultTextStyle.merge(\n                          style: TextStyle(\n                            fontFamily: font,\n                            fontFamilyFallback: fallback,\n                          ),\n                          child: OverlayWidget(child),\n                        ),\n                      );\n\n                      return KeyEventListener(child: widget);\n                    });\n              },\n            ),\n          );\n        });\n  }\n}\n\nint _floatToInt8(double x) {\n  return (x * 255.0).round() & 0xff;\n}\n\nColor darken(Color c, [int percent = 10]) {\n  assert(1 <= percent && percent <= 100);\n  var f = 1 - percent / 100;\n  return Color.fromARGB(\n    _floatToInt8(c.a),\n    _floatToInt8(c.r * f),\n    _floatToInt8(c.g * f),\n    _floatToInt8(c.b * f),\n  );\n}\n\nColor lighten(Color c, [int percent = 10]) {\n  assert(1 <= percent && percent <= 100);\n  var p = percent / 100;\n  return Color.fromARGB(\n    _floatToInt8(c.a),\n    _floatToInt8(c.r + (1 - c.r) * p),\n    _floatToInt8(c.g + (1 - c.g) * p),\n    _floatToInt8(c.b + (1 - c.b) * p),\n  );\n}\n"
  },
  {
    "path": "lib/network/app_dio.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:dio/dio.dart';\nimport 'package:dio/io.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/log.dart';\nimport 'package:pixes/utils/ext.dart';\n\nexport 'package:dio/dio.dart';\n\nclass MyLogInterceptor implements Interceptor {\n  @override\n  void onError(DioException err, ErrorInterceptorHandler handler) {\n    Log.error(\"Network\",\n        \"${err.requestOptions.method} ${err.requestOptions.path}\\n$err\\n${err.response?.data.toString()}\");\n    switch (err.type) {\n      case DioExceptionType.badResponse:\n        var statusCode = err.response?.statusCode;\n        if (statusCode != null) {\n          err = err.copyWith(\n              message: \"Invalid Status Code: $statusCode. \"\n                  \"${_getStatusCodeInfo(statusCode)}\");\n        }\n      case DioExceptionType.connectionTimeout:\n        err = err.copyWith(message: \"Connection Timeout\");\n      case DioExceptionType.receiveTimeout:\n        err = err.copyWith(\n            message: \"Receive Timeout: \"\n                \"This indicates that the server is too busy to respond\");\n      case DioExceptionType.unknown:\n        if (err.toString().contains(\"Connection terminated during handshake\")) {\n          err = err.copyWith(\n              message: \"Connection terminated during handshake: \"\n                  \"This may be caused by the firewall blocking the connection \"\n                  \"or your requests are too frequent.\");\n        } else if (err.toString().contains(\"Connection reset by peer\")) {\n          err = err.copyWith(\n              message: \"Connection reset by peer: \"\n                  \"The error is unrelated to app, please check your network.\");\n        }\n      default:\n        {}\n    }\n    handler.next(err);\n  }\n\n  static const errorMessages = <int, String>{\n    400: \"The Request is invalid.\",\n    401: \"The Request is unauthorized.\",\n    403: \"No permission to access the resource. Check your account or network.\",\n    404: \"Not found.\",\n    429: \"Too many requests. Please try again later.\",\n  };\n\n  String _getStatusCodeInfo(int? statusCode) {\n    if (statusCode != null && statusCode >= 500) {\n      return \"This is server-side error, please try again later. \"\n          \"Do not report this issue.\";\n    } else {\n      return errorMessages[statusCode] ?? \"\";\n    }\n  }\n\n  @override\n  void onResponse(\n      Response<dynamic> response, ResponseInterceptorHandler handler) {\n    var headers = response.headers.map.map((key, value) => MapEntry(\n        key.toLowerCase(), value.length == 1 ? value.first : value.toString()));\n    headers.remove(\"cookie\");\n    String content;\n    if (response.data is List<int>) {\n      content = \"<Bytes>\\nlength:${response.data.length}\";\n    } else {\n      content = response.data.toString();\n    }\n    Log.addLog(\n        (response.statusCode != null && response.statusCode! < 400)\n            ? LogLevel.info\n            : LogLevel.error,\n        \"Network\",\n        \"Response ${response.realUri.toString()} ${response.statusCode}\\n\"\n            \"headers:\\n$headers\\n$content\");\n    handler.next(response);\n  }\n\n  @override\n  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\n    options.connectTimeout = const Duration(seconds: 15);\n    options.receiveTimeout = const Duration(seconds: 15);\n    options.sendTimeout = const Duration(seconds: 15);\n    if (options.headers[\"Host\"] == null && options.headers[\"host\"] == null) {\n      options.headers[\"host\"] = options.uri.host;\n    }\n    Log.info(\"Network\",\n        \"${options.method} ${options.uri}\\n${options.headers}\\n${options.data}\");\n    handler.next(options);\n  }\n}\n\nclass AppDio extends DioForNative {\n  bool isInitialized = false;\n\n  @override\n  Future<Response<T>> request<T>(String path,\n      {Object? data,\n      Map<String, dynamic>? queryParameters,\n      CancelToken? cancelToken,\n      Options? options,\n      ProgressCallback? onSendProgress,\n      ProgressCallback? onReceiveProgress}) async{\n    if (!isInitialized) {\n      isInitialized = true;\n      interceptors.add(MyLogInterceptor());\n    }\n    if(T == Map<String, dynamic>) {\n      var res = await super.request<String>(path,\n          data: data,\n          queryParameters: queryParameters,\n          cancelToken: cancelToken,\n          options: options,\n          onSendProgress: onSendProgress,\n          onReceiveProgress: onReceiveProgress);\n      if(res.data == null) {\n        return Response(\n          data: null,\n          requestOptions: res.requestOptions,\n          statusCode: res.statusCode,\n          statusMessage: res.statusMessage,\n          isRedirect: res.isRedirect,\n          redirects: res.redirects,\n          extra: res.extra,\n          headers: res.headers\n        );\n      }\n      try {\n        var json = jsonDecode(res.data!);\n        return Response(\n            data: json,\n            requestOptions: res.requestOptions,\n            statusCode: res.statusCode,\n            statusMessage: res.statusMessage,\n            isRedirect: res.isRedirect,\n            redirects: res.redirects,\n            extra: res.extra,\n            headers: res.headers\n        );\n      }\n      catch(e) {\n        var data = res.data!;\n        if(data.length > 50) {\n          data = \"${data.substring(0, 50)}...\";\n        }\n        throw \"Failed to decode response: $e\\n$data\";\n      }\n    }\n    return super.request<T>(path,\n        data: data,\n        queryParameters: queryParameters,\n        cancelToken: cancelToken,\n        options: options,\n        onSendProgress: onSendProgress,\n        onReceiveProgress: onReceiveProgress);\n  }\n}\n\nvoid setSystemProxy() {\n  HttpOverrides.global = _ProxyHttpOverrides()..findProxy(Uri());\n}\n\nclass _ProxyHttpOverrides extends HttpOverrides {\n  String proxy = \"DIRECT\";\n\n  String findProxy(Uri uri) {\n    var haveUserProxy = appdata.settings[\"proxy\"] != null &&\n        appdata.settings[\"proxy\"].toString().removeAllBlank.isNotEmpty;\n    if (!App.isLinux && !haveUserProxy) {\n      var channel = const MethodChannel(\"pixes/proxy\");\n      channel.invokeMethod(\"getProxy\").then((value) {\n        if (value.toString().toLowerCase() == \"no proxy\") {\n          proxy = \"DIRECT\";\n        } else {\n          if (proxy.contains(\"https\")) {\n            var proxies = value.split(\";\");\n            for (String proxy in proxies) {\n              proxy = proxy.removeAllBlank;\n              if (proxy.startsWith('https=')) {\n                value = proxy.substring(6);\n              }\n            }\n          }\n          proxy = \"PROXY $value\";\n        }\n      });\n    } else {\n      if (haveUserProxy) {\n        proxy = \"PROXY ${appdata.settings[\"proxy\"]}\";\n      }\n    }\n    // check validation\n    if (proxy.startsWith(\"PROXY\")) {\n      var uri = proxy.replaceFirst(\"PROXY\", \"\").removeAllBlank;\n      if (!uri.startsWith(\"http\")) {\n        uri += \"http://\";\n      }\n      if (!uri.isURL) {\n        return \"DIRECT\";\n      }\n    }\n    return proxy;\n  }\n\n  @override\n  HttpClient createHttpClient(SecurityContext? context) {\n    final client = super.createHttpClient(context);\n    client.connectionTimeout = const Duration(seconds: 5);\n    client.findProxy = findProxy;\n    return client;\n  }\n}\n"
  },
  {
    "path": "lib/network/download.dart",
    "content": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:io';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:intl/intl.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/log.dart';\nimport 'package:pixes/network/app_dio.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/io.dart';\nimport 'package:sqlite3/sqlite3.dart';\n\nextension IllustExt on Illust {\n  bool get downloaded => DownloadManager().checkDownloaded(id);\n\n  bool get downloading =>\n      DownloadManager().tasks.any((element) => element.illust.id == id);\n}\n\nclass DownloadedIllust {\n  final int illustId;\n  final String title;\n  final String author;\n  final int imageCount;\n\n  DownloadedIllust({\n    required this.illustId,\n    required this.title,\n    required this.author,\n    required this.imageCount,\n  });\n}\n\nclass DownloadingTask {\n  final Illust illust;\n\n  void Function(int)? receiveBytesCallback;\n\n  void Function(DownloadingTask)? onCompleted;\n\n  DownloadingTask(this.illust, {this.receiveBytesCallback, this.onCompleted});\n\n  int _downloadingIndex = 0;\n\n  int get totalImages => illust.images.length;\n\n  int get downloadedImages => _downloadingIndex;\n\n  bool _stop = true;\n\n  String? error;\n\n  void start() {\n    _stop = false;\n    _download();\n  }\n\n  Dio get dio => Network().dio;\n\n  void cancel() {\n    _stop = true;\n    DownloadManager().tasks.remove(this);\n    for(var path in imagePaths) {\n      File(path).deleteIfExists();\n    }\n  }\n\n  List<String> imagePaths = [];\n\n  void _download() async{\n    try{\n      while(_downloadingIndex < illust.images.length) {\n        if(_stop) return;\n        var url = illust.images[_downloadingIndex].original;\n        var ext = url.split('.').last;\n        if(![\"jpg\", \"png\", \"gif\", \"webp\", \"jpeg\", \"avif\"].contains(ext)) {\n          ext = \"jpg\";\n        }\n        var path = _generateFilePath(illust, _downloadingIndex, ext);\n        final time = DateFormat(\"yyyy-MM-dd'T'HH:mm:ss'+00:00'\").format(DateTime.now());\n        final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString();\n        var res = await dio.get<ResponseBody>(url, options: Options(\n          responseType: ResponseType.stream,\n          headers: {\n            \"referer\": \"https://app-api.pixiv.net/\",\n            \"user-agent\": \"PixivAndroidApp/5.0.234 (Android 14; Pixes)\",\n            \"x-client-time\": time,\n            \"x-client-hash\": hash,\n            \"accept-enconding\": \"gzip\",\n          },\n        ));\n        var file = File(path);\n        if(!file.existsSync()) {\n          file.createSync(recursive: true);\n        }\n        await for (var data in res.data!.stream) {\n          await file.writeAsBytes(data, mode: FileMode.append);\n          receiveBytesCallback?.call(data.length);\n        }\n        imagePaths.add(path);\n        _downloadingIndex++;\n        _retryCount = 0;\n      }\n      onCompleted?.call(this);\n    }\n    catch(e, s) {\n      _handleError(e);\n      Log.error(\"Download\", \"Download error: $e\\n$s\");\n    }\n  }\n\n  int _retryCount = 0;\n\n  void _handleError(Object error) async{\n    _retryCount++;\n    if(_retryCount > 3) {\n      _stop = true;\n      error = error.toString();\n      return;\n    }\n    await Future.delayed(Duration(seconds: 1 << _retryCount));\n    _download();\n  }\n\n  static String _generateFilePath(Illust illust, int index, String ext) {\n    final String downloadPath = appdata.settings[\"downloadPath\"];\n    String subPathPatten = appdata.settings[\"downloadSubPath\"];\n    subPathPatten = subPathPatten.replaceAll(r\"${id}\", illust.id.toString());\n    subPathPatten = subPathPatten.replaceAll(r\"${title}\", illust.title);\n    subPathPatten = subPathPatten.replaceAll(r\"${author}\", illust.author.name);\n    subPathPatten = subPathPatten.replaceAll(r\"${index}\", index.toString());\n    subPathPatten = subPathPatten.replaceAll(r\"${page}\",\n        illust.images.length == 1 ? \"\" : \"-p$index\");\n    subPathPatten = subPathPatten.replaceAll(r\"${ext}\", ext);\n    subPathPatten = subPathPatten.replaceAll(r\"${AI}\", illust.isAi ? \"AI\" : \"\");\n    List<String> extractTags(String input) {\n      final regex = RegExp(r'\\$\\{tag\\((.*?)\\)\\}');\n      final matches = regex.allMatches(input);\n      return matches.map((match) => match.group(1)!).toList();\n    }\n    var tags = extractTags(subPathPatten);\n    for(var tag in tags) {\n      if (illust.tags.where((e) => e.name == tag || e.translatedName == tag).isNotEmpty) {\n        subPathPatten = subPathPatten.replaceAll(\"\\${tag($tag)}\", tag);\n      }\n    }\n    return _cleanFilePath(\"$downloadPath$subPathPatten\");\n  }\n\n  static String _cleanFilePath(String filePath) {\n    const invalidChars = ['*', '?', '\"', '<', '>', '|'];\n\n    String cleanedPath =\n        filePath.replaceAll(RegExp('[${invalidChars.join(' ')}]'), '');\n\n    cleanedPath = cleanedPath.replaceAll(RegExp(r'[/\\\\]+'), '/');\n\n    return cleanedPath;\n  }\n\n  void retry() {\n    error = null;\n    _stop = false;\n    _download();\n  }\n\n  void pause() {\n    _stop = true;\n  }\n}\n\nclass DownloadManager {\n  factory DownloadManager() => instance ??= DownloadManager._();\n\n  static DownloadManager? instance;\n\n  DownloadManager._(){\n    init();\n  }\n\n  late Database _db;\n\n  int _currentBytes = 0;\n  int _bytesPerSecond = 0;\n\n  int get bytesPerSecond => _bytesPerSecond;\n\n  Timer? _loop;\n\n  var tasks = <DownloadingTask>[];\n\n  void Function()? uiUpdateCallback;\n\n  void registerUiUpdater(void Function() callback) {\n    uiUpdateCallback = callback;\n  }\n\n  void removeUiUpdater() {\n    uiUpdateCallback = null;\n  }\n\n  void init() {\n    _db = sqlite3.open(\"${App.dataPath}/download.db\");\n    _db.execute('''\n      create table if not exists download (\n        illust_id integer primary key not null,\n        title text not null,\n        author text not null,\n        imageCount int not null\n      );\n    ''');\n    _db.execute('''\n      create table if not exists images (\n        illust_id integer not null,\n        image_index integer not null,\n        path text not null,\n        primary key (illust_id, image_index)\n      );\n    ''');\n  }\n\n  void saveInfo(Illust illust, List<String> imagePaths) {\n    _db.execute('''\n      insert into download (illust_id, title, author, imageCount)\n      values (?, ?, ?, ?)\n    ''', [illust.id, illust.title, illust.author.name, imagePaths.length]);\n    for (var i = 0; i < imagePaths.length; i++) {\n      _db.execute('''\n        insert into images (illust_id, image_index, path)\n        values (?, ?, ?)\n      ''', [illust.id, i, imagePaths[i]]);\n    }\n  }\n\n  File? getImage(int illustId, int index) {\n    var res = _db.select('''\n      select * from images\n      where illust_id = ? and image_index = ?;\n    ''', [illustId, index]);\n    if (res.isEmpty) return null;\n    var file = File(res.first[\"path\"] as String);\n    if (!file.existsSync()) return null;\n    return file;\n  }\n\n  bool checkDownloaded(int illustId) {\n    var res = _db.select('''\n      select * from download\n      where illust_id = ?;\n    ''', [illustId]);\n    return res.isNotEmpty;\n  }\n\n  List<DownloadedIllust> listAll() {\n    var res = _db.select('''\n      select * from download;\n    ''');\n    return res.map((e) =>\n        DownloadedIllust(\n          illustId: e[\"illust_id\"] as int,\n          title: e[\"title\"] as String,\n          author: e[\"author\"] as String,\n          imageCount: e[\"imageCount\"] as int,\n        )).toList();\n  }\n\n  void addDownloadingTask(Illust illust) {\n    if(illust.downloaded || illust.downloading) return;\n    var task = DownloadingTask(illust, receiveBytesCallback: receiveBytes, onCompleted: (task) {\n      saveInfo(illust, task.imagePaths);\n      tasks.remove(task);\n    });\n    tasks.add(task);\n    run();\n  }\n\n  void receiveBytes(int bytes) {\n    _currentBytes += bytes;\n  }\n\n  int get maxConcurrentTasks => appdata.settings[\"maxParallels\"];\n\n  bool _paused = false;\n\n  bool get paused => _paused;\n\n  void pause() {\n    _paused = true;\n    for(var task in tasks) {\n      task.pause();\n    }\n  }\n\n  void run() {\n    _loop ??= Timer.periodic(const Duration(seconds: 1), (timer) {\n      if(_paused) return;\n      _bytesPerSecond = _currentBytes;\n      _currentBytes = 0;\n      uiUpdateCallback?.call();\n      for(int i=0; i<maxConcurrentTasks; i++) {\n        var task = tasks.elementAtOrNull(i);\n        if(task != null && task._stop && task.error == null) {\n          task.start();\n        }\n      }\n      if(tasks.isEmpty) {\n        timer.cancel();\n        _loop = null;\n        _currentBytes = 0;\n        _bytesPerSecond = 0;\n      }\n    });\n  }\n\n  void delete(DownloadedIllust illust) {\n    _db.execute('''\n      delete from download\n      where illust_id = ?;\n    ''', [illust.illustId]);\n    var images = _db.select('''\n      select * from images\n      where illust_id = ?;\n    ''', [illust.illustId]);\n    for(var image in images) {\n      File(image[\"path\"] as String).deleteIgnoreError();\n    }\n    _db.execute('''\n      delete from images\n      where illust_id = ?;\n    ''', [illust.illustId]);\n  }\n\n  List<String> getImagePaths(int illustId) {\n    var res = _db.select('''\n      select * from images\n      where illust_id = ?;\n    ''', [illustId]);\n    return res.map((e) => e[\"path\"] as String).toList();\n  }\n\n  Future<void> batchDownload(Future<Res<List<Illust>>> request, int maxCount) async{\n    List<Illust> all = [];\n    String? nextUrl;\n    int retryCount = 0;\n    while(nextUrl != \"end\" && all.length < maxCount) {\n      if(nextUrl != null) {\n        request = Network().getIllustsWithNextUrl(nextUrl);\n      }\n      var res = await request;\n      if(res.error) {\n        retryCount++;\n        if(retryCount > 3) {\n          throw res.error;\n        }\n        await Future.delayed(Duration(seconds: 1 << retryCount));\n        continue;\n      }\n      all.addAll(res.data);\n      nextUrl = res.subData ?? \"end\";\n    }\n    int i = 0;\n    for(var illust in all) {\n      if(i > maxCount)  return;\n      addDownloadingTask(illust);\n      i++;\n    }\n  }\n\n  Future<void> checkAndClearInvalidItems() async{\n    var illusts = listAll();\n    var shouldDelete = <DownloadedIllust>[];\n    for(var item in illusts) {\n      var paths = getImagePaths(item.illustId);\n      var validPaths = <String>[];\n      for(var path in paths) {\n        if(await File(path).exists()) {\n          validPaths.add(path);\n        }\n      }\n      if(validPaths.isEmpty) {\n        shouldDelete.add(item);\n      }\n    }\n    for(var item in shouldDelete) {\n      delete(item);\n    }\n  }\n\n  void resume() {\n    _paused = false;\n  }\n}"
  },
  {
    "path": "lib/network/models.dart",
    "content": "import 'package:pixes/appdata.dart';\n\nclass Account {\n  String accessToken;\n  String refreshToken;\n  final User user;\n\n  Account(this.accessToken, this.refreshToken, this.user);\n\n  Account.fromJson(Map<String, dynamic> json)\n      : accessToken = json['access_token'],\n        refreshToken = json['refresh_token'],\n        user = User.fromJson(json['user']);\n\n  Map<String, dynamic> toJson() => {\n        'access_token': accessToken,\n        'refresh_token': refreshToken,\n        'user': user.toJson()\n      };\n}\n\nclass User {\n  String profile;\n  final String id;\n  String name;\n  String account;\n  String email;\n  bool isPremium;\n\n  User(this.profile, this.id, this.name, this.account, this.email,\n      this.isPremium);\n\n  User.fromJson(Map<String, dynamic> json)\n      : profile = json['profile_image_urls']['px_170x170'],\n        id = json['id'],\n        name = json['name'],\n        account = json['account'],\n        email = json['mail_address'],\n        isPremium = json['is_premium'];\n\n  Map<String, dynamic> toJson() => {\n        'profile_image_urls': {'px_170x170': profile},\n        'id': id,\n        'name': name,\n        'account': account,\n        'mail_address': email,\n        'is_premium': isPremium\n      };\n}\n\nclass UserDetails {\n  final int id;\n  final String name;\n  final String account;\n  final String avatar;\n  final String comment;\n  bool isFollowed;\n  final bool isBlocking;\n  final String? webpage;\n  final String gender;\n  final String birth;\n  final String region;\n  final String job;\n  final int totalFollowUsers;\n  final int myPixivUsers;\n  final int totalIllusts;\n  final int totalMangas;\n  final int totalNovels;\n  final int totalIllustBookmarks;\n  final String? backgroundImage;\n  final String? twitterUrl;\n  final bool isPremium;\n  final String? pawooUrl;\n\n  UserDetails(\n      this.id,\n      this.name,\n      this.account,\n      this.avatar,\n      this.comment,\n      this.isFollowed,\n      this.isBlocking,\n      this.webpage,\n      this.gender,\n      this.birth,\n      this.region,\n      this.job,\n      this.totalFollowUsers,\n      this.myPixivUsers,\n      this.totalIllusts,\n      this.totalMangas,\n      this.totalNovels,\n      this.totalIllustBookmarks,\n      this.backgroundImage,\n      this.twitterUrl,\n      this.isPremium,\n      this.pawooUrl);\n\n  UserDetails.fromJson(Map<String, dynamic> json)\n      : id = json['user']['id'],\n        name = json['user']['name'],\n        account = json['user']['account'],\n        avatar = json['user']['profile_image_urls']['medium'],\n        comment = json['user']['comment'],\n        isFollowed = json['user']['is_followed'],\n        isBlocking = json['user']['is_access_blocking_user'],\n        webpage = json['profile']['webpage'],\n        gender = json['profile']['gender'],\n        birth = json['profile']['birth'],\n        region = json['profile']['region'],\n        job = json['profile']['job'],\n        totalFollowUsers = json['profile']['total_follow_users'],\n        myPixivUsers = json['profile']['total_mypixiv_users'],\n        totalIllusts = json['profile']['total_illusts'],\n        totalMangas = json['profile']['total_manga'],\n        totalNovels = json['profile']['total_novels'],\n        totalIllustBookmarks = json['profile']['total_illust_bookmarks_public'],\n        backgroundImage = json['profile']['background_image_url'],\n        twitterUrl = json['profile']['twitter_url'],\n        isPremium = json['profile']['is_premium'],\n        pawooUrl = json['profile']['pawoo_url'];\n}\n\nclass Author {\n  final int id;\n  final String name;\n  final String account;\n  final String avatar;\n  bool isFollowed;\n\n  Author(this.id, this.name, this.account, this.avatar, this.isFollowed);\n}\n\nclass Tag {\n  final String name;\n  final String? translatedName;\n\n  const Tag(this.name, this.translatedName);\n\n  @override\n  String toString() {\n    return \"$name${translatedName == null ? \"\" : \"($translatedName)\"}\";\n  }\n\n  @override\n  bool operator ==(Object other) {\n    if (other is Tag) {\n      return name == other.name;\n    }\n    return false;\n  }\n\n  @override\n  int get hashCode => name.hashCode;\n\n  static Tag fromJson(Map<String, dynamic> json) {\n    return Tag(json['name'] ?? \"\", json['translated_name']);\n  }\n}\n\nclass IllustImage {\n  final String squareMedium;\n  final String medium;\n  final String large;\n  final String original;\n\n  const IllustImage(this.squareMedium, this.medium, this.large, this.original);\n}\n\nclass Illust {\n  final int id;\n  final String title;\n  final String type;\n  final List<IllustImage> images;\n  final String caption;\n  final int restrict;\n  final Author author;\n  final List<Tag> tags;\n  final DateTime createDate;\n  final int pageCount;\n  final int width;\n  final int height;\n  final int totalView;\n  final int totalBookmarks;\n  bool isBookmarked;\n  final bool isAi;\n  final bool isUgoira;\n  final bool isBlocked;\n\n  bool get isR18 => tags.contains(const Tag(\"R-18\", null));\n\n  bool get isR18G => tags.contains(const Tag(\"R-18G\", null));\n\n  Illust.fromJson(Map<String, dynamic> json)\n      : id = json['id'],\n        title = json['title'],\n        type = json['type'],\n        images = (() {\n          List<IllustImage> images = [];\n          for (var i in json['meta_pages']) {\n            images.add(IllustImage(\n                i['image_urls']['square_medium'],\n                i['image_urls']['medium'],\n                i['image_urls']['large'],\n                i['image_urls']['original']));\n          }\n          if (images.isEmpty) {\n            images.add(IllustImage(\n                json['image_urls']['square_medium'],\n                json['image_urls']['medium'],\n                json['image_urls']['large'],\n                json['meta_single_page']['original_image_url']));\n          }\n          return images;\n        }()),\n        caption = json['caption'],\n        restrict = json['restrict'],\n        author = Author(\n            json['user']['id'],\n            json['user']['name'],\n            json['user']['account'],\n            json['user']['profile_image_urls']['medium'],\n            json['user']['is_followed'] ?? false),\n        tags = (json['tags'] as List)\n            .map((e) => Tag(e['name'], e['translated_name']))\n            .toList(),\n        createDate = DateTime.parse(json['create_date']),\n        pageCount = json['page_count'],\n        width = json['width'],\n        height = json['height'],\n        totalView = json['total_view'],\n        totalBookmarks = json['total_bookmarks'],\n        isBookmarked = json['is_bookmarked'],\n        isAi = json['illust_ai_type'] == 2,\n        isUgoira = json['type'] == \"ugoira\",\n        isBlocked = json['is_muted'] ?? false;\n}\n\nclass TrendingTag {\n  final Tag tag;\n  final Illust illust;\n\n  TrendingTag(this.tag, this.illust);\n}\n\nenum KeywordMatchType {\n  tagsPartialMatches(\"Tags partial match\"),\n  tagsExactMatch(\"Tags exact match\"),\n  titleOrDescriptionSearch(\"Title or description search\");\n\n  final String text;\n\n  const KeywordMatchType(this.text);\n\n  @override\n  toString() => text;\n\n  String toParam() => switch (this) {\n        KeywordMatchType.tagsPartialMatches => \"partial_match_for_tags\",\n        KeywordMatchType.tagsExactMatch => \"exact_match_for_tags\",\n        KeywordMatchType.titleOrDescriptionSearch => \"title_and_caption\"\n      };\n}\n\nenum FavoriteNumber {\n  unlimited(-1),\n  f500(500),\n  f1000(1000),\n  f2000(2000),\n  f5000(5000),\n  f7500(7500),\n  f10000(10000),\n  f20000(20000),\n  f50000(50000),\n  f100000(100000);\n\n  final int number;\n  const FavoriteNumber(this.number);\n\n  @override\n  toString() =>\n      this == FavoriteNumber.unlimited ? \"Unlimited\" : \"$number Bookmarks\";\n\n  String toParam() =>\n      this == FavoriteNumber.unlimited ? \"\" : \" ${number}users入り\";\n}\n\nenum SearchSort {\n  newToOld,\n  oldToNew,\n  popular,\n  popularMale,\n  popularFemale;\n\n  bool get isPremium => appdata.account?.user.isPremium == true;\n\n  static List<SearchSort> get availableValues => [\n        SearchSort.newToOld,\n        SearchSort.oldToNew,\n        SearchSort.popular,\n        if (appdata.account?.user.isPremium == true) SearchSort.popularMale,\n        if (appdata.account?.user.isPremium == true) SearchSort.popularFemale\n      ];\n\n  @override\n  toString() {\n    if (this == SearchSort.popular) {\n      return isPremium ? \"Popular\" : \"Popular(limited)\";\n    } else if (this == SearchSort.newToOld) {\n      return \"New to old\";\n    } else if (this == SearchSort.oldToNew) {\n      return \"Old to new\";\n    } else if (this == SearchSort.popularMale) {\n      return \"Popular(Male)\";\n    } else {\n      return \"Popular(Female)\";\n    }\n  }\n\n  String toParam() => switch (this) {\n        SearchSort.newToOld => \"date_desc\",\n        SearchSort.oldToNew => \"date_asc\",\n        SearchSort.popular => \"popular_desc\",\n        SearchSort.popularMale => \"popular_male_desc\",\n        SearchSort.popularFemale => \"popular_female_desc\",\n      };\n}\n\nenum AgeLimit {\n  unlimited(\"Unlimited\"),\n  allAges(\"All ages\"),\n  r18(\"R18\");\n\n  final String text;\n\n  const AgeLimit(this.text);\n\n  @override\n  toString() => text;\n\n  String toParam() => switch (this) {\n        AgeLimit.unlimited => \"\",\n        AgeLimit.allAges => \" -R-18\",\n        AgeLimit.r18 => \"R-18\",\n      };\n}\n\nclass SearchOptions {\n  KeywordMatchType matchType = KeywordMatchType.tagsPartialMatches;\n  FavoriteNumber favoriteNumber = FavoriteNumber.unlimited;\n  SearchSort sort = SearchSort.newToOld;\n  DateTime? startTime;\n  DateTime? endTime;\n  AgeLimit ageLimit = AgeLimit.unlimited;\n}\n\n/*\njson:\n{\n        \"id\": 20542044,\n        \"name\": \"vocaloidhm01\",\n        \"account\": \"vocaloidhm01\",\n        \"profile_image_urls\": {\n          \"medium\": \"https://i.pximg.net/user-profile/img/2023/04/28/00/21/54/24348957_c74a61e78ddccb467417be7c37b5d463_170.jpg\"\n        },\n        \"is_followed\": false,\n        \"is_access_blocking_user\": false\n}\n */\nclass UserPreview {\n  final int id;\n  final String name;\n  final String account;\n  final String avatar;\n  bool isFollowed;\n  final bool isBlocking;\n  final List<Illust> artworks;\n\n  UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed,\n      this.isBlocking, this.artworks);\n\n  UserPreview.fromJson(Map<String, dynamic> json)\n      : id = json['user']['id'],\n        name = json['user']['name'],\n        account = json['user']['account'],\n        avatar = json['user']['profile_image_urls']['medium'],\n        isFollowed = json['user']['is_followed'],\n        isBlocking = json['user']['is_access_blocking_user'] ?? false,\n        artworks =\n            (json['illusts'] as List).map((e) => Illust.fromJson(e)).toList();\n}\n\n/*\n{\n      \"id\": 176418447,\n      \"comment\": \"\",\n      \"date\": \"2024-05-13T19:28:13+09:00\",\n      \"user\": {\n        \"id\": 54898889,\n        \"name\": \"Rorigod\",\n        \"account\": \"user_gjzr2787\",\n        \"profile_image_urls\": {\n          \"medium\": \"https://i.pximg.net/user-profile/img/2021/09/01/00/46/58/21334581_94fac3456245d2b680ecf1c60aba2c95_170.png\"\n        }\n      },\n      \"has_replies\": false,\n      \"stamp\": {\n        \"stamp_id\": 407,\n        \"stamp_url\": \"https://s.pximg.net/common/images/stamp/generated-stamps/407_s.jpg?20180605\"\n      }\n    }\n */\nclass Comment {\n  final String id;\n  final String comment;\n  final DateTime date;\n  final String uid;\n  final String name;\n  final String avatar;\n  final bool hasReplies;\n  final String? stampUrl;\n\n  Comment.fromJson(Map<String, dynamic> json)\n      : id = json['id'].toString(),\n        comment = json['comment'],\n        date = DateTime.parse(json['date']),\n        uid = json['user']['id'].toString(),\n        name = json['user']['name'],\n        avatar = json['user']['profile_image_urls']['medium'],\n        hasReplies = json['has_replies'] ?? false,\n        stampUrl = json['stamp']?['stamp_url'];\n}\n\n/*\n{\n      \"id\": 20741342,\n      \"title\": \"中身が一般人のやつがれくん\",\n      \"caption\": \"なんか思いついたので書いてみた。<br />よくある芥川成り代わり。<br />３年くらい前の書きかけのやつをサルベージ。<br />じっくりは書いてないので抜け抜け。<br /><br />デイリー１位ありがとうございます✨<br /><br />※※※※※※※※<br />※※※※※※※※<br /><br />以下読了後推奨の蛇足<br /><br />「芥川くん」<br />「なんですかボス」<br />「君は将来的にどんな地位につきたいとかある？」<br />「僕はしがない一構成員ゆえ」<br />「ほら幹部とか隊長とか人事部とかさ。君あれこれオールマイティにできるから希望を聞いておこうと思って」<br />「ございます」<br />「なにかな？」<br />「僕は将来的にポートマフィア直営のいちじく農家になりたいと思います」<br />「なんて？」<br />「さらに、ゆくゆくはいちじく農家兼、いちじくの素晴らしさを世に知らしめるポートマフィア直営いちじくレストランを開きたいと」<br />「なんて？？？」\",\n      \"restrict\": 0,\n      \"x_restrict\": 0,\n      \"is_original\": false,\n      \"image_urls\": {\n        \"square_medium\": \"https://i.pximg.net/c/128x128/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_square1200.jpg\",\n        \"medium\": \"https://i.pximg.net/c/176x352/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_master1200.jpg\",\n        \"large\": \"https://i.pximg.net/c/240x480_80/novel-cover-master/img/2023/09/27/16/14/45/ci20741342_db401e9b27afbf96f772d30759e1d104_master1200.jpg\"\n      },\n      \"create_date\": \"2023-09-27T16:14:45+09:00\",\n      \"tags\": [\n        {\n          \"name\": \"文スト夢\",\n          \"translated_name\": \"Bungo Stray Dogs original/self-insert\",\n          \"added_by_uploaded_user\": true\n        },\n        {\n          \"name\": \"成り代わり\",\n          \"translated_name\": \"取代即有角色\",\n          \"added_by_uploaded_user\": true\n        },\n      ],\n      \"page_count\": 6,\n      \"text_length\": 12550,\n      \"user\": {\n        \"id\": 9275134,\n        \"name\": \"もろろ\",\n        \"account\": \"sleepinglife\",\n        \"profile_image_urls\": {\n          \"medium\": \"https://s.pximg.net/common/images/no_profile.png\"\n        },\n        \"is_followed\": false\n      },\n      \"series\": {\n        \"id\": 11897059,\n        \"title\": \"文スト夢\"\n      },\n      \"is_bookmarked\": false,\n      \"total_bookmarks\": 8099,\n      \"total_view\": 76112,\n      \"visible\": true,\n      \"total_comments\": 146,\n      \"is_muted\": false,\n      \"is_mypixiv_only\": false,\n      \"is_x_restricted\": false,\n      \"novel_ai_type\": 1\n    }\n*/\nclass Novel {\n  final int id;\n  final String title;\n  final String caption;\n  final bool isOriginal;\n  final String image;\n  final DateTime createDate;\n  final List<Tag> tags;\n  final int pages;\n  final int length;\n  final Author author;\n  final int? seriesId;\n  final String? seriesTitle;\n  bool isBookmarked;\n  final int totalBookmarks;\n  final int totalViews;\n  final int commentsCount;\n  final bool isAi;\n\n  Novel.fromJson(Map<String, dynamic> json)\n      : id = json[\"id\"],\n        title = json[\"title\"],\n        caption = json[\"caption\"],\n        isOriginal = json[\"is_original\"],\n        image = json[\"image_urls\"][\"large\"] ??\n            json[\"image_urls\"][\"medium\"] ??\n            json[\"image_urls\"][\"square_medium\"] ??\n            \"\",\n        createDate = DateTime.parse(json[\"create_date\"]),\n        tags = (json['tags'] as List)\n            .map((e) => Tag(e['name'], e['translated_name']))\n            .toList(),\n        pages = json[\"page_count\"],\n        length = json[\"text_length\"],\n        author = Author(\n            json['user']['id'],\n            json['user']['name'],\n            json['user']['account'],\n            json['user']['profile_image_urls']['medium'],\n            json['user']['is_followed'] ?? false),\n        seriesId = json[\"series\"]?[\"id\"],\n        seriesTitle = json[\"series\"]?[\"title\"],\n        isBookmarked = json[\"is_bookmarked\"],\n        totalBookmarks = json[\"total_bookmarks\"],\n        totalViews = json[\"total_view\"],\n        commentsCount = json[\"total_comments\"],\n        isAi = json[\"novel_ai_type\"] == 2;\n}\n\nclass MuteList {\n  List<Tag> tags;\n\n  List<Author> authors;\n\n  int limit;\n\n  MuteList(this.tags, this.authors, this.limit);\n\n  static MuteList? fromJson(Map<String, dynamic> data) {\n    return MuteList(\n        (data['muted_tags'] as List)\n            .map((e) => Tag(e['tag'], e['tag_translation']))\n            .toList(),\n        (data['muted_users'] as List)\n            .map((e) => Author(e['user_id'], e['user_name'], e['user_account'],\n                e['user_profile_image_urls']['medium'], false))\n            .toList(),\n        data['mute_limit_count']);\n  }\n}\n"
  },
  {
    "path": "lib/network/network.dart",
    "content": "import 'dart:convert';\nimport 'dart:math';\n\nimport 'package:crypto/crypto.dart';\nimport 'package:intl/intl.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/log.dart';\nimport 'package:pixes/network/app_dio.dart';\nimport 'package:pixes/network/res.dart';\n\nimport 'models.dart';\n\nexport 'models.dart';\nexport 'res.dart';\n\npart 'novel.dart';\n\nclass Network {\n  static const hashSalt =\n      \"28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c\";\n\n  static const baseUrl = 'https://app-api.pixiv.net';\n  static const oauthUrl = 'https://oauth.secure.pixiv.net';\n\n  static const String clientID = \"MOBrBDS8blbauoSck0ZfDbtuzpyT\";\n  static const String clientSecret = \"lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj\";\n  static const String refreshClientID = \"KzEZED7aC0vird8jWyHM38mXjNTY\";\n  static const String refreshClientSecret =\n      \"W9JZoJe00qPvJsiyCGT3CCtC6ZUtdpKpzMbNlUGP\";\n\n  static Network? instance;\n\n  factory Network() => instance ?? (instance = Network._create());\n\n  Network._create();\n\n  String? codeVerifier;\n\n  String? get token => appdata.account?.accessToken;\n\n  final dio = AppDio();\n\n  Map<String, String> get headers {\n    final time =\n        DateFormat(\"yyyy-MM-dd'T'HH:mm:ss'+00:00'\").format(DateTime.now());\n    final hash = md5.convert(utf8.encode(time + hashSalt)).toString();\n    return {\n      \"X-Client-Time\": time,\n      \"X-Client-Hash\": hash,\n      \"User-Agent\": \"PixivAndroidApp/5.0.234 (Android 14.0; Pixes)\",\n      \"accept-language\": App.locale.toLanguageTag(),\n      \"Accept-Encoding\": \"gzip\",\n      if (token != null) \"Authorization\": \"Bearer $token\"\n    };\n  }\n\n  Future<String> generateWebviewUrl() async {\n    const String chars =\n        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n    codeVerifier =\n        List.generate(128, (i) => chars[Random.secure().nextInt(chars.length)])\n            .join();\n    final codeChallenge = base64Url\n        .encode(sha256.convert(ascii.encode(codeVerifier!)).bytes)\n        .replaceAll('=', '');\n    return \"https://app-api.pixiv.net/web/v1/login?code_challenge=$codeChallenge&code_challenge_method=S256&client=pixiv-android\";\n  }\n\n  Future<Res<bool>> loginWithCode(String code) async {\n    try {\n      var res = await dio.post<String>(\"$oauthUrl/auth/token\",\n          data: {\n            \"client_id\": clientID,\n            \"client_secret\": clientSecret,\n            \"code\": code,\n            \"code_verifier\": codeVerifier,\n            \"grant_type\": \"authorization_code\",\n            \"include_policy\": \"true\",\n            \"redirect_uri\":\n                \"https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback\",\n          },\n          options: Options(\n              contentType: Headers.formUrlEncodedContentType,\n              headers: headers));\n      if (res.statusCode != 200) {\n        throw \"Invalid Status code ${res.statusCode}\";\n      }\n      final data = json.decode(res.data!);\n      appdata.account = Account.fromJson(data);\n      appdata.writeData();\n      return const Res(true);\n    } catch (e, s) {\n      Log.error(\"Network\", \"$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  Future<Res<bool>> refreshToken() async {\n    try {\n      var res = await dio.post<String>(\"$oauthUrl/auth/token\",\n          data: {\n            \"client_id\": clientID,\n            \"client_secret\": clientSecret,\n            \"grant_type\": \"refresh_token\",\n            \"refresh_token\": appdata.account?.refreshToken,\n            \"include_policy\": \"true\",\n          },\n          options: Options(\n              contentType: Headers.formUrlEncodedContentType,\n              validateStatus: (i) => true,\n              headers: headers));\n      if (res.statusCode != 200) {\n        var data = res.data ?? \"\";\n        if (data.contains(\"Invalid refresh token\")) {\n          throw \"Failed to refresh token. Please log out.\";\n        }\n      }\n      var account = Account.fromJson(json.decode(res.data!));\n      appdata.account = account;\n      appdata.writeData();\n      return const Res(true);\n    } catch (e, s) {\n      Log.error(\"Network\", \"$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  Future<Res<Map<String, dynamic>>> apiGet(String path,\n      {Map<String, dynamic>? query}) async {\n    try {\n      if (!path.startsWith(\"http\")) {\n        path = \"$baseUrl$path\";\n      }\n      final res = await dio.get<Map<String, dynamic>>(path,\n          queryParameters: query,\n          options: Options(headers: headers, validateStatus: (status) => true));\n      if (res.statusCode == 200) {\n        return Res(res.data!);\n      } else if (res.statusCode == 400) {\n        if (res.data.toString().contains(\"Access Token\")) {\n          var refresh = await refreshToken();\n          if (refresh.success) {\n            return apiGet(path, query: query);\n          } else {\n            return Res.error(refresh.errorMessage);\n          }\n        } else {\n          return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n        }\n      } else if ((res.statusCode ?? 500) < 500) {\n        return Res.error(res.data?[\"error\"]?[\"message\"] ??\n            \"Invalid Status code ${res.statusCode}\");\n      } else {\n        return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n      }\n    } catch (e, s) {\n      Log.error(\"Network\", \"$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  Future<Res<String>> apiGetPlain(String path,\n      {Map<String, dynamic>? query}) async {\n    try {\n      if (!path.startsWith(\"http\")) {\n        path = \"$baseUrl$path\";\n      }\n      final res = await dio.get<String>(path,\n          queryParameters: query,\n          options: Options(headers: headers, validateStatus: (status) => true));\n      if (res.statusCode == 200) {\n        return Res(res.data!);\n      } else if (res.statusCode == 400) {\n        if (res.data.toString().contains(\"Access Token\")) {\n          var refresh = await refreshToken();\n          if (refresh.success) {\n            return apiGetPlain(path, query: query);\n          } else {\n            return Res.error(refresh.errorMessage);\n          }\n        } else {\n          return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n        }\n      } else {\n        return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n      }\n    } catch (e, s) {\n      Log.error(\"Network\", \"$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  String? encodeFormData(Map<String, dynamic>? data) {\n    if (data == null) return null;\n    StringBuffer buffer = StringBuffer();\n    data.forEach((key, value) {\n      if (value is List) {\n        for (var element in value) {\n          buffer.write(\"$key[]=$element&\");\n        }\n      } else {\n        buffer.write(\"$key=$value&\");\n      }\n    });\n    return buffer.toString();\n  }\n\n  Future<Res<Map<String, dynamic>>> apiPost(String path,\n      {Map<String, dynamic>? query, Map<String, dynamic>? data}) async {\n    try {\n      if (!path.startsWith(\"http\")) {\n        path = \"$baseUrl$path\";\n      }\n      final res = await dio.post<Map<String, dynamic>>(path,\n          queryParameters: query,\n          data: encodeFormData(data),\n          options: Options(\n              headers: headers,\n              validateStatus: (status) => true,\n              contentType: Headers.formUrlEncodedContentType));\n      if (res.statusCode == 200) {\n        return Res(res.data!);\n      } else if (res.statusCode == 400) {\n        if (res.data.toString().contains(\"Access Token\")) {\n          var refresh = await refreshToken();\n          if (refresh.success) {\n            return apiGet(path, query: query);\n          } else {\n            return Res.error(refresh.errorMessage);\n          }\n        } else {\n          return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n        }\n      } else if ((res.statusCode ?? 500) < 500) {\n        return Res.error(res.data?[\"error\"]?[\"message\"] ??\n            \"Invalid Status code ${res.statusCode}\");\n      } else {\n        return Res.error(\"Invalid Status Code: ${res.statusCode}\");\n      }\n    } catch (e, s) {\n      Log.error(\"Network\", \"$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  /// get user details\n  Future<Res<UserDetails>> getUserDetails(Object userId) async {\n    var res = await apiGet(\"/v1/user/detail\",\n        query: {\"user_id\": userId, \"filter\": \"for_android\"});\n    if (res.success) {\n      return Res(UserDetails.fromJson(res.data));\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  static const recommendationUrl =\n      \"/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true\";\n\n  Future<Res<List<Illust>>> getRecommendedIllusts() async {\n    var res = await apiGet(recommendationUrl);\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: recommendationUrl);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getBookmarkedIllusts(String restrict,\n      [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ??\n        \"/v1/user/bookmarks/illust?user_id=${appdata.account?.user.id}&restrict=$restrict\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getUserBookmarks(String uid,\n      [String? nextUrl]) async {\n    var res = await apiGet(\n        nextUrl ?? \"/v1/user/bookmarks/illust?user_id=$uid&restrict=public\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<bool>> addBookmark(String id, String method,\n      [String type = \"public\"]) async {\n    var res = method == \"add\"\n        ? await apiPost(\"/v2/illust/bookmark/$method\",\n            data: {\"illust_id\": id, \"restrict\": type})\n        : await apiPost(\"/v1/illust/bookmark/$method\", data: {\n            \"illust_id\": id,\n          });\n    if (!res.error) {\n      return const Res(true);\n    } else {\n      return Res.fromErrorRes(res);\n    }\n  }\n\n  Future<Res<bool>> follow(String uid, String method,\n      [String type = \"public\"]) async {\n    var res = method == \"add\"\n        ? await apiPost(\"/v1/user/follow/add\",\n            data: {\"user_id\": uid, \"restrict\": type})\n        : await apiPost(\"/v1/user/follow/delete\", data: {\n            \"user_id\": uid,\n          });\n    if (!res.error) {\n      return const Res(true);\n    } else {\n      return Res.fromErrorRes(res);\n    }\n  }\n\n  Future<Res<List<TrendingTag>>> getHotTags() async {\n    var res = await apiGet(\n        \"/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true\");\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    } else {\n      return Res(List.from(res.data[\"trend_tags\"].map((e) => TrendingTag(\n          Tag(e[\"tag\"], e[\"translated_name\"]), Illust.fromJson(e[\"illust\"])))));\n    }\n  }\n\n  Future<Res<List<Illust>>> search(\n      String keyword, SearchOptions options) async {\n    String path = \"\";\n    final encodedKeyword = Uri.encodeComponent(keyword +\n        options.favoriteNumber.toParam() +\n        options.ageLimit.toParam());\n    if (options.sort == SearchSort.popular && !options.sort.isPremium) {\n      path =\n          \"/v1/search/popular-preview/illust?filter=for_android&include_translated_tag_results=true&merge_plain_keyword_results=true&word=$encodedKeyword&search_target=${options.matchType.toParam()}\";\n    } else {\n      path =\n          \"/v1/search/illust?filter=for_android&include_translated_tag_results=true&merge_plain_keyword_results=true&word=$encodedKeyword&sort=${options.sort.toParam()}&search_target=${options.matchType.toParam()}\";\n    }\n\n    var res = await apiGet(path);\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getIllustsWithNextUrl(String nextUrl) async {\n    var res = await apiGet(nextUrl);\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<UserPreview>>> searchUsers(String keyword,\n      [String? nextUrl]) async {\n    var path = nextUrl ??\n        \"/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}\";\n    var res = await apiGet(path);\n    if (res.success) {\n      return Res(\n          (res.data[\"user_previews\"] as List)\n              .map((e) => UserPreview.fromJson(e))\n              .toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getUserIllusts(String uid, String? type) async {\n    var res = await apiGet(\n        \"/v1/user/illusts?filter=for_android&user_id=$uid${type != null ? \"&type=$type\" : \"\"}\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<UserPreview>>> getFollowing(String uid, String type,\n      [String? nextUrl]) async {\n    var path = nextUrl ??\n        \"/v1/user/following?filter=for_android&user_id=$uid&restrict=$type\";\n    var res = await apiGet(path);\n    if (res.success) {\n      return Res(\n          (res.data[\"user_previews\"] as List)\n              .map((e) => UserPreview.fromJson(e))\n              .toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getFollowingArtworks(String restrict,\n      [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ?? \"/v2/illust/follow?restrict=$restrict\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<UserPreview>>> getRecommendationUsers() async {\n    var res = await apiGet(\"/v1/user/recommended?filter=for_android\");\n    if (res.success) {\n      return Res(\n          (res.data[\"user_previews\"] as List)\n              .map((e) => UserPreview.fromJson(e))\n              .toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  /// mode: day, week, month, day_male, day_female, week_original, week_rookie, day_manga, week_manga, month_manga, day_r18_manga, day_r18\n  Future<Res<List<Illust>>> getRanking(String mode, [String? nextUrl]) async {\n    var res = await apiGet(\n        nextUrl ?? \"/v1/illust/ranking?filter=for_android&mode=$mode\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Comment>>> getComments(String id, [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ?? \"/v3/illust/comments?illust_id=$id\");\n    if (res.success) {\n      return Res(\n          (res.data[\"comments\"] as List)\n              .map((e) => Comment.fromJson(e))\n              .toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<bool>> comment(String id, String content) async {\n    var res = await apiPost(\"/v1/illust/comment/add\",\n        data: {\"illust_id\": id, \"comment\": content});\n    if (res.success) {\n      return const Res(true);\n    } else {\n      return Res.fromErrorRes(res);\n    }\n  }\n\n  Future<Res<Illust>> getIllustByID(String id) async {\n    var res = await apiGet(\"/v1/illust/detail?illust_id=$id\");\n    if (res.success) {\n      return Res(Illust.fromJson(res.data[\"illust\"]));\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getRecommendedMangas() async {\n    var res = await apiGet(\n        \"/v1/manga/recommended?filter=for_android&include_ranking_illusts=true&include_privacy_policy=true\");\n    if (res.success) {\n      return Res(\n          (res.data[\"illusts\"] as List).map((e) => Illust.fromJson(e)).toList(),\n          subData: res.data[\"next_url\"]);\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> getHistory(int page) async {\n    String param = \"\";\n    if (page > 1) {\n      param = \"?offset=${30 * (page - 1)}\";\n    }\n    var res = await apiGet(\"/v1/user/browsing-history/illusts$param\");\n    if (res.success) {\n      return Res((res.data[\"illusts\"] as List)\n          .map((e) => Illust.fromJson(e))\n          .toList());\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<MuteList>> getMuteList() async {\n    var res = await apiGet(\"/v1/mute/list\");\n    if (res.success) {\n      return Res(MuteList.fromJson(res.data));\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<bool>> editMute(List<String> addTags, List<String> addUsers,\n      List<String> deleteTags, List<String> deleteUsers) async {\n    var res = await apiPost(\"/v1/mute/edit\",\n        data: {\n          \"add_tags\": addTags,\n          \"add_user_ids\": addUsers,\n          \"delete_tags\": deleteTags,\n          \"delete_user_ids\": deleteUsers\n        }..removeWhere((key, value) => value.isEmpty));\n    if (res.success) {\n      return const Res(true);\n    } else {\n      return Res.fromErrorRes(res);\n    }\n  }\n\n  Future<Res<List<UserPreview>>> relatedUsers(String id) async {\n    var res =\n        await apiGet(\"/v1/user/related?filter=for_android&seed_user_id=$id\");\n    if (res.success) {\n      return Res((res.data[\"user_previews\"] as List)\n          .map((e) => UserPreview.fromJson(e))\n          .toList());\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<List<Illust>>> relatedIllusts(String id) async {\n    var res =\n        await apiGet(\"/v2/illust/related?filter=for_android&illust_id=$id\");\n    if (res.success) {\n      return Res((res.data[\"illusts\"] as List)\n          .map((e) => Illust.fromJson(e))\n          .toList());\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<String>> getNovelImage(String novelId, String imageId) async {\n    var res = await apiGetPlain(\n        \"/web/v1/novel/image?novel_id=$novelId&uploaded_image_id=$imageId\");\n    if (res.success) {\n      var html = res.data;\n      int start = html.indexOf('<img src=\"') + 10;\n      int end = html.indexOf('\"', start);\n      return Res(html.substring(start, end));\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n\n  Future<Res<bool>> sendHistory(List<int> ids) async {\n    var res = await apiPost(\"/v2/user/browsing-history/illust/add\",\n        data: {\"illust_ids\": ids});\n    if (res.success) {\n      return const Res(true);\n    } else {\n      return Res.fromErrorRes(res);\n    }\n  }\n\n  Future<Res<List<Tag>>> getAutoCompleteTags(String keyword) async {\n    var res = await apiGet(\"/v2/search/autocomplete?merge_plain_keyword_results=true&word=${Uri.encodeComponent(keyword)}\");\n    if (res.success) {\n      return Res((res.data[\"tags\"] as List).map((e) => Tag.fromJson(e)).toList());\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/novel.dart",
    "content": "part of \"network.dart\";\n\nextension NovelExt on Network {\n  Future<Res<List<Novel>>> getRecommendNovels() {\n    return getNovelsWithNextUrl(\"/v1/novel/recommended\");\n  }\n\n  Future<Res<List<Novel>>> getNovelsWithNextUrl(String nextUrl) async {\n    var res = await apiGet(nextUrl);\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return Res(\n        (res.data[\"novels\"] as List).map((e) => Novel.fromJson(e)).toList(),\n        subData: res.data[\"next_url\"]);\n  }\n\n  Future<Res<List<Novel>>> searchNovels(String keyword, SearchOptions options) {\n    var url = \"/v1/search/novel?\"\n        \"include_translated_tag_results=true&\"\n        \"merge_plain_keyword_results=true&\"\n        \"word=${Uri.encodeComponent(keyword)}&\"\n        \"sort=${options.sort.toParam()}&\"\n        \"search_target=${options.matchType.toParam()}&\"\n        \"search_ai_type=0\";\n    return getNovelsWithNextUrl(url);\n  }\n\n  /// mode: day, day_male, day_female, week_rookie, week, week_ai\n  Future<Res<List<Novel>>> getNovelRanking(String mode, DateTime? date) {\n    var url = \"/v1/novel/ranking?mode=$mode\";\n    if (date != null) {\n      url += \"&date=${date.year}-${date.month}-${date.day}\";\n    }\n    return getNovelsWithNextUrl(url);\n  }\n\n  Future<Res<List<Novel>>> getBookmarkedNovels(String uid, bool public) {\n    return getNovelsWithNextUrl(\n        \"/v1/user/bookmarks/novel?user_id=$uid&restrict=${public ? \"public\" : \"private\"}\");\n  }\n\n  Future<Res<bool>> favoriteNovel(String id, bool public) async {\n    var res = await apiPost(\"/v2/novel/bookmark/add\", data: {\n      \"novel_id\": id,\n      \"restrict\": public ? \"public\" : \"private\",\n    });\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return const Res(true);\n  }\n\n  Future<Res<bool>> deleteFavoriteNovel(String id) async {\n    var res = await apiPost(\"/v1/novel/bookmark/delete\", data: {\n      \"novel_id\": id,\n    });\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return const Res(true);\n  }\n\n  Future<Res<String>> getNovelContent(String id) async {\n    var res = await apiGetPlain(\n        \"/webview/v2/novel?id=$id&font=default&font_size=16.0px&line_height=1.75&color=%23101010&background_color=%23EFEFEF&margin_top=56px&margin_bottom=48px&theme=light&use_block=true&viewer_version=20221031_ai\");\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    try {\n      var html = res.data;\n      int start = html.indexOf(\"novel:\");\n      while (html[start] != '{') {\n        start++;\n      }\n      int leftCount = 0;\n      int end = start;\n      for (end = start; end < html.length; end++) {\n        if (html[end] == '{') {\n          leftCount++;\n        } else if (html[end] == '}') {\n          leftCount--;\n        }\n        if (leftCount == 0) {\n          end++;\n          break;\n        }\n      }\n      var json = jsonDecode(html.substring(start, end));\n      return Res(json['text']);\n    } catch (e, s) {\n      Log.error(\n          \"Data Convert\", \"Failed to analyze html novel content: \\n$e\\n$s\");\n      return Res.error(e);\n    }\n  }\n\n  Future<Res<List<Novel>>> relatedNovels(String id) async {\n    var res = await apiPost(\"/v1/novel/related\", data: {\n      \"novel_id\": id,\n    });\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return Res(\n        (res.data[\"novels\"] as List).map((e) => Novel.fromJson(e)).toList());\n  }\n\n  Future<Res<List<Novel>>> getUserNovels(String uid) {\n    return getNovelsWithNextUrl(\"/v1/user/novels?user_id=$uid\");\n  }\n\n  Future<Res<List<Novel>>> getNovelSeries(String id, [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ?? \"/v2/novel/series?series_id=$id\");\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return Res(\n        (res.data[\"novels\"] as List).map((e) => Novel.fromJson(e)).toList(),\n        subData: res.data[\"next_url\"]);\n  }\n\n  Future<Res<List<Comment>>> getNovelComments(String id,\n      [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ?? \"/v1/novel/comments?novel_id=$id\");\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return Res(\n        (res.data[\"comments\"] as List).map((e) => Comment.fromJson(e)).toList(),\n        subData: res.data[\"next_url\"]);\n  }\n\n  Future<Res<bool>> commentNovel(String id, String content) async {\n    var res = await apiPost(\"/v1/novel/comment/add\", data: {\n      \"novel_id\": id,\n      \"content\": content,\n    });\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return const Res(true);\n  }\n\n  Future<Res<Novel>> getNovelDetail(String id) async {\n    var res = await apiGet(\"/v2/novel/detail?novel_id=$id\");\n    if (res.error) {\n      return Res.fromErrorRes(res);\n    }\n    return Res(Novel.fromJson(res.data[\"novel\"]));\n  }\n\n  Future<Res<List<Novel>>> getFollowingNovels(String restrict,\n      [String? nextUrl]) async {\n    var res = await apiGet(nextUrl ?? \"/v1/novel/follow?restrict=$restrict\");\n    if (res.success) {\n      return Res(\n        (res.data[\"novels\"] as List).map((e) => Novel.fromJson(e)).toList(),\n        subData: res.data[\"next_url\"],\n      );\n    } else {\n      return Res.error(res.errorMessage);\n    }\n  }\n}\n"
  },
  {
    "path": "lib/network/res.dart",
    "content": "import 'package:flutter/cupertino.dart';\n\n@immutable\nclass Res<T>{\n  ///error info\n  final String? errorMessage;\n\n  String get errorMessageWithoutNull => errorMessage??\"Unknown Error\";\n\n  /// data\n  final T? _data;\n\n  /// is there an error\n  bool get error => errorMessage!=null || _data==null;\n\n  /// whether succeed\n  bool get success => !error;\n\n  /// data\n  ///\n  /// must be called when no error happened, or it will throw error\n  T get data => _data ?? (throw Exception(errorMessage));\n\n  /// get data, or null if there is an error\n  T? get dataOrNull => _data;\n\n  final dynamic subData;\n\n  @override\n  String toString() => _data.toString();\n\n  Res.fromErrorRes(Res another, {this.subData}):\n        _data=null,errorMessage=another.errorMessageWithoutNull;\n\n  /// network result\n  const Res(this._data,{this.errorMessage, this.subData});\n\n  Res.error(dynamic e):errorMessage=e.toString(), _data=null, subData=null;\n}"
  },
  {
    "path": "lib/network/translator.dart",
    "content": "import 'package:pixes/network/app_dio.dart';\n\nabstract class Translator {\n  static Translator? _instance;\n\n  static Translator get instance {\n    if (_instance == null) {\n      init();\n    }\n    return _instance!;\n  }\n\n  static void init() {\n    _instance = GoogleTranslator();\n  }\n\n  /// Translates the given [text] to the given [to] language.\n  Future<String> translate(String text, String to);\n}\n\nclass GoogleTranslator implements Translator {\n  final Dio _dio = AppDio();\n\n  String get url => 'https://translate.google.com/translate_a/single';\n\n  Map<String, dynamic> buildBody(String text, String to) {\n    return {\n      'q': text,\n      'client': 'at',\n      'sl': 'auto',\n      'tl': to,\n      'dt': 't',\n      'ie': 'UTF-8',\n      'oe': 'UTF-8',\n      'dj': '1',\n    };\n  }\n\n  Future<String> translatePart(String part, String to) async {\n    final response = await _dio.post(\n      url,\n      data: buildBody(part, to),\n      options: Options(\n        headers: {\n          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',\n        },\n      ),\n    );\n    var buffer = StringBuffer();\n    for(var e in response.data['sentences']) {\n      buffer.write(e['trans']);\n    }\n    return buffer.toString();\n  }\n\n  @override\n  Future<String> translate(String text, String to) async {\n    final lines = text.split('\\n');\n    var buffer = StringBuffer();\n    var result = '';\n    for(int i=0; i<lines.length; i++) {\n      final line = lines[i];\n      if (buffer.length + line.length > 5000) {\n        result += await translatePart(buffer.toString(), to);\n        buffer.clear();\n      }\n      buffer.write(line);\n      buffer.write('\\n');\n    }\n    if (buffer.isNotEmpty) {\n      result += await translatePart(buffer.toString(), to);\n    }\n    return result;\n  }\n}"
  },
  {
    "path": "lib/pages/bookmarks.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/components/batch_download.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/illust_widget.dart';\nimport '../components/loading.dart';\n\nclass BookMarkedArtworkPage extends StatefulWidget {\n  const BookMarkedArtworkPage({super.key});\n\n  @override\n  State<BookMarkedArtworkPage> createState() => _BookMarkedArtworkPageState();\n}\n\nclass _BookMarkedArtworkPageState extends State<BookMarkedArtworkPage>{\n  String restrict = \"public\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        buildTab(),\n        Expanded(\n          child: _OneBookmarkedPage(restrict, key: Key(restrict),),\n        )\n      ],\n    );\n  }\n\n  Widget buildTab() {\n    return TitleBar(\n      title: \"Bookmarks\".tl,\n      action: Row(\n        children: [\n          BatchDownloadButton(request: () => Network().getBookmarkedIllusts(restrict)),\n          const SizedBox(width: 8,),\n          SegmentedButton(\n            options: [\n              SegmentedButtonOption(\"public\", \"Public\".tl),\n              SegmentedButtonOption(\"private\", \"Private\".tl),\n            ],\n            onPressed: (key) {\n              if(key != restrict) {\n                setState(() {\n                  restrict = key;\n                });\n              }\n            },\n            value: restrict,\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass _OneBookmarkedPage extends StatefulWidget {\n  const _OneBookmarkedPage(this.restrict, {super.key});\n\n  final String restrict;\n\n  @override\n  State<_OneBookmarkedPage> createState() => _OneBookmarkedPageState();\n}\n\nclass _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage, Illust> {\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    return LayoutBuilder(builder: (context, constrains){\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8)\n            + EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if(index == data.length - 1){\n            nextPage();\n          }\n          return IllustWidget(data[index], onTap: () {\n            context.to(() => IllustGalleryPage(\n              illusts: data,\n              initialPage: index,\n              nextUrl: nextUrl\n            ));\n          },);\n        },\n      );\n    });\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async{\n    if(nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().getBookmarkedIllusts(widget.restrict, nextUrl);\n    if(!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\n"
  },
  {
    "path": "lib/pages/comments_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/md.dart';\nimport '../components/message.dart';\n\nclass CommentsPage extends StatefulWidget {\n  const CommentsPage(this.id, {this.isNovel = false, super.key});\n\n  final String id;\n\n  final bool isNovel;\n\n  static void show(BuildContext context, String id, {bool isNovel = false}) {\n    Navigator.of(context)\n        .push(SideBarRoute(CommentsPage(id, isNovel: isNovel)));\n  }\n\n  @override\n  State<CommentsPage> createState() => _CommentsPageState();\n}\n\nclass _CommentsPageState extends MultiPageLoadingState<CommentsPage, Comment> {\n  bool isCommenting = false;\n\n  @override\n  Widget buildContent(BuildContext context, List<Comment> data) {\n    return Stack(\n      children: [\n        Positioned.fill(child: buildBody(context, data)),\n        Positioned(\n          bottom: 0,\n          left: 0,\n          right: 0,\n          child: buildBottom(context),\n        )\n      ],\n    );\n  }\n\n  Widget buildBody(BuildContext context, List<Comment> data) {\n    return ListView.builder(\n        padding: EdgeInsets.zero,\n        itemCount: data.length + 2,\n        itemBuilder: (context, index) {\n          if (index == 0) {\n            return Text(\"Comments\".tl, style: const TextStyle(fontSize: 20))\n                .paddingVertical(16)\n                .paddingHorizontal(12);\n          } else if (index == data.length + 1) {\n            return const SizedBox(\n              height: 64,\n            );\n          }\n          index--;\n          var date = data[index].date;\n          var dateText = \"${date.year}/${date.month}/${date.day}\";\n          return Card(\n            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),\n            margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              children: [\n                Row(\n                  children: [\n                    SizedBox(\n                      height: 38,\n                      width: 38,\n                      child: ClipRRect(\n                        borderRadius: BorderRadius.circular(38),\n                        child: ColoredBox(\n                          color: ColorScheme.of(context).secondaryContainer,\n                          child: GestureDetector(\n                            onTap: () => context.to(\n                                () => UserInfoPage(data[index].id.toString())),\n                            child: AnimatedImage(\n                              image: CachedImageProvider(data[index].avatar),\n                              width: 38,\n                              height: 38,\n                              fit: BoxFit.cover,\n                              filterQuality: FilterQuality.medium,\n                            ),\n                          ),\n                        ),\n                      ),\n                    ),\n                    const SizedBox(\n                      width: 8,\n                    ),\n                    Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Text(\n                          data[index].name,\n                          style: const TextStyle(fontSize: 14),\n                        ),\n                        Text(\n                          dateText,\n                          style: TextStyle(\n                              fontSize: 12,\n                              color: ColorScheme.of(context).outline),\n                        )\n                      ],\n                    )\n                  ],\n                ),\n                const SizedBox(\n                  height: 8,\n                ),\n                if (data[index].comment.isNotEmpty)\n                  Text(\n                    data[index].comment,\n                    style: const TextStyle(fontSize: 16),\n                  ),\n                if (data[index].stampUrl != null)\n                  SizedBox(\n                    height: 64,\n                    width: 64,\n                    child: ClipRRect(\n                      borderRadius: BorderRadius.circular(4),\n                      child: AnimatedImage(\n                        image: CachedImageProvider(data[index].stampUrl!),\n                        width: 64,\n                        height: 64,\n                        fit: BoxFit.cover,\n                      ),\n                    ),\n                  )\n              ],\n            ),\n          );\n        });\n  }\n\n  Widget buildBottom(BuildContext context) {\n    return Card(\n      padding: EdgeInsets.zero,\n      backgroundColor:\n          FluentTheme.of(context).micaBackgroundColor.toOpacity(0.96),\n      child: SizedBox(\n        height: 52,\n        child: TextBox(\n          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),\n          placeholder: \"Comment\".tl,\n          foregroundDecoration: WidgetStatePropertyAll(BoxDecoration(\n            border: Border.all(color: Colors.transparent),\n          )),\n          onSubmitted: (s) {\n            showToast(context, message: \"Sending\".tl);\n            if (isCommenting) return;\n            setState(() {\n              isCommenting = true;\n            });\n            if (widget.isNovel) {\n              Network().commentNovel(widget.id, s).then((value) {\n                if (value.error) {\n                  if (context.mounted) {\n                    context.showToast(message: \"Network Error\");\n                    setState(() {\n                      isCommenting = false;\n                    });\n                  }\n                } else {\n                  isCommenting = false;\n                  nextUrl = null;\n                  reset();\n                }\n              });\n            } else {\n              Network().comment(widget.id, s).then((value) {\n                if (value.error) {\n                  if(context.mounted) {\n                    context.showToast(message: \"Network Error\");\n                    setState(() {\n                      isCommenting = false;\n                    });\n                  }\n                } else {\n                  isCommenting = false;\n                  nextUrl = null;\n                  reset();\n                }\n              });\n            }\n          },\n        ).paddingVertical(8).paddingHorizontal(12),\n      ).paddingBottom(context.padding.bottom + context.viewInsets.bottom),\n    );\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Comment>>> loadData(int page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = widget.isNovel\n        ? await Network().getNovelComments(widget.id, nextUrl)\n        : await Network().getComments(widget.id, nextUrl);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/downloaded_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/services.dart';\nimport 'package:photo_view/photo_view_gallery.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/download.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:share_plus/share_plus.dart';\nimport 'package:window_manager/window_manager.dart';\n\nimport '../utils/io.dart';\nimport 'main_page.dart';\n\nclass DownloadedPage extends StatefulWidget {\n  const DownloadedPage({super.key});\n\n  @override\n  State<DownloadedPage> createState() => _DownloadedPageState();\n}\n\nclass _DownloadedPageState extends State<DownloadedPage> {\n  var illusts = <DownloadedIllust>[];\n  var flyoutControllers = <FlyoutController>[];\n\n  void loadData() {\n    illusts = DownloadManager().listAll();\n    flyoutControllers =\n        List.generate(illusts.length, (index) => FlyoutController());\n  }\n\n  @override\n  void initState() {\n    loadData();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        TitleBar(\n          title: \"Downloaded\".tl,\n          action: Button(\n            child: Text(\"Delete Invalid Items\".tl),\n            onPressed: () async {\n              await DownloadManager().checkAndClearInvalidItems();\n              setState(() {\n                loadData();\n              });\n            },\n          ),\n        ),\n        Expanded(\n          child: buildBody(),\n        ),\n      ],\n    );\n  }\n\n  Widget buildBody() {\n    return GridViewWithFixedItemHeight(\n        itemCount: illusts.length,\n        itemHeight: 152,\n        maxCrossAxisExtent: 742,\n        builder: (context, index) {\n          var image = DownloadManager().getImage(illusts[index].illustId, 0);\n          return Card(\n            margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\n            padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),\n            child: GestureDetector(\n              behavior: HitTestBehavior.opaque,\n              onTap: () {\n                App.rootNavigatorKey.currentContext?.to(() =>\n                    _DownloadedIllustViewPage(DownloadManager()\n                        .getImagePaths(illusts[index].illustId)));\n              },\n              child: Row(\n                children: [\n                  Container(\n                    width: 96,\n                    height: double.infinity,\n                    decoration: BoxDecoration(\n                        borderRadius: BorderRadius.circular(4),\n                        color: ColorScheme.of(context).secondaryContainer),\n                    clipBehavior: Clip.antiAlias,\n                    child: image == null\n                        ? null\n                        : AnimatedImage(\n                            image: FileImage(image),\n                            fit: BoxFit.cover,\n                            width: 96,\n                            height: double.infinity,\n                            filterQuality: FilterQuality.medium,\n                          ),\n                  ),\n                  const SizedBox(width: 16),\n                  Expanded(\n                    child: Column(\n                      crossAxisAlignment: CrossAxisAlignment.start,\n                      children: [\n                        Text(\n                          illusts[index].title,\n                          style: const TextStyle(\n                            fontSize: 16,\n                            fontWeight: FontWeight.bold,\n                          ),\n                          maxLines: 1,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                        const SizedBox(height: 4),\n                        Text(\n                          illusts[index].author,\n                          style: const TextStyle(\n                            fontSize: 12,\n                          ),\n                          maxLines: 1,\n                          overflow: TextOverflow.ellipsis,\n                        ),\n                        Text(\n                          \"${illusts[index].imageCount}P\",\n                          style: const TextStyle(\n                            fontSize: 12,\n                          ),\n                        ),\n                        const Spacer(),\n                        Row(\n                          children: [\n                            const Spacer(),\n                            Button(\n                              child: Text(\"Info\".tl).fixWidth(42),\n                              onPressed: () {\n                                context.to(() => IllustPageWithId(\n                                    illusts[index].illustId.toString()));\n                              },\n                            ),\n                            const SizedBox(width: 6),\n                            FlyoutTarget(\n                              controller: flyoutControllers[index],\n                              child: Button(\n                                child: Text(\"Delete\".tl).fixWidth(42),\n                                onPressed: () {\n                                  flyoutControllers[index].showFlyout(\n                                      navigatorKey:\n                                          App.rootNavigatorKey.currentState,\n                                      builder: (context) {\n                                        return FlyoutContent(\n                                          child: Column(\n                                            mainAxisSize: MainAxisSize.min,\n                                            crossAxisAlignment:\n                                                CrossAxisAlignment.start,\n                                            children: [\n                                              Text(\n                                                'Are you sure you want to delete?'\n                                                    .tl,\n                                                style: const TextStyle(\n                                                    fontWeight:\n                                                        FontWeight.bold),\n                                              ),\n                                              const SizedBox(height: 12.0),\n                                              Button(\n                                                onPressed: () {\n                                                  Flyout.of(context).close();\n                                                  DownloadManager()\n                                                      .delete(illusts[index]);\n                                                  setState(() {\n                                                    illusts.removeAt(index);\n                                                    flyoutControllers\n                                                        .removeAt(index);\n                                                  });\n                                                },\n                                                child: Text('Yes'.tl),\n                                              ),\n                                            ],\n                                          ),\n                                        );\n                                      });\n                                },\n                              ),\n                            ),\n                          ],\n                        ),\n                      ],\n                    ),\n                  ),\n                ],\n              ),\n            ),\n          );\n        }).paddingHorizontal(8);\n  }\n}\n\nclass _DownloadedIllustViewPage extends StatefulWidget {\n  const _DownloadedIllustViewPage(this.imagePaths);\n\n  final List<String> imagePaths;\n\n  @override\n  State<_DownloadedIllustViewPage> createState() =>\n      _DownloadedIllustViewPageState();\n}\n\nclass _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage>\n    with WindowListener {\n  int windowButtonKey = 0;\n\n  @override\n  void initState() {\n    windowManager.addListener(this);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    windowManager.removeListener(this);\n    super.dispose();\n  }\n\n  @override\n  void onWindowMaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  @override\n  void onWindowUnmaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  var controller = PageController();\n\n  int currentPage = 0;\n\n  var menuController = FlyoutController();\n\n  Future<File?> getFile() async {\n    var file = File(widget.imagePaths[currentPage]);\n    if (file.existsSync()) {\n      return file;\n    }\n    return null;\n  }\n\n  void showMenu() {\n    menuController.showFlyout(\n        builder: (context) => MenuFlyout(\n              items: [\n                MenuFlyoutItem(\n                    text: Text(\"Save to\".tl),\n                    onPressed: () async {\n                      var file = await getFile();\n                      if (file != null) {\n                        saveFile(file);\n                      }\n                    }),\n                MenuFlyoutItem(\n                    text: Text(\"Share\".tl),\n                    onPressed: () async {\n                      var file = await getFile();\n                      if (file != null) {\n                        var ext = file.path.split('.').last;\n                        var mediaType = switch (ext) {\n                          'jpg' => 'image/jpeg',\n                          'jpeg' => 'image/jpeg',\n                          'png' => 'image/png',\n                          'gif' => 'image/gif',\n                          'webp' => 'image/webp',\n                          _ => 'application/octet-stream'\n                        };\n                        Share.shareXFiles([\n                          XFile(file.path,\n                              mimeType: mediaType,\n                              name: file.path.split('/').last)\n                        ]);\n                      }\n                    }),\n              ],\n            ));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),\n      color: FluentTheme.of(context).micaBackgroundColor,\n      child: Listener(\n        onPointerSignal: (event) {\n          if (event is PointerScrollEvent &&\n              !HardwareKeyboard.instance.isControlPressed) {\n            if (event.scrollDelta.dy > 0 &&\n                controller.page!.toInt() < widget.imagePaths.length - 1) {\n              controller.jumpToPage(controller.page!.toInt() + 1);\n            } else if (event.scrollDelta.dy < 0 &&\n                controller.page!.toInt() > 0) {\n              controller.jumpToPage(controller.page!.toInt() - 1);\n            }\n          }\n        },\n        child: LayoutBuilder(\n          builder: (context, constrains) {\n            var height = constrains.maxHeight;\n            return Stack(\n              children: [\n                Positioned.fill(\n                    child: PhotoViewGallery.builder(\n                  pageController: controller,\n                  backgroundDecoration:\n                      const BoxDecoration(color: Colors.transparent),\n                  itemCount: widget.imagePaths.length,\n                  builder: (context, index) {\n                    return PhotoViewGalleryPageOptions(\n                      imageProvider: FileImage(File(widget.imagePaths[index])),\n                    );\n                  },\n                  onPageChanged: (index) {\n                    setState(() {\n                      currentPage = index;\n                    });\n                  },\n                )),\n                Positioned(\n                  top: 0,\n                  left: 0,\n                  right: 0,\n                  child: SizedBox(\n                    height: 36,\n                    child: Row(\n                      children: [\n                        const SizedBox(\n                          width: 6,\n                        ),\n                        IconButton(\n                            icon: const Icon(FluentIcons.back).paddingAll(2),\n                            onPressed: () => context.pop()),\n                        const Expanded(\n                          child: DragToMoveArea(\n                            child: SizedBox.expand(),\n                          ),\n                        ),\n                        buildActions(),\n                        if (App.isDesktop)\n                          WindowButtons(\n                            key: ValueKey(windowButtonKey),\n                          ),\n                      ],\n                    ),\n                  ),\n                ),\n                Positioned(\n                  left: 0,\n                  top: height / 2 - 9,\n                  child: IconButton(\n                    icon: const Icon(\n                      FluentIcons.chevron_left,\n                      size: 18,\n                    ),\n                    onPressed: () {\n                      controller.previousPage(\n                        duration: const Duration(milliseconds: 300),\n                        curve: Curves.easeInOut,\n                      );\n                    },\n                  ).paddingAll(8),\n                ),\n                Positioned(\n                  right: 0,\n                  top: height / 2 - 9,\n                  child: IconButton(\n                    icon: const Icon(FluentIcons.chevron_right, size: 18),\n                    onPressed: () {\n                      controller.nextPage(\n                        duration: const Duration(milliseconds: 300),\n                        curve: Curves.easeInOut,\n                      );\n                    },\n                  ).paddingAll(8),\n                ),\n                Positioned(\n                  left: 12,\n                  bottom: 8,\n                  child: Text(\n                    \"${currentPage + 1}/${widget.imagePaths.length}\",\n                  ),\n                )\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget buildActions() {\n    var width = MediaQuery.of(context).size.width;\n    return FlyoutTarget(\n      controller: menuController,\n      child: width > 600\n          ? Button(\n              onPressed: showMenu,\n              child: const Row(\n                children: [\n                  Icon(\n                    MdIcons.menu,\n                    size: 18,\n                  ),\n                  SizedBox(\n                    width: 8,\n                  ),\n                  Text('Actions'),\n                ],\n              ))\n          : IconButton(\n              icon: const Icon(\n                MdIcons.more_horiz,\n                size: 20,\n              ),\n              onPressed: showMenu),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/downloading_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/download.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../utils/io.dart';\n\nclass DownloadingPage extends StatefulWidget {\n  const DownloadingPage({super.key});\n\n  @override\n  State<DownloadingPage> createState() => _DownloadingPageState();\n}\n\nclass _DownloadingPageState extends State<DownloadingPage> {\n  @override\n  void initState() {\n    DownloadManager().registerUiUpdater(() => setState((){}));\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    DownloadManager().removeUiUpdater();\n    super.dispose();\n  }\n\n  Map<String, FlyoutController> controller = {};\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      padding: EdgeInsets.zero,\n      content: CustomScrollView(\n        slivers: [\n          buildTop(),\n          const SliverPadding(padding: EdgeInsets.only(top: 16)),\n          buildContent()\n        ],\n      ),\n    );\n  }\n\n  Widget buildTop() {\n    int bytesPerSecond = DownloadManager().bytesPerSecond;\n\n    bool paused = DownloadManager().paused;\n\n    return SliverTitleBar(\n      title: paused\n        ? \"Paused\".tl\n        :\"${\"Speed\".tl}: ${bytesToText(bytesPerSecond)}/s\",\n      action: SplitButton(\n        onInvoked: (){\n          if(!paused) {\n            DownloadManager().pause();\n            setState(() {});\n          } else {\n            DownloadManager().resume();\n            setState(() {});\n          }\n        },\n        flyout: MenuFlyout(\n          items: [\n            MenuFlyoutItem(text: Text(\"Cancel All\".tl), onPressed: (){\n              var tasks = List.from(DownloadManager().tasks);\n              DownloadManager().tasks.clear();\n              for(var task in tasks) {\n                task.cancel();\n              }\n              setState(() {});\n            })\n          ],\n        ),\n        child: Text(paused ? \"Resume\".tl : \"Pause\".tl)\n            .toCenter().fixWidth(56).fixHeight(32),\n      ),\n    );\n  }\n\n  Widget buildContent() {\n    return SliverList(\n      delegate: SliverChildBuilderDelegate(\n          (context, index) {\n            var task = DownloadManager().tasks[index];\n            return buildItem(task);\n          },\n          childCount: DownloadManager().tasks.length\n      ),\n    ).sliverPaddingHorizontal(12);\n  }\n\n  Widget buildItem(DownloadingTask task) {\n    controller[task.illust.id.toString()] ??= FlyoutController();\n\n    return Card(\n      margin: const EdgeInsets.only(bottom: 8),\n      padding: const EdgeInsets.symmetric(vertical: 8),\n      child: SizedBox(\n        height: 96,\n        child: Row(\n          children: [\n            const SizedBox(width: 12),\n            Container(\n              height: double.infinity,\n              width: 72,\n              decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(4),\n                border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6),\n              ),\n              child: Image(\n                image: CachedImageProvider(task.illust.images.first.medium),\n                fit: BoxFit.cover,\n                filterQuality: FilterQuality.medium,\n              ),\n            ),\n            const SizedBox(width: 12),\n            Expanded(\n              child: Column(\n                crossAxisAlignment: CrossAxisAlignment.start,\n                mainAxisAlignment: MainAxisAlignment.start,\n                children: [\n                  Text(task.illust.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),\n                  const SizedBox(height: 4),\n                  Text(task.illust.author.name, style: const TextStyle(fontSize: 12, color: Colors.grey)),\n                  const Spacer(),\n                  if(task.error == null)\n                    Text(\"${task.downloadedImages}/${task.totalImages} ${\"Downloaded\".tl}\", style: const TextStyle(fontSize: 12, color: Colors.grey))\n                  else\n                    Text(\"Error: ${task.error!.replaceAll(\"\\n\", \" \")}\", style: TextStyle(fontSize: 12, color: ColorScheme.of(context).error), maxLines: 2,),\n                ],\n              ),\n            ),\n            const SizedBox(width: 4),\n            Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                if(task.error != null)\n                  Button(\n                    child: Text(\"Retry\".tl).fixWidth(46),\n                    onPressed: () {\n                      task.retry();\n                      setState(() {});\n                    },\n                  ),\n                const SizedBox(height: 4),\n                FlyoutTarget(\n                  controller: controller[task.illust.id.toString()]!,\n                  child: Button(\n                      child: Text(\"Cancel\".tl, style: TextStyle(color: ColorScheme.of(context).error),).fixWidth(46),\n                      onPressed: (){\n                        controller[task.illust.id.toString()]!.showFlyout(\n                          navigatorKey: App.rootNavigatorKey.currentState,\n                          builder: (context) {\n                            return FlyoutContent(\n                              child: Column(\n                                mainAxisSize: MainAxisSize.min,\n                                crossAxisAlignment: CrossAxisAlignment.start,\n                                children: [\n                                  Text(\n                                    'Are you sure you want to cancel this download?'.tl,\n                                    style: const TextStyle(fontWeight: FontWeight.bold),\n                                  ),\n                                  const SizedBox(height: 12.0),\n                                  Button(\n                                    onPressed: () {\n                                      Flyout.of(context).close();\n                                      task.cancel();\n                                      setState(() {});\n                                    },\n                                    child: Text('Yes'.tl),\n                                  ),\n                                ],\n                              ),\n                            );\n                          },\n                        );\n                      }\n                  ),\n                )\n              ],\n            ),\n            const SizedBox(width: 12),\n          ],\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/following_artworks.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/batch_download.dart';\nimport '../components/illust_widget.dart';\nimport '../components/loading.dart';\nimport '../components/segmented_button.dart';\nimport '../network/network.dart';\nimport 'illust_page.dart';\n\nclass FollowingArtworksPage extends StatefulWidget {\n  const FollowingArtworksPage({super.key});\n\n  @override\n  State<FollowingArtworksPage> createState() => _FollowingArtworksPageState();\n}\n\nclass _FollowingArtworksPageState extends State<FollowingArtworksPage> {\n  String restrict = \"all\";\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        buildTab(),\n        Expanded(\n          child: _OneFollowingPage(\n            restrict,\n            key: Key(restrict),\n          ),\n        )\n      ],\n    );\n  }\n\n  Widget buildTab() {\n    return TitleBar(\n      title: \"Following\".tl,\n      action: Row(\n        children: [\n          BatchDownloadButton(\n              request: () => Network().getFollowingArtworks(restrict)),\n          const SizedBox(\n            width: 8,\n          ),\n          SegmentedButton(\n            options: [\n              SegmentedButtonOption(\"all\", \"All\".tl),\n              SegmentedButtonOption(\"public\", \"Public\".tl),\n              SegmentedButtonOption(\"private\", \"Private\".tl),\n            ],\n            onPressed: (key) {\n              if (key != restrict) {\n                setState(() {\n                  restrict = key;\n                });\n              }\n            },\n            value: restrict,\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass _OneFollowingPage extends StatefulWidget {\n  const _OneFollowingPage(this.restrict, {super.key});\n\n  final String restrict;\n\n  @override\n  State<_OneFollowingPage> createState() => _OneFollowingPageState();\n}\n\nclass _OneFollowingPageState\n    extends MultiPageLoadingState<_OneFollowingPage, Illust> {\n  @override\n  Widget buildContent(BuildContext context, List<Illust> data) {\n    checkIllusts(data);\n    return LayoutBuilder(builder: (context, constrains) {\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8) +\n            EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return IllustWidget(data[index], onTap: () {\n            context.to(() => IllustGalleryPage(\n                illusts: data, initialPage: index, nextUrl: nextUrl));\n          });\n        },\n      );\n    });\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().getFollowingArtworks(widget.restrict, nextUrl);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/following_novels_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/widget_utils.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nclass FollowingNovelsPage extends StatefulWidget {\n  const FollowingNovelsPage({super.key});\n\n  @override\n  State<FollowingNovelsPage> createState() => _FollowingNovelsPageState();\n}\n\nclass _FollowingNovelsPageState\n    extends MultiPageLoadingState<FollowingNovelsPage, Novel> {\n  bool public = true;\n\n  @override\n  Widget? buildFrame(BuildContext context, Widget child) {\n    return Column(\n      children: [\n        TitleBar(\n          title: \"Following\".tl,\n          action: SegmentedButton(\n            options: [\n              SegmentedButtonOption(\"public\", \"Public\".tl),\n              SegmentedButtonOption(\"private\", \"Private\".tl),\n            ],\n            onPressed: (key) {\n              var newPublic = key == \"public\";\n              if (newPublic != public) {\n                public = newPublic;\n                nextUrl = null;\n                reset();\n              }\n            },\n            value: public ? \"public\" : \"private\",\n          ),\n        ),\n        Expanded(\n          child: child,\n        )\n      ],\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, List<Novel> data) {\n    return Column(\n      children: [\n        Expanded(\n          child: GridViewWithFixedItemHeight(\n            itemCount: data.length,\n            itemHeight: 164,\n            minCrossAxisExtent: 400,\n            builder: (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return NovelWidget(data[index]);\n            },\n          ).paddingHorizontal(8),\n        )\n      ],\n    );\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(int page) async {\n    if (nextUrl == \"end\") return Res.error(\"No more data\");\n    var res = nextUrl == null\n        ? await Network().getFollowingNovels(public ? \"public\" : \"private\")\n        : await Network().getNovelsWithNextUrl(nextUrl!);\n    nextUrl = res.subData ?? \"end\";\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/following_users_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/grid.dart';\nimport '../components/user_preview.dart';\n\nclass FollowingUsersPage extends StatefulWidget {\n  const FollowingUsersPage(this.uid, {super.key});\n\n  final String uid;\n\n  @override\n  State<FollowingUsersPage> createState() => _FollowingUsersPageState();\n}\n\nclass _FollowingUsersPageState\n    extends MultiPageLoadingState<FollowingUsersPage, UserPreview> {\n  String type = \"public\";\n\n  @override\n  Widget buildContent(BuildContext context, final List<UserPreview> data) {\n    return CustomScrollView(\n      slivers: [\n        SliverToBoxAdapter(\n          child: Row(\n            children: [\n              Text(\n                \"Following\".tl,\n                style:\n                    const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),\n              ).paddingVertical(12).paddingLeft(16),\n              const Spacer(),\n              if (widget.uid == appdata.account?.user.id)\n                SegmentedButton(\n                  value: type,\n                  options: [\n                    SegmentedButtonOption(\"public\", \"Public\".tl),\n                    SegmentedButtonOption(\"private\", \"Private\".tl),\n                  ],\n                  onPressed: (s) {\n                    type = s;\n                    reset();\n                  },\n                ),\n              const SizedBox(\n                width: 16,\n              )\n            ],\n          ),\n        ),\n        SliverGridViewWithFixedItemHeight(\n          delegate: SliverChildBuilderDelegate((context, index) {\n            if (index == data.length - 1) {\n              nextPage();\n            }\n            return UserPreviewWidget(data[index]);\n          }, childCount: data.length),\n          minCrossAxisExtent: 440,\n          itemHeight: 136,\n        ).sliverPaddingHorizontal(8)\n      ],\n    );\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<UserPreview>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().getFollowing(widget.uid, type, nextUrl);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/history.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/history.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/illust_widget.dart';\nimport 'illust_page.dart';\n\nclass HistoryPage extends StatefulWidget {\n  const HistoryPage({super.key});\n\n  @override\n  State<HistoryPage> createState() => _HistoryPageState();\n}\n\nclass _HistoryPageState extends State<HistoryPage> {\n  int page = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        TitleBar(\n          title: \"History\".tl,\n          action: SegmentedButton<int>(\n            options: [\n              SegmentedButtonOption(0, \"Local\".tl,),\n              SegmentedButtonOption(1, \"Network\".tl,),\n            ],\n            value: page,\n            onPressed: (key) {\n              setState(() {\n                page = key;\n              });\n            },\n          ),\n          ),\n        Expanded(\n          child: page == 0\n              ? const LocalHistoryPage()\n              : const NetworkHistoryPage(),\n        ),\n      ],\n    );\n  }\n}\n\nclass LocalHistoryPage extends StatefulWidget {\n  const LocalHistoryPage({super.key});\n\n  @override\n  State<LocalHistoryPage> createState() => _LocalHistoryPageState();\n}\n\nclass _LocalHistoryPageState extends State<LocalHistoryPage> {\n  int page = 1;\n\n  var data = <IllustHistory>[];\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(builder: (context, constrains) {\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8) +\n            EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: HistoryManager().length,\n        itemBuilder: (context, index) {\n          if (index == data.length) {\n            data.addAll(HistoryManager().getHistories(page));\n            page++;\n          }\n          return IllustHistoryWidget(data[index]);\n        },\n      );\n    });\n  }\n}\n\nclass NetworkHistoryPage extends StatefulWidget {\n  const NetworkHistoryPage({super.key});\n\n  @override\n  State<NetworkHistoryPage> createState() => _NetworkHistoryPageState();\n}\n\nclass _NetworkHistoryPageState\n    extends MultiPageLoadingState<NetworkHistoryPage, Illust> {\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    return LayoutBuilder(builder: (context, constrains) {\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8) +\n            EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return IllustWidget(data[index], onTap: () {\n            context.to(() => IllustGalleryPage(\n                  illusts: data,\n                  initialPage: index,\n                ));\n          });\n        },\n      );\n    });\n  }\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) {\n    if (appdata.account?.user.isPremium != true) {\n      return Future.value(Res.error(\"Premium Required\".tl));\n    }\n    return Network().getHistory(page);\n  }\n}\n"
  },
  {
    "path": "lib/pages/illust_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/material.dart' show Icons;\nimport 'package:flutter/services.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/keyboard.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/message.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/components/user_preview.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/history.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/download.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/comments_page.dart';\nimport 'package:pixes/pages/image_page.dart';\nimport 'package:pixes/pages/related_page.dart';\nimport 'package:pixes/pages/search_page.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:share_plus/share_plus.dart';\n\nimport '../components/illust_widget.dart';\nimport '../components/md.dart';\nimport '../components/ugoira.dart';\n\nconst _kBottomBarHeight = 64.0;\n\nclass IllustGalleryPage extends StatefulWidget {\n  const IllustGalleryPage(\n      {required this.illusts,\n      required this.initialPage,\n      this.nextUrl,\n      super.key});\n\n  final List<Illust> illusts;\n\n  final int initialPage;\n\n  final String? nextUrl;\n\n  static var cachedHistoryIds = <int>{};\n\n  @override\n  State<IllustGalleryPage> createState() => _IllustGalleryPageState();\n}\n\nclass _IllustGalleryPageState extends State<IllustGalleryPage> {\n  late List<Illust> illusts;\n\n  late final PageController controller;\n\n  String? nextUrl;\n\n  bool loading = false;\n\n  late int page = widget.initialPage;\n\n  @override\n  void initState() {\n    illusts = List.from(widget.illusts);\n    controller = PageController(initialPage: widget.initialPage);\n    nextUrl = widget.nextUrl;\n    if (nextUrl == \"end\") {\n      nextUrl = null;\n    }\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    if (IllustGalleryPage.cachedHistoryIds.length > 5) {\n      Network().sendHistory(\n          IllustGalleryPage.cachedHistoryIds.toList().reversed.toList());\n      IllustGalleryPage.cachedHistoryIds.clear();\n    }\n    super.dispose();\n  }\n\n  void nextPage() {\n    var length = illusts.length;\n    if (controller.page == length - 1) return;\n    controller.nextPage(\n        duration: const Duration(milliseconds: 200), curve: Curves.ease);\n  }\n\n  void previousPage() {\n    if (controller.page == 0) return;\n    controller.previousPage(\n        duration: const Duration(milliseconds: 200), curve: Curves.ease);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var length = illusts.length;\n    if (nextUrl != null) {\n      length++;\n    }\n\n    return Stack(\n      children: [\n        Positioned.fill(\n          child: PageView.builder(\n            controller: controller,\n            itemCount: length,\n            itemBuilder: (context, index) {\n              if (index == illusts.length) {\n                return buildLast();\n              }\n              return IllustPage(illusts[index],\n                  nextPage: nextPage, previousPage: previousPage);\n            },\n            onPageChanged: (value) => setState(() {\n              page = value;\n            }),\n          ),\n        ),\n        if (page < length - 1 && length > 1 && App.isDesktop)\n          Positioned(\n            right: 0,\n            top: 0,\n            bottom: 32,\n            child: Center(\n                child: IconButton(\n              icon: const Icon(FluentIcons.chevron_right),\n              onPressed: () {\n                nextPage();\n              },\n            )),\n          ),\n        if (page != 0 && length > 1 && App.isDesktop)\n          Positioned(\n            left: 0,\n            top: 0,\n            bottom: 32,\n            child: Center(\n                child: IconButton(\n              icon: const Icon(FluentIcons.chevron_left),\n              onPressed: () {\n                previousPage();\n              },\n            )),\n          ),\n      ],\n    );\n  }\n\n  Widget buildLast() {\n    if (nextUrl == null) {\n      return const SizedBox();\n    }\n    load();\n    return const Center(\n      child: ProgressRing(),\n    );\n  }\n\n  void load() async {\n    if (loading) return;\n    loading = true;\n\n    var res = await Network().getIllustsWithNextUrl(nextUrl!);\n    loading = false;\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      nextUrl = res.subData;\n      illusts.addAll(res.data);\n      setState(() {});\n    }\n  }\n}\n\nclass IllustPage extends StatefulWidget {\n  const IllustPage(this.illust, {this.nextPage, this.previousPage, super.key});\n\n  final Illust illust;\n\n  final void Function()? nextPage;\n\n  final void Function()? previousPage;\n\n  static Map<String, UpdateFollowCallback> followCallbacks = {};\n\n  static void updateFollow(String uid, bool isFollowed) {\n    followCallbacks.forEach((key, value) {\n      if (key.startsWith(\"$uid#\")) {\n        value(isFollowed);\n      }\n    });\n  }\n\n  @override\n  State<IllustPage> createState() => _IllustPageState();\n}\n\nclass _IllustPageState extends State<IllustPage> {\n  String get id => \"${widget.illust.author.id}#${widget.illust.id}\";\n\n  final _bottomBarController = _BottomBarController();\n\n  KeyEventListenerState? keyboardListener;\n\n  @override\n  void initState() {\n    keyboardListener = KeyEventListener.of(context);\n    keyboardListener?.removeAll();\n    keyboardListener?.addHandler(handleKey);\n    IllustPage.followCallbacks[id] = (v) {\n      setState(() {\n        widget.illust.author.isFollowed = v;\n      });\n    };\n    HistoryManager().addHistory(widget.illust);\n    if (appdata.account!.user.isPremium) {\n      IllustGalleryPage.cachedHistoryIds.add(widget.illust.id);\n    }\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    keyboardListener?.removeHandler(handleKey);\n    IllustPage.followCallbacks.remove(id);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    var isBlocked = checkIllusts([widget.illust]).isEmpty;\n    return ColoredBox(\n      color: FluentTheme.of(context).micaBackgroundColor,\n      child: SizedBox.expand(\n        child: ColoredBox(\n          color: FluentTheme.of(context).scaffoldBackgroundColor,\n          child: LayoutBuilder(builder: (context, constrains) {\n            return Stack(\n              children: [\n                if (!isBlocked)\n                  Positioned(\n                    bottom: 0,\n                    left: 0,\n                    right: 0,\n                    top: 0,\n                    child: buildBody(constrains.maxWidth, constrains.maxHeight),\n                  ),\n                if (!isBlocked)\n                  _BottomBar(\n                    widget.illust,\n                    constrains.maxHeight,\n                    constrains.maxWidth,\n                    updateCallback: () => setState(() {}),\n                    controller: _bottomBarController,\n                  ),\n                if (isBlocked)\n                  const Positioned.fill(\n                      child: Center(\n                    child: Center(\n                        child: Text(\n                      \"This artwork is blocked\",\n                    )),\n                  ))\n              ],\n            );\n          }),\n        ),\n      ),\n    );\n  }\n\n  final scrollController = ScrollController();\n\n  void handleKey(LogicalKeyboardKey key) {\n    const kShortcutScrollOffset = 200;\n\n    var shortcuts = appdata.settings[\"shortcuts\"] as List;\n\n    switch (shortcuts.indexOf(key.keyId)) {\n      case 0:\n        if (scrollController.position.pixels >=\n            scrollController.position.maxScrollExtent) {\n          _bottomBarController.openOrClose();\n        } else {\n          scrollController.animateTo(\n            scrollController.offset + kShortcutScrollOffset,\n            duration: const Duration(milliseconds: 200),\n            curve: Curves.easeOut,\n          );\n        }\n        break;\n      case 1:\n        if (_bottomBarController.isOpen()) {\n          _bottomBarController.openOrClose();\n          break;\n        }\n        scrollController.animateTo(\n          scrollController.offset - kShortcutScrollOffset,\n          duration: const Duration(milliseconds: 200),\n          curve: Curves.easeOut,\n        );\n        break;\n      case 2:\n        widget.nextPage?.call();\n        break;\n      case 3:\n        widget.previousPage?.call();\n        break;\n      case 4:\n        _bottomBarController.favorite();\n      case 5:\n        _bottomBarController.download();\n      case 6:\n        _bottomBarController.follow();\n      case 7:\n        if (ModalRoute.of(context)?.isCurrent ?? true) {\n          CommentsPage.show(context, widget.illust.id.toString());\n        } else {\n          context.pop();\n        }\n      case 8:\n        openImage(0);\n    }\n  }\n\n  Widget buildBody(double width, double height) {\n    return ListView.builder(\n        controller: scrollController,\n        itemCount: widget.illust.images.length + 2,\n        padding: EdgeInsets.zero,\n        itemBuilder: (context, index) {\n          return buildImage(width, height, index);\n        });\n  }\n\n  void openImage(int index) {\n    var images = <String>[];\n    for (var i = 0; i < widget.illust.images.length; i++) {\n      var downloadFile = DownloadManager().getImage(widget.illust.id, i);\n      if (downloadFile != null) {\n        images.add(\"file://${downloadFile.path}\");\n      } else {\n        images.add(widget.illust.images[i].original);\n      }\n    }\n    ImagePage.show(images, initialPage: index);\n  }\n\n  Widget buildHeader() {\n    return Column(\n      children: [\n        Text(\n          widget.illust.title,\n          style: const TextStyle(fontSize: 24),\n        ).paddingVertical(8).paddingHorizontal(12).toAlign(Alignment.centerLeft),\n        Row(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisAlignment: MainAxisAlignment.start,\n          children: [\n            const Icon(FluentIcons.view, size: 14),\n            const SizedBox(width: 4),\n            Text(\n              widget.illust.totalView.toString(),\n              style: const TextStyle(fontSize: 12),\n            ),\n            const SizedBox(width: 8),\n            const Icon(FluentIcons.six_point_star, size: 14),\n            const SizedBox(width: 4),\n            Text(\n              widget.illust.totalBookmarks.toString(),\n              style: const TextStyle(fontSize: 12),\n            ),\n          ],\n        ).paddingHorizontal(12),\n        const SizedBox(height: 8),\n        SizedBox(\n          width: double.infinity,\n          child: Wrap(\n            spacing: 4,\n            runSpacing: 4,\n            children: widget.illust.tags.map((e) {\n              var text = e.name;\n              if (e.translatedName != null && e.name != e.translatedName) {\n                text += \"/${e.translatedName}\";\n              }\n              return MouseRegion(\n                cursor: SystemMouseCursors.click,\n                child: GestureDetector(\n                  onTap: () {\n                    context.to(() => SearchResultPage(e.name));\n                  },\n                  child: Card(\n                    padding:\n                        const EdgeInsets.symmetric(vertical: 4, horizontal: 6),\n                    child: Text(\n                      text,\n                      style: const TextStyle(fontSize: 13),\n                    ),\n                  ),\n                ),\n              );\n            }).toList(),\n          ),\n        ).paddingHorizontal(10).paddingBottom(8),\n      ],\n    ).paddingAll(4);\n  }\n\n  Widget buildImage(double width, double height, int index) {\n    if (index == 0) {\n      return buildHeader();\n    }\n    index--;\n    File? downloadFile;\n    if (widget.illust.downloaded) {\n      downloadFile = DownloadManager().getImage(widget.illust.id, index);\n    }\n    if (index == widget.illust.images.length) {\n      return SizedBox(\n        height: _kBottomBarHeight + context.padding.bottom,\n      );\n    }\n    height -= _kBottomBarHeight + context.padding.bottom;\n    var imageWidth = width;\n    var imageHeight = widget.illust.height * width / widget.illust.width;\n    if (imageHeight > height) {\n      // 确保图片能够完整显示在屏幕上\n      var scale = imageHeight / height;\n      imageWidth = imageWidth / scale;\n      imageHeight = height;\n    }\n    Widget image;\n\n    var imageUrl = appdata.settings[\"showOriginalImage\"]\n        ? widget.illust.images[index].original\n        : widget.illust.images[index].large;\n\n    if (!widget.illust.isUgoira) {\n      image = SizedBox(\n        width: imageWidth,\n        height: imageHeight,\n        child: GestureDetector(\n          onTap: () => openImage(index),\n          child: Image(\n              key: ValueKey(index),\n              image: downloadFile == null\n                  ? CachedImageProvider(imageUrl) as ImageProvider\n                  : FileImage(downloadFile) as ImageProvider,\n              width: imageWidth,\n              fit: BoxFit.cover,\n              height: imageHeight,\n              loadingBuilder: (context, child, loadingProgress) {\n                if (loadingProgress == null) return child;\n                double? value;\n                if (loadingProgress.expectedTotalBytes != null) {\n                  value = (loadingProgress.cumulativeBytesLoaded /\n                          loadingProgress.expectedTotalBytes!) *\n                      100;\n                }\n                if (value != null && (value > 100 || value < 0)) {\n                  value = null;\n                }\n                return Center(\n                  child: SizedBox(\n                    width: 24,\n                    height: 24,\n                    child: ProgressRing(\n                      value: value,\n                    ),\n                  ),\n                );\n              }),\n        ),\n      );\n    } else {\n      image = UgoiraWidget(\n        id: widget.illust.id.toString(),\n        previewImage: CachedImageProvider(widget.illust.images[index].large),\n        width: imageWidth,\n        height: imageHeight,\n      );\n    }\n\n    return Center(\n      child: image,\n    );\n  }\n}\n\nclass _BottomBarController {\n  VoidCallback? _openOrClose;\n\n  VoidCallback get openOrClose => _openOrClose!;\n\n  bool Function()? _isOpen;\n\n  bool isOpen() => _isOpen!();\n\n  VoidCallback? _favorite;\n\n  VoidCallback get favorite => _favorite!;\n\n  VoidCallback? _download;\n\n  VoidCallback get download => _download!;\n\n  VoidCallback? _follow;\n\n  VoidCallback get follow => _follow!;\n}\n\nclass _BottomBar extends StatefulWidget {\n  const _BottomBar(this.illust, this.height, this.width,\n      {this.updateCallback, this.controller});\n\n  final void Function()? updateCallback;\n\n  final Illust illust;\n\n  final double height;\n\n  final double width;\n\n  final _BottomBarController? controller;\n\n  @override\n  State<_BottomBar> createState() => _BottomBarState();\n}\n\nclass _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin {\n  double pageHeight = 0;\n\n  double widgetHeight = 48;\n\n  final key = GlobalKey();\n\n  double _width = 0;\n\n  late VerticalDragGestureRecognizer _recognizer;\n\n  late final AnimationController animationController;\n\n  double get minValue => pageHeight - widgetHeight;\n  double get maxValue =>\n      pageHeight - _kBottomBarHeight - context.padding.bottom;\n\n  @override\n  void initState() {\n    _width = widget.width;\n    pageHeight = widget.height;\n    Future.delayed(const Duration(milliseconds: 200), () {\n      final box = key.currentContext?.findRenderObject() as RenderBox?;\n      widgetHeight = (box?.size.height) ?? 0;\n    });\n    _recognizer = VerticalDragGestureRecognizer()\n      ..onStart = _handlePointerDown\n      ..onUpdate = _handlePointerMove\n      ..onEnd = _handlePointerUp\n      ..onCancel = _handlePointerCancel;\n    animationController = AnimationController(\n        vsync: this, duration: const Duration(milliseconds: 180), value: 1);\n    if (widget.controller != null) {\n      widget.controller!._openOrClose = () {\n        if (animationController.value == 0) {\n          animationController.animateTo(1);\n        } else if (animationController.value == 1) {\n          animationController.animateTo(0);\n        }\n      };\n      widget.controller!._isOpen = () => animationController.value == 0;\n      widget.controller!._favorite = favorite;\n      widget.controller!._download = () {\n        DownloadManager().addDownloadingTask(widget.illust);\n        setState(() {});\n      };\n      widget.controller!._follow = follow;\n    }\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    animationController.dispose();\n    _recognizer.dispose();\n    super.dispose();\n  }\n\n  void _handlePointerDown(DragStartDetails details) {}\n  void _handlePointerMove(DragUpdateDetails details) {\n    var offset = details.primaryDelta ?? 0;\n    final minValue = pageHeight - widgetHeight;\n    final maxValue = pageHeight - _kBottomBarHeight - context.padding.bottom;\n    var top = animationController.value * (maxValue - minValue) + minValue;\n    top = (top + offset).clamp(minValue, maxValue);\n    animationController.value = (top - minValue) / (maxValue - minValue);\n  }\n\n  void _handlePointerUp(DragEndDetails details) {\n    var speed = details.primaryVelocity ?? 0;\n    const minShouldTransitionSpeed = 1000;\n    if (speed > minShouldTransitionSpeed) {\n      animationController.forward();\n    } else if (speed < 0 - minShouldTransitionSpeed) {\n      animationController.reverse();\n    } else {\n      _handlePointerCancel();\n    }\n  }\n\n  void _handlePointerCancel() {\n    if (animationController.value == 1 || animationController.value == 0) {\n      return;\n    }\n    if (animationController.value >= 0.5) {\n      animationController.forward();\n    } else {\n      animationController.reverse();\n    }\n  }\n\n  @override\n  void didUpdateWidget(covariant _BottomBar oldWidget) {\n    if (widget.height != pageHeight) {\n      setState(() {\n        pageHeight = widget.height;\n      });\n    }\n    _recognizer.dispose();\n    if (_width != widget.width) {\n      _width = widget.width;\n      Future.microtask(() {\n        final box = key.currentContext?.findRenderObject() as RenderBox?;\n        var oldHeight = widgetHeight;\n        widgetHeight = (box?.size.height) ?? 0;\n        if (oldHeight != widgetHeight) {\n          setState(() {});\n        }\n      });\n    }\n    super.didUpdateWidget(oldWidget);\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return AnimatedBuilder(\n      animation: CurvedAnimation(\n        parent: animationController,\n        curve: Curves.ease,\n        reverseCurve: Curves.ease,\n      ),\n      builder: (context, child) {\n        return Positioned(\n          top: minValue + (maxValue - minValue) * animationController.value,\n          left: 0,\n          right: 0,\n          child: Listener(\n            onPointerDown: (event) {\n              _recognizer.addPointer(event);\n            },\n            onPointerSignal: (event) {\n              if (event is PointerScrollEvent) {\n                var offset = (event).scrollDelta.dy;\n                if (offset < 0) {\n                  animationController.reverse();\n                } else {\n                  animationController.forward();\n                }\n              }\n            },\n            child: Card(\n              borderRadius:\n                  const BorderRadius.vertical(top: Radius.circular(8)),\n              backgroundColor:\n                  FluentTheme.of(context).micaBackgroundColor.toOpacity(0.96),\n              padding: const EdgeInsets.symmetric(horizontal: 8),\n              child: SizedBox(\n                width: double.infinity,\n                key: key,\n                child: Column(\n                  crossAxisAlignment: CrossAxisAlignment.start,\n                  children: [\n                    buildTop(),\n                    buildMoreActions(),\n                    SelectableText(\n                      \"${\"Artwork ID\".tl}: ${widget.illust.id}\\n\"\n                      \"${\"Artist ID\".tl}: ${widget.illust.author.id}\\n\"\n                      \"${widget.illust.createDate.toString().split('.').first}\",\n                      style: TextStyle(color: ColorScheme.of(context).outline),\n                    ).paddingLeft(4),\n                    SizedBox(\n                      height: 8 + context.padding.bottom,\n                    )\n                  ],\n                ),\n              ),\n            ),\n          ),\n        );\n      },\n    );\n  }\n\n  Widget buildTop() {\n    return SizedBox(\n      height: _kBottomBarHeight,\n      width: double.infinity,\n      child: LayoutBuilder(builder: (context, constrains) {\n        return Row(\n          children: [\n            buildAuthor(),\n            ...buildActions(constrains.maxWidth),\n            const Spacer(),\n            if (animationController.value == 1)\n              IconButton(\n                  icon: const Icon(FluentIcons.up),\n                  onPressed: () {\n                    animationController.reverse();\n                  })\n            else\n              IconButton(\n                  icon: const Icon(FluentIcons.down),\n                  onPressed: () {\n                    animationController.forward();\n                  })\n          ],\n        );\n      }),\n    );\n  }\n\n  bool isFollowing = false;\n\n  void follow() async {\n    if (isFollowing) return;\n    setState(() {\n      isFollowing = true;\n    });\n    var method = widget.illust.author.isFollowed ? \"delete\" : \"add\";\n    var res =\n        await Network().follow(widget.illust.author.id.toString(), method);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      widget.illust.author.isFollowed = !widget.illust.author.isFollowed;\n    }\n    setState(() {\n      isFollowing = false;\n    });\n    UserInfoPage.followCallbacks[widget.illust.author.id.toString()]\n        ?.call(widget.illust.author.isFollowed);\n    UserPreviewWidget.followCallbacks[widget.illust.author.id.toString()]\n        ?.call(widget.illust.author.isFollowed);\n  }\n\n  Widget buildAuthor() {\n    final bool showUserName = MediaQuery.of(context).size.width > 640;\n\n    return Card(\n      margin: const EdgeInsets.symmetric(vertical: 8),\n      padding: const EdgeInsets.symmetric(horizontal: 8),\n      borderRadius: BorderRadius.circular(8),\n      child: SizedBox(\n        height: double.infinity,\n        width: showUserName ? 246 : 136,\n        child: Row(\n          children: [\n            SizedBox(\n              height: 40,\n              width: 40,\n              child: ClipRRect(\n                borderRadius: BorderRadius.circular(40),\n                child: ColoredBox(\n                  color: ColorScheme.of(context).secondaryContainer,\n                  child: GestureDetector(\n                    onTap: () => context.to(() => UserInfoPage(\n                          widget.illust.author.id.toString(),\n                        )),\n                    child: AnimatedImage(\n                      image: CachedImageProvider(widget.illust.author.avatar),\n                      width: 40,\n                      height: 40,\n                      fit: BoxFit.cover,\n                      filterQuality: FilterQuality.medium,\n                    ),\n                  ),\n                ),\n              ),\n            ),\n            const SizedBox(\n              width: 8,\n            ),\n            if (showUserName)\n              Expanded(\n                child: Text(\n                  widget.illust.author.name,\n                  maxLines: 2,\n                ),\n              ),\n            if (isFollowing)\n              Button(\n                  onPressed: follow,\n                  child: const SizedBox(\n                    width: 42,\n                    height: 24,\n                    child: Center(\n                      child: SizedBox.square(\n                        dimension: 18,\n                        child: ProgressRing(\n                          strokeWidth: 2,\n                        ),\n                      ),\n                    ),\n                  ))\n            else if (!widget.illust.author.isFollowed)\n              Button(onPressed: follow, child: Text(\"Follow\".tl).fixWidth(62))\n            else\n              Button(\n                onPressed: follow,\n                child: Text(\n                  \"Unfollow\".tl,\n                  style: TextStyle(color: ColorScheme.of(context).error),\n                ).fixWidth(62),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n\n  bool isBookmarking = false;\n\n  void favorite([String type = \"public\"]) async {\n    if (isBookmarking) return;\n    setState(() {\n      isBookmarking = true;\n    });\n    var method = widget.illust.isBookmarked ? \"delete\" : \"add\";\n    var res =\n        await Network().addBookmark(widget.illust.id.toString(), method, type);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      widget.illust.isBookmarked = !widget.illust.isBookmarked;\n      IllustWidget.favoriteCallbacks[widget.illust.id.toString()]\n          ?.call(widget.illust.isBookmarked);\n    }\n    setState(() {\n      isBookmarking = false;\n    });\n  }\n\n  Iterable<Widget> buildActions(double width) sync* {\n    yield const SizedBox(\n      width: 8,\n    );\n\n    void download() {\n      DownloadManager().addDownloadingTask(widget.illust);\n      setState(() {});\n    }\n\n    bool showText = width > 640;\n\n    yield Button(\n      onPressed: favorite,\n      child: SizedBox(\n        height: 28,\n        child: Row(\n          children: [\n            if (isBookmarking)\n              const SizedBox(\n                width: 18,\n                height: 18,\n                child: ProgressRing(\n                  strokeWidth: 2,\n                ),\n              )\n            else if (widget.illust.isBookmarked)\n              Icon(\n                Icons.favorite,\n                color: ColorScheme.of(context).error,\n                size: 18,\n              )\n            else\n              const Icon(\n                Icons.favorite_border,\n                size: 18,\n              ),\n            if (showText)\n              const SizedBox(\n                width: 8,\n              ),\n            if (showText)\n              if (widget.illust.isBookmarked)\n                Text(\"Cancel\".tl)\n              else\n                Text(\"Favorite\".tl)\n          ],\n        ),\n      ),\n    );\n\n    yield const SizedBox(\n      width: 8,\n    );\n\n    if (!widget.illust.downloaded) {\n      if (widget.illust.downloading) {\n        yield Button(\n          onPressed: () => {},\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                Icon(\n                  FluentIcons.download,\n                  color: ColorScheme.of(context).outline,\n                  size: 18,\n                ),\n                if (showText)\n                  const SizedBox(\n                    width: 8,\n                  ),\n                if (showText)\n                  Text(\n                    \"Downloading\".tl,\n                    style: TextStyle(color: ColorScheme.of(context).outline),\n                  ),\n              ],\n            ),\n          ),\n        );\n      } else {\n        yield Button(\n          onPressed: download,\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                const Icon(\n                  FluentIcons.download,\n                  size: 18,\n                ),\n                if (showText)\n                  const SizedBox(\n                    width: 8,\n                  ),\n                if (showText) Text(\"Download\".tl),\n              ],\n            ),\n          ),\n        );\n      }\n    }\n\n    yield const SizedBox(\n      width: 8,\n    );\n\n    yield Button(\n      onPressed: () => CommentsPage.show(context, widget.illust.id.toString()),\n      child: SizedBox(\n        height: 28,\n        child: Row(\n          children: [\n            const Icon(\n              FluentIcons.comment,\n              size: 18,\n            ),\n            if (showText)\n              const SizedBox(\n                width: 8,\n              ),\n            if (showText) Text(\"Comment\".tl),\n          ],\n        ),\n      ),\n    );\n  }\n\n  Widget buildMoreActions() {\n    return Wrap(\n      runSpacing: 8,\n      spacing: 8,\n      children: [\n        Button(\n          onPressed: () => favorite(\"private\"),\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                if (isBookmarking)\n                  const SizedBox(\n                    width: 18,\n                    height: 18,\n                    child: ProgressRing(\n                      strokeWidth: 2,\n                    ),\n                  )\n                else if (widget.illust.isBookmarked)\n                  Icon(\n                    Icons.favorite,\n                    color: ColorScheme.of(context).error,\n                    size: 18,\n                  )\n                else\n                  const Icon(\n                    Icons.favorite_border,\n                    size: 18,\n                  ),\n                const SizedBox(\n                  width: 8,\n                ),\n                if (widget.illust.isBookmarked)\n                  Text(\"Cancel\".tl)\n                else\n                  Text(\"Private\".tl)\n              ],\n            ),\n          ),\n        ).fixWidth(96),\n        Button(\n          onPressed: () {\n            Share.share(\n                \"${widget.illust.title}\\nhttps://pixiv.net/artworks/${widget.illust.id}\");\n          },\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                const Icon(\n                  Icons.share,\n                  size: 18,\n                ),\n                const SizedBox(\n                  width: 8,\n                ),\n                Text(\"Share\".tl)\n              ],\n            ),\n          ),\n        ).fixWidth(96),\n        Button(\n          onPressed: () {\n            var text = \"https://pixiv.net/artworks/${widget.illust.id}\";\n            Clipboard.setData(ClipboardData(text: text));\n            showToast(context, message: \"Copied\".tl);\n          },\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                const Icon(Icons.copy, size: 18),\n                const SizedBox(\n                  width: 8,\n                ),\n                Text(\"Link\".tl)\n              ],\n            ),\n          ),\n        ).fixWidth(96),\n        Button(\n          onPressed: () {\n            context.to(() => RelatedIllustsPage(widget.illust.id.toString()));\n          },\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                const Icon(Icons.stars, size: 18),\n                const SizedBox(\n                  width: 8,\n                ),\n                Text(\"Related\".tl)\n              ],\n            ),\n          ),\n        ).fixWidth(96),\n        Button(\n          onPressed: () async {\n            await Navigator.of(context)\n                .push(SideBarRoute(_BlockingPage(widget.illust)));\n            if (mounted) {\n              widget.updateCallback?.call();\n            }\n          },\n          child: SizedBox(\n            height: 28,\n            child: Row(\n              children: [\n                const Icon(MdIcons.block, size: 18),\n                const SizedBox(\n                  width: 8,\n                ),\n                Text(\"Block\".tl)\n              ],\n            ),\n          ),\n        ).fixWidth(96),\n      ],\n    ).paddingHorizontal(2).paddingBottom(4);\n  }\n}\n\nclass _BlockingPage extends StatefulWidget {\n  const _BlockingPage(this.illust);\n\n  final Illust illust;\n\n  @override\n  State<_BlockingPage> createState() => __BlockingPageState();\n}\n\nclass __BlockingPageState extends State<_BlockingPage> {\n  List<int> blockedTags = [];\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        TitleBar(title: \"Block\".tl),\n        Expanded(\n          child: ListView.builder(\n            padding: EdgeInsets.only(bottom: context.padding.bottom),\n            itemCount: widget.illust.tags.length + 2,\n            itemBuilder: (context, index) {\n              if (index == widget.illust.tags.length + 1) {\n                return buildSubmit();\n              }\n\n              var text = index == 0\n                  ? widget.illust.author.name\n                  : widget.illust.tags[index - 1].name;\n\n              var subTitle = index == 0\n                  ? \"author\"\n                  : widget.illust.tags[index - 1].translatedName ?? \"\";\n\n              return Card(\n                margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),\n                borderColor: blockedTags.contains(index)\n                    ? ColorScheme.of(context).outlineVariant\n                    : ColorScheme.of(context).outlineVariant.toOpacity(0.2),\n                padding: EdgeInsets.zero,\n                child: ListTile(\n                  title: Text(text),\n                  subtitle: Text(subTitle),\n                  trailing: Button(\n                          onPressed: () {\n                            if (blockedTags.contains(index)) {\n                              blockedTags.remove(index);\n                            } else {\n                              blockedTags.add(index);\n                            }\n                            setState(() {});\n                          },\n                          child: blockedTags.contains(index)\n                              ? Text(\"Cancel\".tl)\n                              : Text(\"Block\".tl))\n                      .fixWidth(72),\n                ),\n              );\n            },\n          ),\n        )\n      ],\n    );\n  }\n\n  var flyout = FlyoutController();\n\n  bool isSubmitting = false;\n\n  Widget buildSubmit() {\n    return FlyoutTarget(\n      controller: flyout,\n      child: FilledButton(\n        onPressed: () async {\n          if (this.blockedTags.isEmpty) {\n            return;\n          }\n          if (isSubmitting) return;\n          var blockedTags = <String>[];\n          var blockedUsers = <String>[];\n          for (var i in this.blockedTags) {\n            if (i == 0) {\n              blockedUsers.add(widget.illust.author.id.toString());\n            } else {\n              blockedTags.add(widget.illust.tags[i - 1].name);\n            }\n          }\n          bool addToAccount = false;\n          bool addToLocal = false;\n          if (appdata.account!.user.isPremium) {\n            await flyout.showFlyout(\n                navigatorKey: App.rootNavigatorKey.currentState,\n                builder: (context) {\n                  return MenuFlyout(\n                    items: [\n                      MenuFlyoutItem(\n                          text: Text(\"Local\".tl),\n                          onPressed: () {\n                            addToLocal = true;\n                          }),\n                      MenuFlyoutItem(\n                          text: Text(\"Account\".tl),\n                          onPressed: () {\n                            addToAccount = true;\n                          }),\n                      MenuFlyoutItem(\n                          text: Text(\"Both\".tl),\n                          onPressed: () {\n                            addToLocal = true;\n                            addToAccount = true;\n                          }),\n                    ],\n                  );\n                });\n          } else {\n            addToLocal = true;\n          }\n          if (addToAccount) {\n            setState(() {\n              isSubmitting = true;\n            });\n            var res =\n                await Network().editMute(blockedTags, blockedUsers, [], []);\n            setState(() {\n              isSubmitting = false;\n            });\n            if (res.error) {\n              if (mounted) {\n                context.showToast(message: \"Network Error\");\n              }\n              return;\n            }\n          }\n          if (addToLocal) {\n            for (var tag in blockedTags) {\n              appdata.settings['blockTags'].add(tag);\n            }\n            for (var user in blockedUsers) {\n              appdata.settings['blockTags'].add('user:$user');\n            }\n            appdata.writeSettings();\n          }\n          if (mounted) {\n            context.pop();\n          }\n        },\n        child: isSubmitting\n            ? const ProgressRing(\n                strokeWidth: 1.6,\n              ).fixWidth(18).fixHeight(18).toAlign(Alignment.center)\n            : Text(\"Submit\".tl),\n      ).fixWidth(96).fixHeight(32),\n    ).toAlign(Alignment.center).paddingTop(16);\n  }\n}\n\nclass IllustPageWithId extends StatefulWidget {\n  const IllustPageWithId(this.id, {super.key});\n\n  final String id;\n\n  @override\n  State<IllustPageWithId> createState() => _IllustPageWithIdState();\n}\n\nclass _IllustPageWithIdState extends LoadingState<IllustPageWithId, Illust> {\n  @override\n  Widget buildContent(BuildContext context, Illust data) {\n    return IllustPage(data);\n  }\n\n  @override\n  Future<Res<Illust>> loadData() {\n    return Network().getIllustByID(widget.id);\n  }\n}\n"
  },
  {
    "path": "lib/pages/image_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter/services.dart';\nimport 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';\nimport 'package:photo_view/photo_view_gallery.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/message.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/cache_manager.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/pages/main_page.dart';\nimport 'package:pixes/utils/io.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:share_plus/share_plus.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass ImagePage extends StatefulWidget {\n  const ImagePage(this.urls, {this.initialPage = 0, super.key});\n\n  final List<String> urls;\n\n  final int initialPage;\n\n  static show(List<String> urls, {int initialPage = 0}) {\n    App.rootNavigatorKey.currentState?.push(AppPageRoute(\n        builder: (context) => ImagePage(urls, initialPage: initialPage)));\n  }\n\n  @override\n  State<ImagePage> createState() => _ImagePageState();\n}\n\nclass _ImagePageState extends State<ImagePage> with WindowListener {\n  int windowButtonKey = 0;\n\n  @override\n  void initState() {\n    windowManager.addListener(this);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    windowManager.removeListener(this);\n    super.dispose();\n  }\n\n  @override\n  void onWindowMaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  @override\n  void onWindowUnmaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  late var controller = PageController(initialPage: widget.initialPage);\n\n  late int currentPage = widget.initialPage;\n\n  var menuController = FlyoutController();\n\n  Future<File?> getFile() async {\n    var image = widget.urls[currentPage];\n    if (image.startsWith(\"file://\")) {\n      return File(image.replaceFirst(\"file://\", \"\"));\n    }\n    var key = image;\n    if (key.startsWith(\"novel:\")) {\n      key = key.split(':').last;\n    }\n    var file = await CacheManager().findCache(key);\n    return file == null ? null : File(file);\n  }\n\n  String getExtensionName() {\n    var fileName = widget.urls[currentPage].split('/').last;\n    if (fileName.contains('.')) {\n      return '.${fileName.split('.').last}';\n    }\n    return '.jpg';\n  }\n\n  void showMenu() {\n    menuController.showFlyout(\n        barrierColor: Colors.transparent,\n        position: App.isMobile ? Offset(context.size!.width, 0) : null,\n        builder: (context) => MenuFlyout(\n              items: [\n                MenuFlyoutItem(\n                    text: Text(\"Save to\".tl),\n                    onPressed: () async {\n                      var file = await getFile();\n                      if (file != null) {\n                        var fileName = widget.urls[currentPage].split('/').last;\n                        if (!fileName.contains('.')) {\n                          fileName += getExtensionName();\n                        }\n                        saveFile(file, fileName);\n                      }\n                    }),\n                if (App.isMobile)\n                  MenuFlyoutItem(\n                      text: Text(\"Save to gallery\".tl),\n                      onPressed: () async {\n                        var file = await getFile();\n                        if (file != null) {\n                          var fileName =\n                              widget.urls[currentPage].split('/').last;\n                          if (!fileName.contains('.')) {\n                            fileName += getExtensionName();\n                          }\n                          await ImageGallerySaverPlus.saveImage(\n                            await file.readAsBytes(),\n                            quality: 100,\n                            name: fileName,\n                          );\n                          if (context.mounted) {\n                            showToast(context, message: \"Saved\".tl);\n                          }\n                        }\n                      }),\n                MenuFlyoutItem(\n                    text: Text(\"Share\".tl),\n                    onPressed: () async {\n                      var file = await getFile();\n                      if (file != null) {\n                        var ext = getExtensionName();\n                        var fileName = widget.urls[currentPage].split('/').last;\n                        if (!fileName.contains('.')) {\n                          fileName += ext;\n                        }\n                        var mediaType = switch (ext) {\n                          '.jpg' => 'image/jpeg',\n                          '.jpeg' => 'image/jpeg',\n                          '.png' => 'image/png',\n                          '.gif' => 'image/gif',\n                          '.webp' => 'image/webp',\n                          _ => 'application/octet-stream'\n                        };\n                        Share.shareXFiles([\n                          XFile.fromData(await file.readAsBytes(),\n                              mimeType: mediaType, name: fileName)\n                        ]);\n                      }\n                    }),\n              ],\n            ));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),\n      color: FluentTheme.of(context).micaBackgroundColor,\n      child: Listener(\n        onPointerSignal: (event) {\n          if (event is PointerScrollEvent &&\n              !HardwareKeyboard.instance.isControlPressed) {\n            if (event.scrollDelta.dy > 0 &&\n                controller.page!.toInt() < widget.urls.length - 1) {\n              controller.jumpToPage(controller.page!.toInt() + 1);\n            } else if (event.scrollDelta.dy < 0 &&\n                controller.page!.toInt() > 0) {\n              controller.jumpToPage(controller.page!.toInt() - 1);\n            }\n          }\n        },\n        child: LayoutBuilder(\n          builder: (context, constrains) {\n            var height = constrains.maxHeight;\n            return Stack(\n              children: [\n                Positioned.fill(\n                    child: PhotoViewGallery.builder(\n                  pageController: controller,\n                  backgroundDecoration:\n                      const BoxDecoration(color: Colors.transparent),\n                  itemCount: widget.urls.length,\n                  builder: (context, index) {\n                    var image = widget.urls[index];\n\n                    return PhotoViewGalleryPageOptions(\n                      filterQuality: FilterQuality.medium,\n                      imageProvider: getImageProvider(image),\n                    );\n                  },\n                  onPageChanged: (index) {\n                    setState(() {\n                      currentPage = index;\n                    });\n                  },\n                )),\n                Positioned(\n                  top: 0,\n                  left: 0,\n                  right: 0,\n                  child: SizedBox(\n                    height: 36,\n                    child: Row(\n                      children: [\n                        if (App.isMacOS)\n                          const SizedBox(width: 72),\n                        const SizedBox(\n                          width: 6,\n                        ),\n                        IconButton(\n                            icon: const Icon(FluentIcons.back).paddingAll(2),\n                            onPressed: () => context.pop()),\n                        const Expanded(\n                          child: DragToMoveArea(\n                            child: SizedBox.expand(),\n                          ),\n                        ),\n                        buildActions(),\n                        if (App.isMacOS)\n                          const SizedBox(width: 16),\n                        if (App.isWindows || App.isLinux)\n                          WindowButtons(\n                            key: ValueKey(windowButtonKey),\n                          ),\n                      ],\n                    ),\n                  ),\n                ),\n                if (currentPage != 0)\n                  Positioned(\n                    left: 0,\n                    top: height / 2 - 9,\n                    child: IconButton(\n                      icon: const Icon(\n                        FluentIcons.chevron_left,\n                        size: 18,\n                      ),\n                      onPressed: () {\n                        controller.previousPage(\n                          duration: const Duration(milliseconds: 300),\n                          curve: Curves.easeInOut,\n                        );\n                      },\n                    ).paddingAll(8),\n                  ),\n                if (currentPage != widget.urls.length - 1)\n                  Positioned(\n                    right: 0,\n                    top: height / 2 - 9,\n                    child: IconButton(\n                      icon: const Icon(FluentIcons.chevron_right, size: 18),\n                      onPressed: () {\n                        controller.nextPage(\n                          duration: const Duration(milliseconds: 300),\n                          curve: Curves.easeInOut,\n                        );\n                      },\n                    ).paddingAll(8),\n                  ),\n                Positioned(\n                  left: 12,\n                  bottom: 8,\n                  child: Text(\n                    \"${currentPage + 1}/${widget.urls.length}\",\n                  ),\n                )\n              ],\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget buildActions() {\n    var width = MediaQuery.of(context).size.width;\n    return FlyoutTarget(\n      controller: menuController,\n      child: width > 600\n          ? Button(\n              onPressed: showMenu,\n              child: Row(\n                children: [\n                  const Icon(\n                    MdIcons.menu,\n                    size: 18,\n                  ),\n                  const SizedBox(\n                    width: 8,\n                  ),\n                  Text('Actions'.tl),\n                ],\n              ))\n          : IconButton(\n              icon: const Icon(\n                MdIcons.more_horiz,\n                size: 20,\n              ),\n              onPressed: showMenu),\n    );\n  }\n\n  ImageProvider getImageProvider(String url) {\n    if (url.startsWith(\"file://\")) {\n      return FileImage(File(url.replaceFirst(\"file://\", \"\")));\n    } else if (url.startsWith(\"novel:\")) {\n      var ids = url.split(':').last.split('/');\n      return CachedNovelImageProvider(ids[0], ids[1]);\n    }\n    return CachedImageProvider(url) as ImageProvider;\n  }\n}\n"
  },
  {
    "path": "lib/pages/login_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/button.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/webview_page.dart';\nimport 'package:pixes/utils/app_links.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\n\nclass LoginPage extends StatefulWidget {\n  const LoginPage(this.callback, {super.key});\n\n  final void Function() callback;\n\n  @override\n  State<LoginPage> createState() => _LoginPageState();\n}\n\nclass _LoginPageState extends State<LoginPage> {\n  bool checked = false;\n\n  bool waitingForAuth = false;\n\n  bool isLogging = false;\n\n  @override\n  Widget build(BuildContext context) {\n    if (isLogging) {\n      return buildLoading(context);\n    } else if (!waitingForAuth) {\n      return buildLogin(context);\n    } else {\n      return buildWaiting(context);\n    }\n  }\n\n  Widget buildLogin(BuildContext context) {\n    return SizedBox(\n      child: Center(\n        child: Card(\n            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),\n            child: SizedBox(\n              width: 300,\n              height: 300,\n              child: Column(\n                children: [\n                  Align(\n                    alignment: Alignment.centerLeft,\n                    child: Text(\n                      \"Login\".tl,\n                      style: const TextStyle(\n                          fontSize: 24, fontWeight: FontWeight.bold),\n                    ),\n                  ),\n                  Expanded(\n                    child: Center(\n                      child: Column(\n                        mainAxisSize: MainAxisSize.min,\n                        children: [\n                          FluentButton(\n                            onPressed: onContinue,\n                            enabled: checked,\n                            width: 96,\n                            child: Text(\"Continue\".tl),\n                          ),\n                          const SizedBox(\n                            height: 16,\n                          ),\n                          Container(\n                            padding: const EdgeInsets.symmetric(\n                                horizontal: 8, vertical: 4),\n                            child: Text(\n                                \"You need to complete the login operation in the browser window that will open.\"\n                                    .tl),\n                          )\n                        ],\n                      ),\n                    ),\n                  ),\n                  Row(\n                    children: [\n                      Checkbox(\n                          checked: checked,\n                          onChanged: (value) => setState(() {\n                                checked = value ?? false;\n                              })),\n                      const SizedBox(\n                        width: 8,\n                      ),\n                      Expanded(\n                        child: Text(\"I understand pixes is a free unofficial application.\".tl),\n                      )\n                    ],\n                  )\n                ],\n              ),\n            )),\n      ),\n    );\n  }\n\n  Widget buildWaiting(BuildContext context) {\n    return SizedBox(\n      child: Center(\n        child: Card(\n            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),\n            child: SizedBox(\n              width: 300,\n              height: 300,\n              child: Column(\n                children: [\n                  Align(\n                    alignment: Alignment.centerLeft,\n                    child: Text(\n                      \"Waiting...\".tl,\n                      style: const TextStyle(\n                          fontSize: 24, fontWeight: FontWeight.bold),\n                    ),\n                  ),\n                  Expanded(\n                    child: Center(\n                      child: Container(\n                        padding: const EdgeInsets.symmetric(\n                            horizontal: 8, vertical: 4),\n                        child: Text(\n                            \"Waiting for authentication. Please finished in the browser.\"\n                                .tl),\n                      ),\n                    ),\n                  ),\n                  Row(\n                    children: [\n                      Button(\n                          child: Text(\"Back\".tl),\n                          onPressed: () {\n                            setState(() {\n                              waitingForAuth = false;\n                            });\n                          }),\n                      const Spacer(),\n                    ],\n                  )\n                ],\n              ),\n            )),\n      ),\n    );\n  }\n\n  Widget buildLoading(BuildContext context) {\n    return SizedBox(\n      child: Center(\n        child: Card(\n            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),\n            child: SizedBox(\n              width: 300,\n              height: 300,\n              child: Column(\n                children: [\n                  Align(\n                    alignment: Alignment.centerLeft,\n                    child: Text(\n                      \"Logging in\".tl,\n                      style: const TextStyle(\n                          fontSize: 24, fontWeight: FontWeight.bold),\n                    ),\n                  ),\n                  const Expanded(\n                    child: Center(\n                      child: ProgressRing(),\n                    ),\n                  ),\n                ],\n              ),\n            )),\n      ),\n    );\n  }\n\n  void onContinue() async {\n    bool? useExternal;\n    if (App.isMobile) {\n      await showDialog(\n        context: context,\n        barrierDismissible: true,\n        builder: (context) => ContentDialog(\n            title: Text(\"Choose a way to login\".tl),\n            content: Text(\"${\"Use Webview: you cannot sign in with Google.\".tl}\"\n                \"\\n\\n\"\n                \"${\"Use an external browser: You can sign in using Google. However, some browsers may not be compatible with the application\".tl}\"),\n            actions: [\n              Button(\n                child: Text(\"Webview\".tl),\n                onPressed: () {\n                  useExternal = false;\n                  App.rootNavigatorKey.currentState!.pop();\n                },\n              ),\n              Button(\n                child: Text(\"External browser\".tl),\n                onPressed: () {\n                  useExternal = true;\n                  App.rootNavigatorKey.currentState!.pop();\n                },\n              )\n            ]),\n      );\n    } else {\n      useExternal = true;\n    }\n    if (useExternal == null) {\n      return;\n    }\n    var url = await Network().generateWebviewUrl();\n    onLink = (uri) {\n      if (uri.scheme == \"pixiv\") {\n        onFinished(uri.queryParameters[\"code\"]!);\n        onLink = null;\n        return true;\n      }\n      return false;\n    };\n    setState(() {\n      waitingForAuth = true;\n    });\n    if (!useExternal! && mounted) {\n      context.to(() => WebviewPage(\n            url,\n            onNavigation: (req) {\n              if (req.url.startsWith(\"pixiv://\")) {\n                App.rootNavigatorKey.currentState!.pop();\n                onLink?.call(Uri.parse(req.url));\n                return false;\n              }\n              return true;\n            },\n          ));\n    } else {\n      launchUrlString(url);\n    }\n  }\n\n  void onFinished(String code) async {\n    setState(() {\n      isLogging = true;\n      waitingForAuth = false;\n    });\n    var res = await Network().loginWithCode(code);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: res.errorMessage!);\n      }\n      setState(() {\n        isLogging = false;\n      });\n    } else {\n      widget.callback();\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/logs.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/log.dart';\n\nclass LogsPage extends StatefulWidget {\n  const LogsPage({super.key});\n\n  @override\n  State<LogsPage> createState() => _LogsPageState();\n}\n\nclass _LogsPageState extends State<LogsPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        const TitleBar(title: \"Logs\"),\n        Expanded(\n          child: ListView.builder(\n            reverse: true,\n            controller: ScrollController(),\n            itemCount: Log.logs.length,\n            itemBuilder: (context, index){\n              index =  Log.logs.length - index - 1;\n              return Padding(\n                padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),\n                child: SelectionArea(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    children: [\n                      Row(\n                        children: [\n                          Container(\n                            decoration: BoxDecoration(\n                              color: ColorScheme.of(context).surfaceContainerHighest,\n                              borderRadius: const BorderRadius.all(Radius.circular(16)),\n                            ),\n                            child: Padding(\n                              padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),\n                              child: Text(Log.logs[index].title),\n                            ),\n                          ),\n                          const SizedBox(width: 3,),\n                          Container(\n                            decoration: BoxDecoration(\n                              color: [\n                                ColorScheme.of(context).error,\n                                ColorScheme.of(context).errorContainer,\n                                ColorScheme.of(context).primaryContainer\n                              ][Log.logs[index].level.index],\n                              borderRadius: const BorderRadius.all(Radius.circular(16)),\n                            ),\n                            child: Padding(\n                              padding: const EdgeInsets.fromLTRB(5, 0, 5, 1),\n                              child: Text(\n                                Log.logs[index].level.name,\n                                style: TextStyle(color: Log.logs[index].level.index==0?Colors.white:Colors.black),),\n                            ),\n                          ),\n                        ],\n                      ),\n                      Text(Log.logs[index].content),\n                      Text(Log.logs[index].time.toString().replaceAll(RegExp(r\"\\.\\w+\"), \"\")),\n                      Button(onPressed: (){\n                        Clipboard.setData(ClipboardData(text: Log.logs[index].content));\n                      }, child: const Text(\"复制\")),\n                      const Divider(),\n                    ],\n                  ),\n                ),\n              );\n            },\n          )\n        )\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/main_page.dart",
    "content": "import \"dart:async\";\n\nimport \"package:fluent_ui/fluent_ui.dart\";\nimport \"package:flutter/foundation.dart\";\nimport \"package:flutter/services.dart\";\nimport \"package:pixes/appdata.dart\";\nimport \"package:pixes/components/md.dart\";\nimport \"package:pixes/foundation/app.dart\";\nimport \"package:pixes/foundation/image_provider.dart\";\nimport \"package:pixes/network/network.dart\";\nimport \"package:pixes/pages/bookmarks.dart\";\nimport \"package:pixes/pages/downloaded_page.dart\";\nimport \"package:pixes/pages/following_artworks.dart\";\nimport \"package:pixes/pages/following_novels_page.dart\";\nimport \"package:pixes/pages/history.dart\";\nimport \"package:pixes/pages/novel_bookmarks_page.dart\";\nimport \"package:pixes/pages/novel_ranking_page.dart\";\nimport \"package:pixes/pages/novel_recommendation_page.dart\";\nimport \"package:pixes/pages/ranking.dart\";\nimport \"package:pixes/pages/recommendation_page.dart\";\nimport \"package:pixes/pages/login_page.dart\";\nimport \"package:pixes/pages/search_page.dart\";\nimport \"package:pixes/pages/settings_page.dart\";\nimport \"package:pixes/pages/user_info_page.dart\";\nimport \"package:pixes/utils/loop.dart\";\nimport \"package:pixes/utils/mouse_listener.dart\";\nimport \"package:pixes/utils/translation.dart\";\nimport \"package:window_manager/window_manager.dart\";\n\nimport \"../components/page_route.dart\";\nimport \"../utils/debug.dart\";\nimport \"downloading_page.dart\";\n\ndouble get _appBarHeight => App.isDesktop ? 36.0 : 48.0;\n\nclass TitleBarAction {\n  final IconData icon;\n  final String title;\n  final void Function() onPressed;\n\n  TitleBarAction(this.icon, this.title, this.onPressed);\n}\n\nclass TitleBarController extends StateController {\n  TitleBarController();\n\n  final List<TitleBarAction> actions = [\n    if (kDebugMode) TitleBarAction(MdIcons.bug_report, \"Debug\", debug)\n  ];\n\n  void addAction(TitleBarAction action) {\n    actions.add(action);\n    update();\n  }\n\n  void removeAction(TitleBarAction action) {\n    actions.remove(action);\n    update();\n  }\n}\n\nclass MainPage extends StatefulWidget {\n  const MainPage({super.key});\n\n  @override\n  State<MainPage> createState() => _MainPageState();\n}\n\nclass _MainPageState extends State<MainPage>\n    with WindowListener\n    implements PopEntry {\n  final navigatorKey = GlobalKey<NavigatorState>();\n\n  int index = 4;\n\n  int windowButtonKey = 0;\n\n  ModalRoute<dynamic>? _route;\n\n  @override\n  void initState() {\n    StateController.put<TitleBarController>(TitleBarController());\n    windowManager.addListener(this);\n    listenMouseSideButtonToBack(navigatorKey);\n    App.mainNavigatorKey = navigatorKey;\n    index = appdata.settings[\"initialPage\"] ?? 4;\n    super.initState();\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);\n    if (nextRoute != _route) {\n      _route?.unregisterPopEntry(this);\n      _route = nextRoute;\n      _route?.registerPopEntry(this);\n    }\n  }\n\n  @override\n  void dispose() {\n    StateController.remove<TitleBarController>();\n    windowManager.removeListener(this);\n    ModalRoute.of(context)!.unregisterPopEntry(this);\n    super.dispose();\n  }\n\n  @override\n  void onWindowMaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  @override\n  void onWindowUnmaximize() {\n    setState(() {\n      windowButtonKey++;\n    });\n  }\n\n  bool get isLogin => Network().token != null;\n\n  @override\n  Widget build(BuildContext context) {\n    if (!isLogin) {\n      return NavigationView(\n        appBar: buildAppBar(context, navigatorKey),\n        content: LoginPage(() => setState(() {})),\n      );\n    }\n    return DefaultSelectionStyle.merge(\n      selectionColor: FluentTheme.of(context).selectionColor.toOpacity(0.4),\n      child: NavigationView(\n        appBar: buildAppBar(context, navigatorKey),\n        pane: NavigationPane(\n          selected: index,\n          onChanged: (value) {\n            setState(() {\n              index = value;\n            });\n            navigate(value);\n          },\n          items: [\n            UserPane(),\n            PaneItem(\n              icon: const Icon(\n                MdIcons.search,\n                size: 20,\n              ),\n              title: Text('Search'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(\n                MdIcons.downloading,\n                size: 20,\n              ),\n              title: Text('Downloading'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(\n                MdIcons.download,\n                size: 20,\n              ),\n              title: Text('Downloaded'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItemSeparator(),\n            PaneItemHeader(\n                header: Text('${\"Illustrations\".tl}/${\"Manga\".tl}')\n                    .paddingBottom(4)\n                    .paddingLeft(8)),\n            PaneItem(\n              icon: const Icon(\n                MdIcons.explore_outlined,\n                size: 20,\n              ),\n              title: Text('Explore'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.bookmark_outline, size: 20),\n              title: Text('Bookmarks'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.interests_outlined, size: 20),\n              title: Text('Following'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.history, size: 20),\n              title: Text('History'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.leaderboard_outlined, size: 20),\n              title: Text('Ranking'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItemSeparator(),\n            PaneItemHeader(\n                header: Text(\"Novel\".tl).paddingBottom(4).paddingLeft(8)),\n            PaneItem(\n              icon: const Icon(MdIcons.featured_play_list_outlined, size: 20),\n              title: Text('Recommendation'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.collections_bookmark_outlined, size: 20),\n              title: Text('Bookmarks'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.interests_outlined, size: 20),\n              title: Text('Following'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItem(\n              icon: const Icon(MdIcons.leaderboard_outlined, size: 20),\n              title: Text('Ranking'.tl),\n              body: const SizedBox.shrink(),\n            ),\n            PaneItemSeparator(),\n            PaneItemAction(\n              icon: const Icon(MdIcons.settings_outlined, size: 20),\n              title: Text('Settings'.tl),\n              body: const SizedBox.shrink(),\n              onTap: () {\n                navigatorKey.currentContext?.to(() => const SettingsPage());\n              },\n            ),\n          ],\n        ),\n        paneBodyBuilder: (pane, child) => MediaQuery.removePadding(\n          context: context,\n          removeTop: true,\n          child: Navigator(\n            key: navigatorKey,\n            onGenerateRoute: (settings) => AppPageRoute(\n              isRoot: true,\n              builder: (context) => pageBuilders.elementAtOrNull(index)!(),\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  static final pageBuilders = <Widget Function()>[\n    () => UserInfoPage(appdata.account!.user.id),\n    () => const SearchPage(),\n    () => const DownloadingPage(),\n    () => const DownloadedPage(),\n    () => const RecommendationPage(),\n    () => const BookMarkedArtworkPage(),\n    () => const FollowingArtworksPage(),\n    () => const HistoryPage(),\n    () => const RankingPage(),\n    () => const NovelRecommendationPage(),\n    () => const NovelBookmarksPage(),\n    () => const FollowingNovelsPage(),\n    () => const NovelRankingPage(),\n  ];\n\n  void navigate(int index) {\n    var page = pageBuilders.elementAtOrNull(index) ??\n        () => Center(\n              child: Text(\"Invalid Page: $index\"),\n            );\n    navigatorKey.currentState!.pushAndRemoveUntil(\n      AppPageRoute(\n        builder: (context) => page(),\n        isRoot: true,\n      ),\n      (route) => false,\n    );\n  }\n\n  NavigationAppBar buildAppBar(\n      BuildContext context, GlobalKey<NavigatorState> navigatorKey) {\n    return NavigationAppBar(\n      automaticallyImplyLeading: false,\n      height: _appBarHeight,\n      title: () {\n        return StateBuilder<TitleBarController>(\n          builder: (controller) {\n            Widget content = Padding(\n              padding: const EdgeInsets.only(bottom: 4),\n              child: Align(\n                alignment: AlignmentDirectional.centerStart,\n                child: Row(\n                  children: [\n                    if (App.isMacOS)\n                      const SizedBox(width: 72),\n                    if (App.isMacOS)\n                      _MacosBackButton(navigatorKey).paddingRight(8),\n                    if (!App.isDesktop)\n                      const Text(\n                        \"Pixes\",\n                        style: TextStyle(fontSize: 13),\n                      ),\n                    if (!App.isDesktop) const Spacer(),\n                    if (App.isDesktop)\n                      const Expanded(\n                        child: SizedBox(\n                          height: double.infinity,\n                          child: DragToMoveArea(\n                              child: Align(\n                            alignment: Alignment.centerLeft,\n                            child: Text(\n                              \"Pixes\",\n                              style: TextStyle(fontSize: 13),\n                            ),\n                          )),\n                        ),\n                      ),\n                    for (var action in controller.actions)\n                      Button(\n                        onPressed: action.onPressed,\n                        child: Row(\n                          children: [\n                            Icon(\n                              action.icon,\n                              size: 18,\n                            ),\n                            const SizedBox(width: 4),\n                            Text(action.title),\n                          ],\n                        ),\n                      ).paddingTop(4).paddingLeft(4),\n                    if (App.isDesktop)\n                      const SizedBox(width: 128)\n                    else\n                      Tooltip(\n                          message: \"Search\".tl,\n                          child: IconButton(\n                            icon: const Icon(\n                              MdIcons.search,\n                              size: 18,\n                            ),\n                            onPressed: () {\n                              if (index == 1) {\n                                return;\n                              }\n                              setState(() {\n                                index = 1;\n                              });\n                              navigate(1);\n                            },\n                          )),\n                  ],\n                ),\n              ),\n            );\n\n            return content;\n          },\n        );\n      }(),\n      leading: App.isMacOS ? null : _BackButton(navigatorKey),\n      actions: App.isDesktop && !App.isMacOS\n          ? WindowButtons(\n              key: ValueKey(windowButtonKey),\n            )\n          : null,\n    );\n  }\n\n  final popValue = ValueNotifier(false);\n\n  @override\n  ValueListenable<bool> get canPopNotifier => popValue;\n\n  @override\n  void onPopInvokedWithResult(bool didPop, result) {\n    if (App.rootNavigatorKey.currentState?.canPop() ?? false) {\n      App.rootNavigatorKey.currentState?.pop();\n    } else if (App.mainNavigatorKey?.currentState?.canPop() ?? false) {\n      App.mainNavigatorKey?.currentState?.pop();\n    } else {\n      SystemNavigator.pop();\n    }\n  }\n\n  @override\n  void onPopInvoked(bool didPop) {}\n}\n\nclass _BackButton extends StatefulWidget {\n  const _BackButton(this.navigatorKey);\n\n  final GlobalKey<NavigatorState> navigatorKey;\n\n  @override\n  State<_BackButton> createState() => _BackButtonState();\n}\n\nclass _BackButtonState extends State<_BackButton> {\n  GlobalKey<NavigatorState> get navigatorKey => widget.navigatorKey;\n\n  bool enabled = false;\n\n  Timer? timer;\n\n  @override\n  void initState() {\n    enabled = navigatorKey.currentState?.canPop() == true;\n    Loop.register(loop);\n    super.initState();\n  }\n\n  void loop() {\n    bool enabled = navigatorKey.currentState?.canPop() == true;\n    if (enabled != this.enabled) {\n      setState(() {\n        this.enabled = enabled;\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    Loop.remove(loop);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    void onPressed() {\n      if (navigatorKey.currentState?.canPop() ?? false) {\n        navigatorKey.currentState?.pop();\n      }\n    }\n\n    return NavigationPaneTheme(\n      data: NavigationPaneTheme.of(context).merge(NavigationPaneThemeData(\n        unselectedIconColor: WidgetStateProperty.resolveWith((states) {\n          if (states.isDisabled) {\n            return ButtonThemeData.buttonColor(context, states);\n          }\n          return ButtonThemeData.uncheckedInputColor(\n            FluentTheme.of(context),\n            states,\n          ).basedOnLuminance();\n        }),\n      )),\n      child: Builder(\n        builder: (context) => PaneItem(\n          icon: const Center(child: Icon(FluentIcons.back, size: 12.0)),\n          title: const Text(\"Back\"),\n          body: const SizedBox.shrink(),\n          enabled: enabled,\n        )\n            .build(\n              context,\n              false,\n              onPressed,\n              displayMode: PaneDisplayMode.compact,\n            )\n            .paddingTop(2),\n      ),\n    );\n  }\n}\n\nclass _MacosBackButton extends StatefulWidget {\n  const _MacosBackButton(this.navigatorKey);\n\n  final GlobalKey<NavigatorState> navigatorKey;\n\n  @override\n  State<_MacosBackButton> createState() => __MacosBackButtonState();\n}\n\nclass __MacosBackButtonState extends State<_MacosBackButton> {\n  GlobalKey<NavigatorState> get navigatorKey => widget.navigatorKey;\n\n  bool enabled = false;\n  bool _isHovered = false;\n\n  @override\n  void initState() {\n    enabled = navigatorKey.currentState?.canPop() == true;\n    Loop.register(loop);\n    super.initState();\n  }\n\n  void loop() {\n    bool enabled = navigatorKey.currentState?.canPop() == true;\n    if (enabled != this.enabled) {\n      setState(() {\n        this.enabled = enabled;\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    Loop.remove(loop);\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final theme = FluentTheme.of(context);\n    final iconColor = enabled\n        ? theme.iconTheme.color ?? Colors.black\n        : (theme.iconTheme.color ?? Colors.black).toOpacity(0.3);\n    final bgColor = (_isHovered && enabled)\n        ? theme.inactiveBackgroundColor\n        : theme.micaBackgroundColor;\n\n    return GestureDetector(\n      onTap: enabled\n          ? () {\n              if (navigatorKey.currentState?.canPop() ?? false) {\n                navigatorKey.currentState?.pop();\n              }\n            }\n          : null,\n      child: MouseRegion(\n        onEnter: (_) => setState(() => _isHovered = true),\n        onExit: (_) => setState(() => _isHovered = false),\n        cursor: enabled ? SystemMouseCursors.click : SystemMouseCursors.basic,\n        child: AnimatedContainer(\n          duration: const Duration(milliseconds: 100),\n          decoration: BoxDecoration(\n            color: bgColor,\n            borderRadius: BorderRadius.circular(4),\n          ),\n          padding: const EdgeInsets.all(4),\n          child: Icon(\n            FluentIcons.back,\n            size: 14,\n            color: iconColor,\n          ),\n        ),\n      ),\n    );\n  }\n}\n\nclass WindowButtons extends StatelessWidget {\n  const WindowButtons({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    final FluentThemeData theme = FluentTheme.of(context);\n    final color = theme.iconTheme.color ?? Colors.black;\n    final hoverColor = theme.inactiveBackgroundColor;\n\n    return SizedBox(\n      width: 138,\n      height: _appBarHeight,\n      child: Row(\n        children: [\n          WindowButton(\n            icon: MinimizeIcon(color: color),\n            hoverColor: hoverColor,\n            onPressed: () async {\n              bool isMinimized = await windowManager.isMinimized();\n              if (isMinimized) {\n                windowManager.restore();\n              } else {\n                windowManager.minimize();\n              }\n            },\n          ),\n          FutureBuilder<bool>(\n            future: windowManager.isMaximized(),\n            builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {\n              if (snapshot.data == true) {\n                return WindowButton(\n                  icon: RestoreIcon(\n                    color: color,\n                  ),\n                  hoverColor: hoverColor,\n                  onPressed: () {\n                    windowManager.unmaximize();\n                  },\n                );\n              }\n              return WindowButton(\n                icon: MaximizeIcon(\n                  color: color,\n                ),\n                hoverColor: hoverColor,\n                onPressed: () {\n                  windowManager.maximize();\n                },\n              );\n            },\n          ),\n          WindowButton(\n            icon: CloseIcon(\n              color: color,\n            ),\n            hoverIcon: CloseIcon(\n              color: theme.brightness == Brightness.light\n                  ? Colors.white\n                  : Colors.black,\n            ),\n            hoverColor: Colors.red,\n            onPressed: () {\n              windowManager.close();\n            },\n          ),\n        ],\n      ),\n    );\n  }\n}\n\nclass WindowButton extends StatefulWidget {\n  const WindowButton(\n      {required this.icon,\n      required this.onPressed,\n      required this.hoverColor,\n      this.hoverIcon,\n      super.key});\n\n  final Widget icon;\n\n  final void Function() onPressed;\n\n  final Color hoverColor;\n\n  final Widget? hoverIcon;\n\n  @override\n  State<WindowButton> createState() => _WindowButtonState();\n}\n\nclass _WindowButtonState extends State<WindowButton> {\n  bool isHovering = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return MouseRegion(\n      onEnter: (event) => setState(() {\n        isHovering = true;\n      }),\n      onExit: (event) => setState(() {\n        isHovering = false;\n      }),\n      child: GestureDetector(\n        onTap: widget.onPressed,\n        child: Container(\n          width: 46,\n          height: double.infinity,\n          decoration:\n              BoxDecoration(color: isHovering ? widget.hoverColor : null),\n          child: isHovering ? widget.hoverIcon ?? widget.icon : widget.icon,\n        ),\n      ),\n    );\n  }\n}\n\nclass UserPane extends PaneItem {\n  UserPane() : super(icon: const SizedBox(), body: const SizedBox());\n\n  @override\n  Widget build(BuildContext context, bool selected, VoidCallback? onPressed,\n      {PaneDisplayMode? displayMode,\n      bool showTextOnTop = true,\n      int? itemIndex,\n      bool? autofocus}) {\n    final maybeBody = NavigationView.maybeOf(context);\n    var mode = displayMode ?? maybeBody?.displayMode ?? PaneDisplayMode.minimal;\n\n    if (maybeBody?.compactOverlayOpen == true) {\n      mode = PaneDisplayMode.open;\n    }\n\n    Widget body = () {\n      switch (mode) {\n        case PaneDisplayMode.minimal:\n        case PaneDisplayMode.open:\n          return LayoutBuilder(builder: (context, constrains) {\n            if (constrains.maxHeight < 72 || constrains.maxWidth < 120) {\n              return const SizedBox();\n            }\n            return Container(\n              width: double.infinity,\n              height: 64,\n              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),\n              child: Row(\n                children: [\n                  Center(\n                    child: ClipRRect(\n                      borderRadius: BorderRadius.circular(48),\n                      child: Image(\n                        height: 48,\n                        width: 48,\n                        image:\n                            CachedImageProvider(appdata.account!.user.profile),\n                        fit: BoxFit.fill,\n                      ),\n                    ),\n                  ),\n                  const SizedBox(\n                    width: 8,\n                  ),\n                  if (constrains.maxWidth > 90)\n                    Expanded(\n                      child: Center(\n                        child: SizedBox(\n                          width: double.infinity,\n                          child: Column(\n                            mainAxisSize: MainAxisSize.min,\n                            crossAxisAlignment: CrossAxisAlignment.start,\n                            children: [\n                              Text(\n                                appdata.account!.user.name,\n                                style: const TextStyle(\n                                    fontSize: 16, fontWeight: FontWeight.w500),\n                              ),\n                              Text(\n                                kDebugMode\n                                    ? \"<hide due to debug>\"\n                                    : appdata.account!.user.email,\n                                style: const TextStyle(fontSize: 12),\n                              )\n                            ],\n                          ),\n                        ),\n                      ),\n                    )\n                ],\n              ),\n            );\n          });\n        case PaneDisplayMode.compact:\n        case PaneDisplayMode.top:\n          return LayoutBuilder(builder: (context, constrains) {\n            if (constrains.maxHeight < 48 || constrains.maxWidth < 32) {\n              return const SizedBox();\n            }\n            return Center(\n              child: ClipRRect(\n                borderRadius: BorderRadius.circular(32),\n                child: Image(\n                  height: 30,\n                  width: 30,\n                  image: NetworkImage(appdata.account!.user.profile),\n                  fit: BoxFit.fill,\n                ),\n              ).paddingAll(4),\n            );\n          });\n        default:\n          throw \"Invalid Display mode\";\n      }\n    }();\n\n    var button = HoverButton(\n      builder: (context, states) {\n        final theme = NavigationPaneTheme.of(context);\n        return Container(\n          margin: const EdgeInsets.symmetric(horizontal: 6.0),\n          decoration: BoxDecoration(\n            color: () {\n              final tileColor = this.tileColor ??\n                  theme.tileColor ??\n                  kDefaultPaneItemColor(context, mode == PaneDisplayMode.top);\n              final newStates = states.toSet()..remove(WidgetState.disabled);\n              if (selected && selectedTileColor != null) {\n                return selectedTileColor!.resolve(newStates);\n              }\n              return tileColor.resolve(\n                selected\n                    ? {\n                        states.isHovered\n                            ? WidgetState.pressed\n                            : WidgetState.hovered,\n                      }\n                    : newStates,\n              );\n            }(),\n            borderRadius: BorderRadius.circular(4.0),\n          ),\n          child: FocusBorder(\n            focused: states.isFocused,\n            renderOutside: false,\n            child: body,\n          ),\n        );\n      },\n      onPressed: onPressed,\n    );\n\n    return Padding(\n      key: key,\n      padding: const EdgeInsetsDirectional.only(bottom: 4.0),\n      child: button,\n    );\n  }\n}\n\n/// Close\nclass CloseIcon extends StatelessWidget {\n  final Color color;\n\n  const CloseIcon({super.key, required this.color});\n\n  @override\n  Widget build(BuildContext context) => _AlignedPaint(_ClosePainter(color));\n}\n\nclass _ClosePainter extends _IconPainter {\n  _ClosePainter(super.color);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    Paint p = getPaint(color, true);\n    canvas.drawLine(const Offset(0, 0), Offset(size.width, size.height), p);\n    canvas.drawLine(Offset(0, size.height), Offset(size.width, 0), p);\n  }\n}\n\n/// Maximize\nclass MaximizeIcon extends StatelessWidget {\n  final Color color;\n\n  const MaximizeIcon({super.key, required this.color});\n\n  @override\n  Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));\n}\n\nclass _MaximizePainter extends _IconPainter {\n  _MaximizePainter(super.color);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    Paint p = getPaint(color);\n    canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);\n  }\n}\n\n/// Restore\nclass RestoreIcon extends StatelessWidget {\n  final Color color;\n\n  const RestoreIcon({\n    super.key,\n    required this.color,\n  });\n\n  @override\n  Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));\n}\n\nclass _RestorePainter extends _IconPainter {\n  _RestorePainter(super.color);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    Paint p = getPaint(color);\n    canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);\n    canvas.drawLine(const Offset(2, 2), const Offset(2, 0), p);\n    canvas.drawLine(const Offset(2, 0), Offset(size.width, 0), p);\n    canvas.drawLine(\n        Offset(size.width, 0), Offset(size.width, size.height - 2), p);\n    canvas.drawLine(Offset(size.width, size.height - 2),\n        Offset(size.width - 2, size.height - 2), p);\n  }\n}\n\n/// Minimize\nclass MinimizeIcon extends StatelessWidget {\n  final Color color;\n\n  const MinimizeIcon({super.key, required this.color});\n\n  @override\n  Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));\n}\n\nclass _MinimizePainter extends _IconPainter {\n  _MinimizePainter(super.color);\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    Paint p = getPaint(color);\n    canvas.drawLine(\n        Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);\n  }\n}\n\n/// Helpers\nabstract class _IconPainter extends CustomPainter {\n  _IconPainter(this.color);\n\n  final Color color;\n\n  @override\n  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;\n}\n\nclass _AlignedPaint extends StatelessWidget {\n  const _AlignedPaint(this.painter);\n\n  final CustomPainter painter;\n\n  @override\n  Widget build(BuildContext context) {\n    return Align(\n        alignment: Alignment.center,\n        child: CustomPaint(size: const Size(10, 10), painter: painter));\n  }\n}\n\nPaint getPaint(Color color, [bool isAntiAlias = false]) => Paint()\n  ..color = color\n  ..style = PaintingStyle.stroke\n  ..isAntiAlias = isAntiAlias\n  ..strokeWidth = 1;\n"
  },
  {
    "path": "lib/pages/novel_bookmarks_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/widget_utils.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nclass NovelBookmarksPage extends StatefulWidget {\n  const NovelBookmarksPage({super.key});\n\n  @override\n  State<NovelBookmarksPage> createState() => _NovelBookmarksPageState();\n}\n\nclass _NovelBookmarksPageState\n    extends MultiPageLoadingState<NovelBookmarksPage, Novel> {\n  bool public = true;\n\n  @override\n  Widget? buildFrame(BuildContext context, Widget child) {\n    return Column(\n      children: [\n        TitleBar(\n          title: \"Bookmarks\".tl,\n          action: SegmentedButton(\n            options: [\n              SegmentedButtonOption(\"public\", \"Public\".tl),\n              SegmentedButtonOption(\"private\", \"Private\".tl),\n            ],\n            onPressed: (key) {\n              var newPublic = key == \"public\";\n              if (newPublic != public) {\n                public = newPublic;\n                nextUrl = null;\n                reset();\n              }\n            },\n            value: public ? \"public\" : \"private\",\n          ),\n        ),\n        Expanded(\n          child: child,\n        )\n      ],\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, List<Novel> data) {\n    return Column(\n      children: [\n        Expanded(\n          child: GridViewWithFixedItemHeight(\n            itemCount: data.length,\n            itemHeight: 164,\n            minCrossAxisExtent: 400,\n            builder: (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return NovelWidget(data[index]);\n            },\n          ).paddingHorizontal(8),\n        )\n      ],\n    );\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(int page) async {\n    if (nextUrl == \"end\") return Res.error(\"No more data\");\n    var res = nextUrl == null\n        ? await Network().getBookmarkedNovels(appdata.account!.user.id, public)\n        : await Network().getNovelsWithNextUrl(nextUrl!);\n    nextUrl = res.subData ?? \"end\";\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/novel_page.dart",
    "content": "import 'dart:collection';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/comments_page.dart';\nimport 'package:pixes/pages/novel_reading_page.dart';\nimport 'package:pixes/pages/search_page.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/app_links.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\n\nconst kFluentButtonPadding = 28.0;\n\nclass NovelPage extends StatefulWidget {\n  const NovelPage(this.novel, {super.key});\n\n  final Novel novel;\n\n  @override\n  State<NovelPage> createState() => _NovelPageState();\n}\n\nclass _NovelPageState extends State<NovelPage> {\n  final scrollController = ScrollController();\n\n  @override\n  Widget build(BuildContext context) {\n    return Scrollbar(\n        controller: scrollController,\n        child: ScrollConfiguration(\n          behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),\n          child: CustomScrollView(\n            controller: scrollController,\n            slivers: [\n              SliverToBoxAdapter(\n                child: buildTop(),\n              ),\n              SliverToBoxAdapter(\n                child: buildActions(),\n              ),\n              SliverToBoxAdapter(\n                child: buildDescription(),\n              ),\n              if (widget.novel.seriesId != null)\n                NovelSeriesWidget(\n                    widget.novel.seriesId!, widget.novel.seriesTitle!),\n              SliverPadding(\n                  padding: EdgeInsets.only(\n                      top: 16 + MediaQuery.of(context).padding.bottom)),\n            ],\n          ),\n        ).padding(const EdgeInsets.symmetric(horizontal: 16)));\n  }\n\n  Widget buildTop() {\n    return Card(\n        child: SizedBox(\n      height: 128,\n      child: Row(\n        children: [\n          Container(\n            width: 96,\n            height: double.infinity,\n            decoration: BoxDecoration(\n              color: ColorScheme.of(context).secondaryContainer,\n              borderRadius: BorderRadius.circular(4),\n            ),\n            clipBehavior: Clip.antiAlias,\n            child: AnimatedImage(\n                fit: BoxFit.cover,\n                filterQuality: FilterQuality.medium,\n                width: double.infinity,\n                height: double.infinity,\n                image: CachedImageProvider(widget.novel.image)),\n          ),\n          const SizedBox(width: 12),\n          Expanded(\n            child: Column(\n              crossAxisAlignment: CrossAxisAlignment.start,\n              mainAxisAlignment: MainAxisAlignment.start,\n              children: [\n                Text(widget.novel.title,\n                    maxLines: 3,\n                    style: const TextStyle(\n                      fontSize: 16,\n                      fontWeight: FontWeight.bold,\n                    )),\n                const SizedBox(height: 4),\n                const Spacer(),\n                if (widget.novel.seriesId != null)\n                  Text(\n                    overflow: TextOverflow.ellipsis,\n                    \"${\"Series\".tl}: ${widget.novel.seriesTitle!}\",\n                    style: TextStyle(\n                      color: ColorScheme.of(context).primary,\n                      fontSize: 12,\n                    ),\n                  ).paddingVertical(4)\n              ],\n            ),\n          ),\n        ],\n      ),\n    )).paddingTop(12);\n  }\n\n  Widget buildStats() {\n    return Container(\n      height: 74,\n      constraints: const BoxConstraints(maxWidth: 560),\n      padding: const EdgeInsets.only(bottom: 10),\n      child: Row(\n        children: [\n          const SizedBox(width: 2),\n          Expanded(\n            child: Container(\n              height: 68,\n              decoration: BoxDecoration(\n                border: Border.all(\n                  color: ColorScheme.of(context).outlineVariant,\n                  width: 0.6,\n                ),\n                borderRadius: BorderRadius.circular(4),\n              ),\n              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n              child: Row(\n                children: [\n                  Column(\n                    mainAxisSize: MainAxisSize.min,\n                    children: [\n                      const Icon(\n                        FluentIcons.view,\n                        size: 20,\n                      ),\n                      Text(\n                        \"Views\".tl,\n                        style: const TextStyle(fontSize: 12),\n                      )\n                    ],\n                  ),\n                  const SizedBox(width: 8),\n                  Text(\n                    widget.novel.totalViews.toString(),\n                    style: TextStyle(\n                      color: ColorScheme.of(context).primary,\n                      fontWeight: FontWeight.w500,\n                      fontSize: 18,\n                    ),\n                  )\n                ],\n              ),\n            ),\n          ),\n          const SizedBox(width: 16),\n          Expanded(\n              child: Container(\n            height: 68,\n            decoration: BoxDecoration(\n              border: Border.all(\n                color: ColorScheme.of(context).outlineVariant,\n                width: 0.6,\n              ),\n              borderRadius: BorderRadius.circular(4),\n            ),\n            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\n            child: Row(\n              children: [\n                Column(\n                  mainAxisSize: MainAxisSize.min,\n                  children: [\n                    const Icon(\n                      FluentIcons.six_point_star,\n                      size: 20,\n                    ),\n                    Text(\n                      \"Favorites\".tl,\n                      style: const TextStyle(fontSize: 12),\n                    )\n                  ],\n                ),\n                const SizedBox(width: 8),\n                Text(\n                  widget.novel.totalBookmarks.toString(),\n                  style: TextStyle(\n                    color: ColorScheme.of(context).primary,\n                    fontWeight: FontWeight.w500,\n                    fontSize: 18,\n                  ),\n                )\n              ],\n            ),\n          )),\n          const SizedBox(width: 2),\n        ],\n      ),\n    );\n  }\n\n  Widget buildAuthor() {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(maxWidth: 560),\n      child: Card(\n        margin: const EdgeInsets.only(left: 2, right: 2, bottom: 12),\n        borderColor: ColorScheme.of(context).outlineVariant.toOpacity(0.52),\n        child: GestureDetector(\n          behavior: HitTestBehavior.opaque,\n          onTap: () {\n            context.to(() => UserInfoPage(widget.novel.author.id.toString()));\n          },\n          child: SizedBox(\n            height: 38,\n            child: Row(\n              children: [\n                Container(\n                  width: 36,\n                  height: 36,\n                  decoration: BoxDecoration(\n                    color: ColorScheme.of(context).secondaryContainer,\n                    borderRadius: BorderRadius.circular(36),\n                  ),\n                  clipBehavior: Clip.antiAlias,\n                  child: AnimatedImage(\n                    fit: BoxFit.cover,\n                    width: 36,\n                    height: 36,\n                    filterQuality: FilterQuality.medium,\n                    image: CachedImageProvider(widget.novel.author.avatar),\n                  ),\n                ),\n                const SizedBox(width: 12),\n                Expanded(\n                  child: Column(\n                    crossAxisAlignment: CrossAxisAlignment.start,\n                    mainAxisAlignment: MainAxisAlignment.center,\n                    children: [\n                      Text(\n                        widget.novel.author.name,\n                        style: const TextStyle(\n                          fontSize: 14,\n                          fontWeight: FontWeight.bold,\n                        ),\n                        maxLines: 1,\n                        overflow: TextOverflow.ellipsis,\n                      ),\n                      Text(\n                        widget.novel.createDate.toString().substring(0, 10),\n                        style: TextStyle(\n                          fontSize: 12,\n                          color: ColorScheme.of(context).outline,\n                        ),\n                      ),\n                    ],\n                  ),\n                ),\n                const Icon(MdIcons.chevron_right)\n              ],\n            ),\n          ),\n        ),\n      ),\n    );\n  }\n\n  bool isAddingFavorite = false;\n\n  var favoriteFlyout = FlyoutController();\n\n  Widget buildActions() {\n    void favorite() async {\n      if (isAddingFavorite) return;\n      bool? public;\n      if (!widget.novel.isBookmarked) {\n        await favoriteFlyout.showFlyout(\n          navigatorKey: App.rootNavigatorKey.currentState!,\n          builder: (context) {\n            return MenuFlyout(\n              items: [\n                MenuFlyoutItem(\n                  text: Text(\"Public\".tl),\n                  onPressed: () {\n                    public = true;\n                  },\n                ),\n                MenuFlyoutItem(\n                  text: Text(\"Private\".tl),\n                  onPressed: () {\n                    public = false;\n                  },\n                ),\n              ],\n            );\n          },\n        );\n        if (public == null) {\n          return;\n        }\n      }\n      setState(() {\n        isAddingFavorite = true;\n      });\n      var res = widget.novel.isBookmarked\n          ? await Network().deleteFavoriteNovel(widget.novel.id.toString())\n          : await Network().favoriteNovel(widget.novel.id.toString(), public!);\n      if (res.error) {\n        if (mounted) {\n          context.showToast(message: res.errorMessage ?? \"Network Error\");\n        }\n      } else {\n        widget.novel.isBookmarked = !widget.novel.isBookmarked;\n      }\n      setState(() {\n        isAddingFavorite = false;\n      });\n    }\n\n    return LayoutBuilder(builder: (context, constraints) {\n      final width = constraints.maxWidth;\n      return Card(\n        margin: const EdgeInsets.only(top: 12),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          children: [\n            if (width < 560) buildAuthor().toAlign(Alignment.centerLeft),\n            if (width < 560) buildStats().toAlign(Alignment.centerLeft),\n            if (width >= 560)\n              ConstrainedBox(\n                constraints: const BoxConstraints(maxWidth: 1132),\n                child: Row(\n                  children: [\n                    Expanded(child: buildAuthor()),\n                    const SizedBox(width: 12),\n                    Expanded(child: buildStats()),\n                  ],\n                ),\n              ).toAlign(Alignment.centerLeft),\n            LayoutBuilder(\n              builder: (context, constrains) {\n                var width = constrains.maxWidth;\n                bool shouldFillSpace = width < 500;\n                return Row(\n                  children: [\n                    FilledButton(\n                        child: Row(\n                          children: [\n                            const Icon(MdIcons.menu_book_outlined, size: 18),\n                            const SizedBox(width: 12),\n                            Text(\"Read\".tl),\n                            const Spacer(),\n                            const Icon(MdIcons.chevron_right, size: 18)\n                                .paddingTop(2),\n                          ],\n                        )\n                            .fixWidth(shouldFillSpace\n                                ? width / 2 - 4 - kFluentButtonPadding\n                                : 220)\n                            .fixHeight(32),\n                        onPressed: () {\n                          context.to(() => NovelReadingPage(widget.novel));\n                        }),\n                    const SizedBox(width: 16),\n                    FlyoutTarget(\n                      controller: favoriteFlyout,\n                      child: Button(\n                        onPressed: favorite,\n                        child: Row(\n                          mainAxisAlignment: constrains.maxWidth > 420\n                              ? MainAxisAlignment.start\n                              : MainAxisAlignment.center,\n                          children: [\n                            if (isAddingFavorite)\n                              const SizedBox(\n                                width: 18,\n                                height: 18,\n                                child: ProgressRing(\n                                  strokeWidth: 2,\n                                ),\n                              )\n                            else if (widget.novel.isBookmarked)\n                              Icon(\n                                MdIcons.favorite,\n                                size: 18,\n                                color: ColorScheme.of(context).error,\n                              )\n                            else\n                              const Icon(MdIcons.favorite_outline, size: 18),\n                            if (constrains.maxWidth > 420)\n                              const SizedBox(width: 12),\n                            if (constrains.maxWidth > 420) Text(\"Favorite\".tl)\n                          ],\n                        )\n                            .fixWidth(shouldFillSpace\n                            ? width / 4 - 4 - kFluentButtonPadding\n                            : 64)\n                            .fixHeight(32),\n                      ),\n                    ),\n                    const SizedBox(width: 8),\n                    Button(\n                        child: Row(\n                          mainAxisAlignment: constrains.maxWidth > 420\n                              ? MainAxisAlignment.start\n                              : MainAxisAlignment.center,\n                          children: [\n                            const Icon(MdIcons.comment, size: 18),\n                            if (constrains.maxWidth > 420)\n                              const SizedBox(width: 12),\n                            if (constrains.maxWidth > 420) Text(\"Comments\".tl)\n                          ],\n                        )\n                            .fixWidth(shouldFillSpace\n                                ? width / 4 - 4 - kFluentButtonPadding\n                                : 64)\n                            .fixHeight(32),\n                        onPressed: () {\n                          CommentsPage.show(context, widget.novel.id.toString(),\n                              isNovel: true);\n                        }),\n                  ],\n                );\n              },\n            ).paddingHorizontal(2),\n            SelectableText(\n              \"ID: ${widget.novel.id}\",\n              style: TextStyle(\n                  fontSize: 13, color: ColorScheme.of(context).outline),\n            ).paddingTop(8).paddingLeft(2),\n          ],\n        ),\n      );\n    });\n  }\n\n  Widget buildDescription() {\n    return Card(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(\n            \"Description\".tl,\n            style: const TextStyle(\n              fontSize: 18,\n              fontWeight: FontWeight.bold,\n            ),\n          ),\n          const SizedBox(height: 8),\n          SelectableText.rich(\n              TextSpan(children: buildDescriptionText().toList())),\n          const SizedBox(height: 12),\n          SizedBox(\n            width: double.infinity,\n            child: Wrap(\n              crossAxisAlignment: WrapCrossAlignment.start,\n              children: [\n                for (final tag in widget.novel.tags)\n                  MouseRegion(\n                    cursor: SystemMouseCursors.click,\n                    child: GestureDetector(\n                      onTap: () {\n                        context.to(() => SearchNovelResultPage(tag.name));\n                      },\n                      child: Container(\n                        margin: const EdgeInsets.only(right: 8, bottom: 6),\n                        padding: const EdgeInsets.symmetric(\n                            horizontal: 10, vertical: 6),\n                        decoration: BoxDecoration(\n                          color: ColorScheme.of(context).primaryContainer,\n                          borderRadius: BorderRadius.circular(4),\n                        ),\n                        child: Text(\n                          tag.name,\n                          style: const TextStyle(fontSize: 12),\n                        ),\n                      ),\n                    ),\n                  ),\n              ],\n            ),\n          ),\n          const SizedBox(height: 12),\n          Button(\n              child: Row(\n                children: [\n                  const Icon(MdIcons.bookmark_outline, size: 18),\n                  const SizedBox(width: 12),\n                  Text(\"Related\".tl)\n                ],\n              ).fixWidth(64).fixHeight(32),\n              onPressed: () {\n                context\n                    .to(() => _RelatedNovelsPage(widget.novel.id.toString()));\n              }),\n        ],\n      ),\n    ).paddingTop(12);\n  }\n\n  Iterable<TextSpan> buildDescriptionText() sync* {\n    var text = widget.novel.caption;\n    text = text.replaceAll(\"<br />\", \"\\n\");\n    text = text.replaceAll('\\n\\n', '\\n');\n    var labels = Queue<String>();\n    var buffer = StringBuffer();\n    var style = const TextStyle();\n    String? link;\n    Map<String, String> attributes = {};\n    for (int i = 0; i < text.length; i++) {\n      if (text[i] == '<' && text[i + 1] != '/') {\n        var label =\n            text.substring(i + 1, text.indexOf('>', i)).split(' ').first;\n        labels.addLast(label);\n        for (var part\n            in text.substring(i + 1, text.indexOf('>', i)).split(' ')) {\n          var kv = part.split('=');\n          if (kv.length >= 2) {\n            attributes[kv[0]] =\n                kv.join('=').substring(kv[0].length + 2).replaceAll('\"', '');\n          }\n        }\n        i = text.indexOf('>', i);\n      } else if (text[i] == '<' && text[i + 1] == '/') {\n        var label = text.substring(i + 2, text.indexOf('>', i));\n        if (label == labels.last) {\n          switch (label) {\n            case \"strong\":\n              style = style.copyWith(fontWeight: FontWeight.bold);\n            case \"a\":\n              style = style.copyWith(color: ColorScheme.of(context).primary);\n              link = attributes[\"href\"];\n          }\n          labels.removeLast();\n        }\n        i = text.indexOf('>', i);\n      } else {\n        buffer.write(text[i]);\n      }\n\n      if (i + 1 >= text.length ||\n          (labels.isEmpty &&\n              (text[i + 1] == '<' || (i != 0 && text[i - 1] == '>')))) {\n        var content = buffer.toString();\n        var url = link;\n        yield TextSpan(\n            text: content,\n            style: style,\n            recognizer: url != null\n                ? (TapGestureRecognizer()\n                  ..onTap = () {\n                    if (!handleLink(Uri.parse(url))) {\n                      launchUrlString(url);\n                    }\n                  })\n                : null);\n        buffer.clear();\n        link = null;\n        attributes.clear();\n        style = const TextStyle();\n      }\n    }\n  }\n}\n\nclass NovelSeriesWidget extends StatefulWidget {\n  const NovelSeriesWidget(this.seriesId, this.title, {super.key});\n\n  final int seriesId;\n\n  final String title;\n\n  @override\n  State<NovelSeriesWidget> createState() => _NovelSeriesWidgetState();\n}\n\nclass _NovelSeriesWidgetState\n    extends MultiPageLoadingState<NovelSeriesWidget, Novel> {\n  @override\n  Widget? buildFrame(BuildContext context, Widget child) {\n    return DecoratedSliver(\n      decoration: BoxDecoration(\n          color: FluentTheme.of(context).cardColor,\n          borderRadius: BorderRadius.circular(4),\n          border: Border.all(\n            color: ColorScheme.of(context).outlineVariant.toOpacity(0.6),\n            width: 0.5,\n          )),\n      sliver: SliverMainAxisGroup(slivers: [\n        SliverToBoxAdapter(\n          child: Text(widget.title.trim(),\n              style: const TextStyle(\n                fontSize: 18,\n                fontWeight: FontWeight.bold,\n              )).paddingTop(16).paddingLeft(12).paddingRight(12),\n        ),\n        const SliverPadding(padding: EdgeInsets.only(top: 8)),\n        child\n      ]),\n    ).sliverPadding(const EdgeInsets.only(top: 16));\n  }\n\n  @override\n  Widget buildLoading(BuildContext context) {\n    return SliverToBoxAdapter(\n      child: const Center(\n        child: ProgressRing(),\n      ).fixHeight(124),\n    );\n  }\n\n  @override\n  Widget buildError(BuildContext context, String error) {\n    return SliverToBoxAdapter(\n      child: Center(\n        child: Text(error),\n      ).fixHeight(124),\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, final List<Novel> data) {\n    return SliverGridViewWithFixedItemHeight(\n      itemHeight: 164,\n      minCrossAxisExtent: 400,\n      delegate: SliverChildBuilderDelegate(\n        (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return NovelWidget(data[index]);\n        },\n        childCount: data.length,\n      ),\n    ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8));\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res =\n        await Network().getNovelSeries(widget.seriesId.toString(), nextUrl);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass NovelPageWithId extends StatefulWidget {\n  const NovelPageWithId(this.id, {super.key});\n\n  final String id;\n\n  @override\n  State<NovelPageWithId> createState() => _NovelPageWithIdState();\n}\n\nclass _NovelPageWithIdState extends LoadingState<NovelPageWithId, Novel> {\n  @override\n  Future<Res<Novel>> loadData() async {\n    return Network().getNovelDetail(widget.id);\n  }\n\n  @override\n  Widget buildContent(BuildContext context, Novel data) {\n    return NovelPage(data);\n  }\n}\n\nclass _RelatedNovelsPage extends StatefulWidget {\n  const _RelatedNovelsPage(this.id);\n\n  final String id;\n\n  @override\n  State<_RelatedNovelsPage> createState() => __RelatedNovelsPageState();\n}\n\nclass __RelatedNovelsPageState\n    extends LoadingState<_RelatedNovelsPage, List<Novel>> {\n  @override\n  Widget buildContent(BuildContext context, List<Novel> data) {\n    return Column(\n      children: [\n        TitleBar(title: \"Related Novels\".tl),\n        Expanded(\n            child: GridViewWithFixedItemHeight(\n          itemHeight: 164,\n          itemCount: data.length,\n          minCrossAxisExtent: 400,\n          builder: (context, index) {\n            return NovelWidget(data[index]);\n          },\n        )),\n      ],\n    );\n  }\n\n  @override\n  Future<Res<List<Novel>>> loadData() async {\n    return Network().relatedNovels(widget.id);\n  }\n}\n"
  },
  {
    "path": "lib/pages/novel_ranking_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/grid.dart';\n\nclass NovelRankingPage extends StatefulWidget {\n  const NovelRankingPage({super.key});\n\n  @override\n  State<NovelRankingPage> createState() => _NovelRankingPageState();\n}\n\nclass _NovelRankingPageState extends State<NovelRankingPage> {\n  String type = \"day\";\n\n  /// mode: day, day_male, day_female, week_rookie, week, week_ai\n  static const types = {\n    \"day\": \"Daily\",\n    \"week\": \"Weekly\",\n    \"day_male\": \"For male\",\n    \"day_female\": \"For female\",\n    \"week_rookie\": \"Rookies\",\n  };\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      padding: EdgeInsets.zero,\n      content: Column(\n        children: [\n          buildHeader(),\n          Expanded(\n            child: _OneRankingPage(type, key: Key(type),),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader() {\n    return TitleBar(\n      title: \"Ranking\".tl,\n      action: DropDownButton(\n            title: Text(types[type]!.tl),\n            items: types.entries.map((e) => MenuFlyoutItem(\n              text: Text(e.value.tl),\n              onPressed: () {\n                setState(() {\n                  type = e.key;\n                });\n              },\n            )).toList(),\n          ),\n    );\n  }\n}\n\nclass _OneRankingPage extends StatefulWidget {\n  const _OneRankingPage(this.type, {super.key});\n\n  final String type;\n\n  @override\n  State<_OneRankingPage> createState() => _OneRankingPageState();\n}\n\nclass _OneRankingPageState extends MultiPageLoadingState<_OneRankingPage, Novel> {\n  @override\n  Widget buildContent(BuildContext context, final List<Novel> data) {\n    return GridViewWithFixedItemHeight(\n            itemCount: data.length,\n            itemHeight: 164,\n            minCrossAxisExtent: 400,\n            builder: (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return NovelWidget(data[index]);\n            },\n          ).paddingHorizontal(8);\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(page) async{\n    if(nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().getNovelRanking(widget.type, null);\n    if(!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}"
  },
  {
    "path": "lib/pages/novel_reading_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/animated_image.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/foundation/log.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/network/translator.dart';\nimport 'package:pixes/pages/image_page.dart';\nimport 'package:pixes/pages/main_page.dart';\nimport 'package:pixes/utils/ext.dart';\nimport 'package:pixes/utils/translation.dart';\n\nclass NovelReadingPage extends StatefulWidget {\n  const NovelReadingPage(this.novel, {super.key});\n\n  final Novel novel;\n\n  @override\n  State<NovelReadingPage> createState() => _NovelReadingPageState();\n}\n\nclass _NovelReadingPageState extends LoadingState<NovelReadingPage, String> {\n  TitleBarAction? action;\n\n  bool isShowingSettings = false;\n\n  String? translatedContent;\n\n  @override\n  void initState() {\n    action = TitleBarAction(MdIcons.tune, \"Settings\".tl, () {\n      if (!isShowingSettings) {\n        _NovelReadingSettings.show(\n          context,\n          () {\n            setState(() {});\n          },\n          TranslationController(\n            content: data!,\n            isTranslated: translatedContent != null,\n            onTranslated: (s) {\n              setState(() {\n                translatedContent = s;\n              });\n            },\n            revert: () {\n              setState(() {\n                translatedContent = null;\n              });\n            },\n          ),\n        ).then(\n          (value) {\n            isShowingSettings = false;\n          },\n        );\n        isShowingSettings = true;\n      } else {\n        Navigator.of(context).pop();\n      }\n    });\n    Future.delayed(const Duration(milliseconds: 200), () {\n      StateController.find<TitleBarController>().addAction(action!);\n    });\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    Future.delayed(const Duration(milliseconds: 200), () {\n      StateController.find<TitleBarController>().removeAction(action!);\n    });\n    super.dispose();\n  }\n\n  @override\n  Widget buildContent(BuildContext context, String data) {\n    var content = buildList(context).toList();\n    return ScaffoldPage(\n      padding: EdgeInsets.zero,\n      content: SelectionArea(\n          child: DefaultTextStyle.merge(\n        style: const TextStyle(fontSize: 16.0, height: 1.6),\n        child: ListView.builder(\n          padding: const EdgeInsets.all(16.0),\n          itemBuilder: (context, index) {\n            return content[index];\n          },\n          itemCount: content.length,\n        ),\n      )),\n    );\n  }\n\n  @override\n  Future<Res<String>> loadData() {\n    return Network().getNovelContent(widget.novel.id.toString());\n  }\n\n  Iterable<Widget> buildList(BuildContext context) sync* {\n    double fontSizeAdd = appdata.settings[\"readingFontSize\"] - 16.0;\n    double fontHeight = appdata.settings[\"readingLineHeight\"];\n\n    yield Text(widget.novel.title,\n        style: TextStyle(\n            fontSize: 24.0 + fontSizeAdd, fontWeight: FontWeight.bold));\n    yield const SizedBox(height: 12.0);\n    yield const Divider(\n      style: DividerThemeData(horizontalMargin: EdgeInsets.all(0)),\n    );\n    yield const SizedBox(height: 12.0);\n\n    var novelContent = (translatedContent ?? data!).split('\\n');\n    for (var content in novelContent) {\n      if (content.isEmpty) continue;\n      if (content.startsWith('[uploadedimage:')) {\n        var imageId = content.nums;\n        yield GestureDetector(\n          onTap: () {\n            ImagePage.show([\"novel:${widget.novel.id.toString()}/$imageId\"]);\n          },\n          child: SizedBox(\n            height: 300,\n            width: double.infinity,\n            child: AnimatedImage(\n              image:\n                  CachedNovelImageProvider(widget.novel.id.toString(), imageId),\n              filterQuality: FilterQuality.medium,\n              fit: BoxFit.contain,\n              height: 300,\n              width: double.infinity,\n            ),\n          ),\n        );\n      } else if (content.startsWith('[chapter:')) {\n        var title = content.replaceLast(']', '').split(':')[1];\n        yield Text(title,\n                style: TextStyle(\n                    fontSize: 20.0 + fontSizeAdd,\n                    fontWeight: FontWeight.bold,\n                    height: fontHeight))\n            .paddingBottom(8);\n      } else {\n        yield Text(content,\n                style:\n                    TextStyle(fontSize: 16.0 + fontSizeAdd, height: fontHeight))\n            .paddingBottom(appdata.settings[\"readingParagraphSpacing\"]);\n      }\n    }\n  }\n}\n\nclass TranslationController {\n  final String content;\n\n  final bool isTranslated;\n\n  final void Function(String translated) onTranslated;\n\n  final void Function() revert;\n\n  const TranslationController({\n    required this.content,\n    required this.isTranslated,\n    required this.onTranslated,\n    required this.revert,\n  });\n}\n\nclass _NovelReadingSettings extends StatefulWidget {\n  const _NovelReadingSettings(this.callback, this.controller);\n\n  final void Function() callback;\n\n  final TranslationController controller;\n\n  static Future show(\n    BuildContext context,\n    void Function() callback,\n    TranslationController controller,\n  ) {\n    return Navigator.of(context).push(\n      SideBarRoute(_NovelReadingSettings(callback, controller)),\n    );\n  }\n\n  @override\n  State<_NovelReadingSettings> createState() => __NovelReadingSettingsState();\n}\n\nclass __NovelReadingSettingsState extends State<_NovelReadingSettings> {\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        children: [\n          TitleBar(title: \"Reading Settings\".tl),\n          const SizedBox(height: 8),\n          Card(\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(\"Font Size\".tl),\n              subtitle: Slider(\n                value: appdata.settings[\"readingFontSize\"],\n                onChanged: (value) {\n                  setState(() {\n                    appdata.settings[\"readingFontSize\"] = value;\n                  });\n                  appdata.writeSettings();\n                  widget.callback();\n                },\n                min: 12.0,\n                max: 24.0,\n                divisions: 12,\n                label: appdata.settings[\"readingFontSize\"].toString(),\n              ),\n              trailing: Text(appdata.settings[\"readingFontSize\"].toString()),\n            ),\n          ).paddingHorizontal(8).paddingBottom(8),\n          Card(\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(\"Line Height\".tl),\n              subtitle: Slider(\n                value: appdata.settings[\"readingLineHeight\"],\n                onChanged: (value) {\n                  setState(() {\n                    appdata.settings[\"readingLineHeight\"] = value;\n                  });\n                  appdata.writeSettings();\n                  widget.callback();\n                },\n                min: 1.0,\n                max: 2.0,\n                divisions: 10,\n                label: appdata.settings[\"readingLineHeight\"].toString(),\n              ),\n              trailing: Text(appdata.settings[\"readingLineHeight\"].toString()),\n            ),\n          ).paddingHorizontal(8).paddingBottom(8),\n          Card(\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(\"Paragraph Spacing\".tl),\n              subtitle: Slider(\n                value: appdata.settings[\"readingParagraphSpacing\"],\n                onChanged: (value) {\n                  setState(() {\n                    appdata.settings[\"readingParagraphSpacing\"] = value;\n                  });\n                  appdata.writeSettings();\n                  widget.callback();\n                },\n                min: 0.0,\n                max: 16.0,\n                divisions: 8,\n                label: appdata.settings[\"readingParagraphSpacing\"].toString(),\n              ),\n              trailing:\n                  Text(appdata.settings[\"readingParagraphSpacing\"].toString()),\n            ),\n          ).paddingHorizontal(8).paddingBottom(8),\n          // 深色模式\n          Card(\n            margin: const EdgeInsets.symmetric(horizontal: 8),\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(\"Theme\".tl),\n              trailing: DropDownButton(\n                  title: Text(appdata.settings[\"theme\"] ?? \"System\".tl),\n                  items: [\n                    MenuFlyoutItem(\n                        text: Text(\"System\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"System\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: Text(\"light\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"Light\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: Text(\"dark\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"Dark\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                  ]),\n            ),\n          ).paddingBottom(8),\n          Card(\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(\"Translate Novel\".tl),\n              trailing: widget.controller.isTranslated\n                  ? Button(\n                      onPressed: () {\n                        widget.controller.revert();\n                        context.pop();\n                      },\n                      child: Text(\"Revert\".tl),\n                    )\n                  : Button(\n                      onPressed: translate,\n                      child: isTranslating\n                          ? const SizedBox(\n                              width: 42,\n                              height: 18,\n                              child: Center(\n                                child: SizedBox.square(\n                                  dimension: 18,\n                                  child: ProgressRing(\n                                    strokeWidth: 2,\n                                  ),\n                                ),\n                              ),\n                            )\n                          : Text(\"Translate\".tl),\n                    ),\n            ),\n          ).paddingHorizontal(8).paddingBottom(8),\n        ],\n      ),\n    );\n  }\n\n  bool isTranslating = false;\n\n  void translate() async {\n    setState(() {\n      isTranslating = true;\n    });\n    try {\n      var translated = await Translator.instance\n          .translate(widget.controller.content, \"zh-CN\");\n      widget.controller.onTranslated(translated);\n      if (mounted) {\n        context.pop();\n      }\n    } catch (e) {\n      setState(() {\n        isTranslating = false;\n      });\n      if (mounted) {\n        context.showToast(message: \"Failed to translate\".tl);\n      }\n      Log.error(\"Translate\", e.toString());\n    }\n  }\n}\n"
  },
  {
    "path": "lib/pages/novel_recommendation_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nclass NovelRecommendationPage extends StatefulWidget {\n  const NovelRecommendationPage({super.key});\n\n  @override\n  State<NovelRecommendationPage> createState() =>\n      _NovelRecommendationPageState();\n}\n\nclass _NovelRecommendationPageState\n    extends MultiPageLoadingState<NovelRecommendationPage, Novel> {\n  @override\n  Widget buildContent(BuildContext context, List<Novel> data) {\n    return Column(\n      children: [\n        TitleBar(title: \"Recommendation\".tl),\n        Expanded(\n          child: GridViewWithFixedItemHeight(\n            itemCount: data.length,\n            itemHeight: 164,\n            minCrossAxisExtent: 400,\n            builder: (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return NovelWidget(data[index]);\n            },\n          ).paddingHorizontal(8),\n        )\n      ],\n    );\n  }\n\n  @override\n  Future<Res<List<Novel>>> loadData(int page) {\n    return Network().getRecommendNovels();\n  }\n}\n"
  },
  {
    "path": "lib/pages/ranking.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/batch_download.dart';\nimport '../components/illust_widget.dart';\nimport '../components/loading.dart';\nimport '../components/title_bar.dart';\nimport '../network/network.dart';\nimport 'illust_page.dart';\n\nclass RankingPage extends StatefulWidget {\n  const RankingPage({super.key});\n\n  @override\n  State<RankingPage> createState() => _RankingPageState();\n}\n\nclass _RankingPageState extends State<RankingPage> {\n  String type = \"day\";\n\n  /// mode: day, week, month, day_male, day_female, week_original, week_rookie, day_manga, week_manga, month_manga, day_r18_manga, day_r18\n  static const types = {\n    \"day\": \"Daily\",\n    \"week\": \"Weekly\",\n    \"month\": \"Monthly\",\n    \"day_male\": \"For male\",\n    \"day_female\": \"For female\",\n    \"week_original\": \"Originals\",\n    \"week_rookie\": \"Rookies\",\n    \"day_manga\": \"Daily Manga\",\n    \"week_manga\": \"Weekly Manga\",\n    \"month_manga\": \"Monthly Manga\",\n    \"day_r18_manga\": \"R18\",\n  };\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      padding: EdgeInsets.zero,\n      content: Column(\n        children: [\n          buildHeader(),\n          Expanded(\n            child: _OneRankingPage(type, key: Key(type),),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader() {\n    return TitleBar(\n      title: \"Ranking\".tl,\n      action: Row(\n        children: [\n          BatchDownloadButton(request: () => Network().getRanking(type)),\n          const SizedBox(width: 8,),\n          DropDownButton(\n            title: Text(types[type]!.tl),\n            items: types.entries.map((e) => MenuFlyoutItem(\n              text: Text(e.value.tl),\n              onPressed: () {\n                setState(() {\n                  type = e.key;\n                });\n              },\n            )).toList(),\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass _OneRankingPage extends StatefulWidget {\n  const _OneRankingPage(this.type, {super.key});\n\n  final String type;\n\n  @override\n  State<_OneRankingPage> createState() => _OneRankingPageState();\n}\n\nclass _OneRankingPageState extends MultiPageLoadingState<_OneRankingPage, Illust> {\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    checkIllusts(data);\n    return LayoutBuilder(builder: (context, constrains){\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8)\n            + EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if(index == data.length - 1){\n            nextPage();\n          }\n          return IllustWidget(data[index], onTap: () {\n            context.to(() => IllustGalleryPage(\n                illusts: data,\n                initialPage: index,\n                nextUrl: nextUrl\n            ));\n          });\n        },\n      );\n    });\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async{\n    if(nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().getRanking(widget.type, nextUrl);\n    if(!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\n\n"
  },
  {
    "path": "lib/pages/recommendation_page.dart",
    "content": "import 'package:flutter/widgets.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/components/illust_widget.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/grid.dart';\nimport '../components/segmented_button.dart';\nimport '../components/user_preview.dart';\n\nclass RecommendationPage extends StatefulWidget {\n  const RecommendationPage({super.key});\n\n  @override\n  State<RecommendationPage> createState() => _RecommendationPageState();\n}\n\nclass _RecommendationPageState extends State<RecommendationPage> {\n  var type = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        buildTab(),\n        Expanded(\n          child: type != 2\n              ? _RecommendationArtworksPage(\n                  type,\n                  key: Key(type.toString()),\n                )\n              : const _RecommendationUsersPage(),\n        )\n      ],\n    );\n  }\n\n  Widget buildTab() {\n    return TitleBar(\n      title: \"Explore\".tl,\n      action: SegmentedButton<int>(\n        options: [\n          SegmentedButtonOption(0, \"Illustrations\".tl),\n          SegmentedButtonOption(1, \"Mangas\".tl),\n          SegmentedButtonOption(2, \"Users\".tl),\n        ],\n        onPressed: (key) {\n          if (key != type) {\n            setState(() {\n              type = key;\n            });\n          }\n        },\n        value: type,\n      ),\n    );\n  }\n}\n\nclass _RecommendationArtworksPage extends StatefulWidget {\n  const _RecommendationArtworksPage(this.type, {super.key});\n\n  final int type;\n\n  @override\n  State<_RecommendationArtworksPage> createState() =>\n      _RecommendationArtworksPageState();\n}\n\nclass _RecommendationArtworksPageState\n    extends MultiPageLoadingState<_RecommendationArtworksPage, Illust> {\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    checkIllusts(data);\n    return LayoutBuilder(builder: (context, constrains) {\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8) +\n            EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return IllustWidget(\n            data[index],\n            onTap: () {\n              context.to(() => IllustGalleryPage(\n                    illusts: data,\n                    initialPage: index,\n                    nextUrl: Network.recommendationUrl,\n                  ));\n            },\n          );\n        },\n      );\n    });\n  }\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) {\n    return widget.type == 0\n        ? Network().getRecommendedIllusts()\n        : Network().getRecommendedMangas();\n  }\n}\n\nclass _RecommendationUsersPage extends StatefulWidget {\n  const _RecommendationUsersPage();\n\n  @override\n  State<_RecommendationUsersPage> createState() =>\n      _RecommendationUsersPageState();\n}\n\nclass _RecommendationUsersPageState\n    extends MultiPageLoadingState<_RecommendationUsersPage, UserPreview> {\n  @override\n  Widget buildContent(BuildContext context, List<UserPreview> data) {\n    return CustomScrollView(\n      slivers: [\n        SliverGridViewWithFixedItemHeight(\n          delegate: SliverChildBuilderDelegate((context, index) {\n            if (index == data.length - 1) {\n              nextPage();\n            }\n            return UserPreviewWidget(data[index]);\n          }, childCount: data.length),\n          minCrossAxisExtent: 440,\n          itemHeight: 136,\n        ).sliverPaddingHorizontal(8)\n      ],\n    );\n  }\n\n  @override\n  Future<Res<List<UserPreview>>> loadData(page) async {\n    var res = await Network().getRecommendationUsers();\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/related_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/components/illust_widget.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/utils/translation.dart';\n\nclass RelatedIllustsPage extends StatefulWidget {\n  const RelatedIllustsPage(this.id, {super.key});\n\n  final String id;\n\n  @override\n  State<RelatedIllustsPage> createState() => _RelatedIllustsPageState();\n}\n\nclass _RelatedIllustsPageState\n    extends MultiPageLoadingState<RelatedIllustsPage, Illust> {\n  @override\n  Widget? buildFrame(BuildContext context, Widget child) {\n    return Column(\n      children: [\n        TitleBar(title: \"Related artworks\".tl),\n        Expanded(\n          child: child,\n        )\n      ],\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    return LayoutBuilder(builder: (context, constrains) {\n      return MasonryGridView.builder(\n        padding: const EdgeInsets.symmetric(horizontal: 8) +\n            EdgeInsets.only(bottom: context.padding.bottom),\n        gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n          maxCrossAxisExtent: 240,\n        ),\n        itemCount: data.length,\n        itemBuilder: (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return IllustWidget(data[index]);\n        },\n      );\n    });\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = nextUrl == null\n        ? await Network().relatedIllusts(widget.id)\n        : await Network().getIllustsWithNextUrl(nextUrl!);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n"
  },
  {
    "path": "lib/pages/search_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/components/search_field.dart';\nimport 'package:pixes/components/user_preview.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/pages/novel_page.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/app_links.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/debounce.dart';\nimport 'package:pixes/utils/ext.dart';\nimport 'package:pixes/utils/translation.dart';\n\nimport '../components/animated_image.dart';\nimport '../components/grid.dart';\nimport '../components/illust_widget.dart';\nimport '../components/md.dart';\nimport '../foundation/image_provider.dart';\n\nconst searchTypes = [\n  \"Search artwork\",\n  \"Search novel\",\n  \"Search user\",\n  \"Artwork ID\",\n  \"Artist ID\",\n  \"Novel ID\"\n];\n\nclass SearchPage extends StatefulWidget {\n  const SearchPage({super.key});\n\n  @override\n  State<SearchPage> createState() => _SearchPageState();\n}\n\nclass _SearchPageState extends State<SearchPage> {\n  int searchType = 0;\n\n  void search(String text) {\n    if (text.isURL && handleLink(Uri.parse(text))) {\n      return;\n    } else if (\"https://$text\".isURL &&\n        handleLink(Uri.parse(\"https://$text\"))) {\n      return;\n    }\n    switch (searchType) {\n      case 0:\n        context.to(() => SearchResultPage(text));\n      case 1:\n        context.to(() => SearchNovelResultPage(text));\n      case 2:\n        context.to(() => SearchUserResultPage(text));\n      case 3:\n        context.to(() => IllustPageWithId(text));\n      case 4:\n        context.to(() => UserInfoPage(text));\n      case 5:\n        context.to(() => NovelPageWithId(text));\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      padding: const EdgeInsets.only(top: 8),\n      content: Column(\n        children: [\n          _SearchBar(\n            searchType: searchType,\n            onTypeChanged: (type) {\n              setState(() {\n                searchType = type;\n              });\n            },\n            onSearch: (text) {\n              if (text.isEmpty) {\n                return;\n              }\n              search(text);\n            },\n          ),\n          const Expanded(\n            child: _TrendingTagsView(),\n          )\n        ],\n      ),\n    );\n  }\n}\n\nclass _TrendingTagsView extends StatefulWidget {\n  const _TrendingTagsView();\n\n  @override\n  State<_TrendingTagsView> createState() => _TrendingTagsViewState();\n}\n\nclass _TrendingTagsViewState\n    extends LoadingState<_TrendingTagsView, List<TrendingTag>> {\n  @override\n  Widget buildContent(BuildContext context, List<TrendingTag> data) {\n    return MasonryGridView.builder(\n      padding: const EdgeInsets.symmetric(horizontal: 8.0) +\n          EdgeInsets.only(bottom: context.padding.bottom),\n      gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n        maxCrossAxisExtent: 240,\n      ),\n      itemCount: data.length,\n      itemBuilder: (context, index) {\n        return buildItem(data[index]);\n      },\n    );\n  }\n\n  Widget buildItem(TrendingTag tag) {\n    final illust = tag.illust;\n\n    var text = tag.tag.name;\n    if (tag.tag.translatedName != null) {\n      text += \"/${tag.tag.translatedName}\";\n    }\n\n    return LayoutBuilder(builder: (context, constrains) {\n      final width = constrains.maxWidth;\n      final height = illust.height * width / illust.width;\n      return Container(\n        width: width,\n        height: height,\n        padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),\n        child: Card(\n          padding: EdgeInsets.zero,\n          margin: EdgeInsets.zero,\n          child: MouseRegion(\n            cursor: SystemMouseCursors.click,\n            child: GestureDetector(\n              onTap: () {\n                context.to(() => SearchResultPage(tag.tag.name));\n              },\n              child: Stack(\n                children: [\n                  Positioned.fill(\n                      child: ClipRRect(\n                    borderRadius: BorderRadius.circular(4.0),\n                    child: AnimatedImage(\n                      image: CachedImageProvider(illust.images.first.medium),\n                      fit: BoxFit.cover,\n                      width: width - 16.0,\n                      height: height - 16.0,\n                    ),\n                  )),\n                  Positioned(\n                    bottom: -2,\n                    left: 0,\n                    right: 0,\n                    child: Container(\n                      decoration: BoxDecoration(\n                          color: FluentTheme.of(context)\n                              .micaBackgroundColor\n                              .toOpacity(0.84),\n                          borderRadius: BorderRadius.circular(4)),\n                      child: Text(text)\n                          .paddingHorizontal(4)\n                          .paddingVertical(6)\n                          .paddingBottom(2),\n                    ),\n                  )\n                ],\n              ),\n            ),\n          ),\n        ),\n      );\n    });\n  }\n\n  @override\n  Future<Res<List<TrendingTag>>> loadData() {\n    return Network().getHotTags();\n  }\n}\n\nclass SearchSettings extends StatefulWidget {\n  const SearchSettings({this.onChanged, this.isNovel = false, super.key});\n\n  final void Function()? onChanged;\n\n  final bool isNovel;\n\n  @override\n  State<SearchSettings> createState() => _SearchSettingsState();\n}\n\nclass _SearchSettingsState extends State<SearchSettings> {\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        children: [\n          Padding(\n            padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),\n            child: Text(\n              \"Search Settings\".tl,\n              style: const TextStyle(fontSize: 18),\n            ),\n          ).toAlign(Alignment.centerLeft),\n          buildItem(\n              title: \"Match\".tl,\n              child: DropDownButton(\n                title: Text(appdata.searchOptions.matchType.toString().tl),\n                items: KeywordMatchType.values\n                    .map((e) => MenuFlyoutItem(\n                        text: Text(e.toString().tl),\n                        onPressed: () {\n                          if (appdata.searchOptions.matchType != e) {\n                            setState(() => appdata.searchOptions.matchType = e);\n                            widget.onChanged?.call();\n                          }\n                        }))\n                    .toList(),\n              )),\n          if (!widget.isNovel)\n            buildItem(\n                title: \"Favorite number\".tl,\n                child: DropDownButton(\n                  title:\n                      Text(appdata.searchOptions.favoriteNumber.toString().tl),\n                  items: FavoriteNumber.values\n                      .map((e) => MenuFlyoutItem(\n                          text: Text(e.toString().tl),\n                          onPressed: () {\n                            if (appdata.searchOptions.favoriteNumber != e) {\n                              setState(() =>\n                                  appdata.searchOptions.favoriteNumber = e);\n                              widget.onChanged?.call();\n                            }\n                          }))\n                      .toList(),\n                )),\n          buildItem(\n              title: \"Sort\".tl,\n              child: DropDownButton(\n                title: Text(appdata.searchOptions.sort.toString().tl),\n                items: SearchSort.values\n                    .map((e) => MenuFlyoutItem(\n                        text: Text(e.toString().tl),\n                        onPressed: () {\n                          if (appdata.searchOptions.sort != e) {\n                            setState(() => appdata.searchOptions.sort = e);\n                            widget.onChanged?.call();\n                          }\n                        }))\n                    .toList(),\n              )),\n          if (!widget.isNovel)\n            Card(\n                padding: EdgeInsets.zero,\n                margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n                child: SizedBox(\n                  width: double.infinity,\n                  child: Column(\n                    children: [\n                      Text(\n                        \"Start Time\".tl,\n                        style: const TextStyle(fontSize: 16),\n                      )\n                          .paddingVertical(8)\n                          .toAlign(Alignment.centerLeft)\n                          .paddingLeft(16),\n                      DatePicker(\n                        selected: appdata.searchOptions.startTime,\n                        onChanged: (t) {\n                          if (appdata.searchOptions.startTime != t) {\n                            setState(() => appdata.searchOptions.startTime = t);\n                            widget.onChanged?.call();\n                          }\n                        },\n                      ),\n                      const SizedBox(\n                        height: 8,\n                      )\n                    ],\n                  ),\n                )),\n          if (!widget.isNovel)\n            Card(\n                padding: EdgeInsets.zero,\n                margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n                child: SizedBox(\n                  width: double.infinity,\n                  child: Column(\n                    children: [\n                      Text(\n                        \"End Time\".tl,\n                        style: const TextStyle(fontSize: 16),\n                      )\n                          .paddingVertical(8)\n                          .toAlign(Alignment.centerLeft)\n                          .paddingLeft(16),\n                      DatePicker(\n                        selected: appdata.searchOptions.endTime,\n                        onChanged: (t) {\n                          if (appdata.searchOptions.endTime != t) {\n                            setState(() => appdata.searchOptions.endTime = t);\n                            widget.onChanged?.call();\n                          }\n                        },\n                      ),\n                      const SizedBox(\n                        height: 8,\n                      )\n                    ],\n                  ),\n                )),\n          if (!widget.isNovel)\n            buildItem(\n                title: \"Age limit\".tl,\n                child: DropDownButton(\n                  title: Text(appdata.searchOptions.ageLimit.toString().tl),\n                  items: AgeLimit.values\n                      .map((e) => MenuFlyoutItem(\n                          text: Text(e.toString().tl),\n                          onPressed: () {\n                            if (appdata.searchOptions.ageLimit != e) {\n                              setState(\n                                  () => appdata.searchOptions.ageLimit = e);\n                              widget.onChanged?.call();\n                            }\n                          }))\n                      .toList(),\n                )),\n          const SizedBox(height: 4),\n          Center(\n            child: Row(\n              mainAxisSize: MainAxisSize.min,\n              children: [\n                const Icon(FluentIcons.info, size: 16),\n                const SizedBox(\n                  width: 4,\n                ),\n                Text(\"Close the pane to apply the settings\".tl)\n              ],\n            ),\n          ),\n          SizedBox(\n            height: context.padding.bottom,\n          )\n        ],\n      ),\n    );\n  }\n\n  Widget buildItem({required String title, required Widget child}) {\n    return Card(\n      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n      padding: EdgeInsets.zero,\n      child: ListTile(\n        title: Text(title),\n        trailing: child,\n      ),\n    );\n  }\n}\n\nclass SearchResultPage extends StatefulWidget {\n  const SearchResultPage(this.keyword, {super.key});\n\n  final String keyword;\n\n  @override\n  State<SearchResultPage> createState() => _SearchResultPageState();\n}\n\nclass _SearchResultPageState\n    extends MultiPageLoadingState<SearchResultPage, Illust> {\n  late String keyword = widget.keyword;\n\n  late String oldKeyword = widget.keyword;\n\n  late final controller = TextEditingController(text: widget.keyword);\n\n  @override\n  void reset() {\n    nextUrl = null;\n    super.reset();\n  }\n\n  void search() {\n    if (keyword != oldKeyword) {\n      oldKeyword = keyword;\n      reset();\n    }\n  }\n\n  @override\n  Widget buildContent(BuildContext context, final List<Illust> data) {\n    checkIllusts(data);\n    return CustomScrollView(\n      slivers: [\n        buildSearchBar(),\n        SliverMasonryGrid(\n          gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n            maxCrossAxisExtent: 240,\n          ),\n          delegate: SliverChildBuilderDelegate(\n            (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return IllustWidget(\n                data[index],\n                onTap: () {\n                  context.to(() => IllustGalleryPage(\n                      illusts: data, initialPage: index, nextUrl: nextUrl));\n                },\n              );\n            },\n            childCount: data.length,\n          ),\n        ).sliverPaddingHorizontal(8),\n        SliverPadding(\n          padding: EdgeInsets.only(bottom: context.padding.bottom),\n        )\n      ],\n    );\n  }\n\n  Widget buildSearchBar() {\n    return SliverToBoxAdapter(\n      child: Center(\n        child: ConstrainedBox(\n          constraints: const BoxConstraints(maxWidth: 560),\n          child: SizedBox(\n            height: 42,\n            width: double.infinity,\n            child: LayoutBuilder(\n              builder: (context, constrains) {\n                return SizedBox(\n                  height: 42,\n                  width: constrains.maxWidth,\n                  child: Row(\n                    children: [\n                      Expanded(\n                        child: TextBox(\n                          controller: controller,\n                          placeholder: \"Search artworks\".tl,\n                          onChanged: (s) => keyword = s,\n                          onSubmitted: (s) => search(),\n                          foregroundDecoration: WidgetStatePropertyAll(\n                            BoxDecoration(\n                              border: Border.all(\n                                color: ColorScheme.of(context)\n                                    .outlineVariant\n                                    .toOpacity(0.6),\n                              ),\n                              borderRadius: BorderRadius.circular(4),\n                            ),\n                          ),\n                          suffix: MouseRegion(\n                            cursor: SystemMouseCursors.click,\n                            child: GestureDetector(\n                              onTap: search,\n                              child: const Icon(\n                                FluentIcons.search,\n                                size: 16,\n                              ).paddingHorizontal(12),\n                            ),\n                          ),\n                        ),\n                      ),\n                      const SizedBox(\n                        width: 4,\n                      ),\n                      Button(\n                        child: const SizedBox(\n                          height: 42,\n                          child: Center(\n                            child: Icon(FluentIcons.settings),\n                          ),\n                        ),\n                        onPressed: () async {\n                          bool isChanged = false;\n                          await Navigator.of(context)\n                              .push(SideBarRoute(SearchSettings(\n                            onChanged: () => isChanged = true,\n                          )));\n                          if (isChanged) {\n                            reset();\n                          }\n                        },\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n          ).paddingHorizontal(16),\n        ),\n      ),\n    ).sliverPadding(const EdgeInsets.only(top: 12));\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = nextUrl == null\n        ? await Network().search(keyword, appdata.searchOptions)\n        : await Network().getIllustsWithNextUrl(nextUrl!);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass SearchUserResultPage extends StatefulWidget {\n  const SearchUserResultPage(this.keyword, {super.key});\n\n  final String keyword;\n\n  @override\n  State<SearchUserResultPage> createState() => _SearchUserResultPageState();\n}\n\nclass _SearchUserResultPageState\n    extends MultiPageLoadingState<SearchUserResultPage, UserPreview> {\n  @override\n  Widget buildContent(BuildContext context, final List<UserPreview> data) {\n    return CustomScrollView(\n      slivers: [\n        SliverToBoxAdapter(\n          child: Text(\n            \"${\"Search\".tl}: ${widget.keyword}\",\n            style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),\n          ).paddingVertical(12).paddingHorizontal(16),\n        ),\n        SliverGridViewWithFixedItemHeight(\n          delegate: SliverChildBuilderDelegate((context, index) {\n            if (index == data.length - 1) {\n              nextPage();\n            }\n            return UserPreviewWidget(data[index]);\n          }, childCount: data.length),\n          minCrossAxisExtent: 440,\n          itemHeight: 136,\n        ).sliverPaddingHorizontal(8),\n        SliverPadding(\n          padding: EdgeInsets.only(bottom: context.padding.bottom),\n        )\n      ],\n    );\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<UserPreview>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = await Network().searchUsers(widget.keyword, nextUrl);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass SearchNovelResultPage extends StatefulWidget {\n  const SearchNovelResultPage(this.keyword, {super.key});\n\n  final String keyword;\n\n  @override\n  State<SearchNovelResultPage> createState() => _SearchNovelResultPageState();\n}\n\nclass _SearchNovelResultPageState\n    extends MultiPageLoadingState<SearchNovelResultPage, Novel> {\n  late String keyword = widget.keyword;\n\n  late String oldKeyword = widget.keyword;\n\n  late final controller = TextEditingController(text: widget.keyword);\n\n  void search() {\n    if (keyword != oldKeyword) {\n      oldKeyword = keyword;\n      reset();\n    }\n  }\n\n  @override\n  Widget buildContent(BuildContext context, final List<Novel> data) {\n    return CustomScrollView(\n      slivers: [\n        buildSearchBar(),\n        SliverGridViewWithFixedItemHeight(\n          itemHeight: 164,\n          minCrossAxisExtent: 400,\n          delegate: SliverChildBuilderDelegate(\n            (context, index) {\n              if (index == data.length - 1) {\n                nextPage();\n              }\n              return NovelWidget(data[index]);\n            },\n            childCount: data.length,\n          ),\n        ).sliverPaddingHorizontal(8),\n        SliverPadding(\n          padding: EdgeInsets.only(bottom: context.padding.bottom),\n        )\n      ],\n    );\n  }\n\n  Widget buildSearchBar() {\n    return SliverToBoxAdapter(\n      child: Center(\n        child: ConstrainedBox(\n          constraints: const BoxConstraints(maxWidth: 560),\n          child: SizedBox(\n            height: 42,\n            width: double.infinity,\n            child: LayoutBuilder(\n              builder: (context, constrains) {\n                return SizedBox(\n                  height: 42,\n                  width: constrains.maxWidth,\n                  child: Row(\n                    children: [\n                      Expanded(\n                        child: TextBox(\n                          controller: controller,\n                          placeholder: \"Search artworks\".tl,\n                          onChanged: (s) => keyword = s,\n                          onSubmitted: (s) => search(),\n                          foregroundDecoration: WidgetStatePropertyAll(\n                            BoxDecoration(\n                                border: Border.all(\n                                    color: ColorScheme.of(context)\n                                        .outlineVariant\n                                        .toOpacity(0.6)),\n                                borderRadius: BorderRadius.circular(4)),\n                          ),\n                          suffix: MouseRegion(\n                            cursor: SystemMouseCursors.click,\n                            child: GestureDetector(\n                              onTap: search,\n                              child: const Icon(\n                                FluentIcons.search,\n                                size: 16,\n                              ).paddingHorizontal(12),\n                            ),\n                          ),\n                        ),\n                      ),\n                      const SizedBox(\n                        width: 4,\n                      ),\n                      Button(\n                        child: const SizedBox(\n                          height: 42,\n                          child: Center(\n                            child: Icon(FluentIcons.settings),\n                          ),\n                        ),\n                        onPressed: () async {\n                          bool isChanged = false;\n                          await Navigator.of(context)\n                              .push(SideBarRoute(SearchSettings(\n                            onChanged: () => isChanged = true,\n                            isNovel: true,\n                          )));\n                          if (isChanged) {\n                            reset();\n                          }\n                        },\n                      )\n                    ],\n                  ),\n                );\n              },\n            ),\n          ).paddingHorizontal(16),\n        ),\n      ),\n    ).sliverPadding(const EdgeInsets.only(top: 12));\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = nextUrl == null\n        ? await Network().searchNovels(keyword, appdata.searchOptions)\n        : await Network().getNovelsWithNextUrl(nextUrl!);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass _SearchBar extends StatefulWidget {\n  const _SearchBar({\n    required this.searchType,\n    required this.onTypeChanged,\n    required this.onSearch,\n  });\n\n  final int searchType;\n\n  final void Function(int) onTypeChanged;\n\n  final void Function(String) onSearch;\n\n  @override\n  State<_SearchBar> createState() => _SearchBarState();\n}\n\nclass _SearchBarState extends State<_SearchBar> {\n  final optionController = FlyoutController();\n\n  final textController = TextEditingController();\n\n  var autoCompleteItems = <AutoCompleteItem>[];\n\n  var debouncer = Debounce(delay: const Duration(milliseconds: 300));\n\n  var autoCompleteKey = 0;\n\n  var isLoadingAutoCompleteItems = false;\n\n  Widget buildSearchOption(BuildContext context) {\n    return MenuFlyout(\n      items: List.generate(\n        searchTypes.length,\n        (index) => MenuFlyoutItem(\n          text: Text(searchTypes[index].tl),\n          onPressed: () => widget.onTypeChanged(index),\n        ),\n      ),\n    );\n  }\n\n  void onTextChanged(String text) {\n    if (widget.searchType == 3 ||\n        widget.searchType == 4 ||\n        widget.searchType == 5) {\n      return;\n    }\n\n    if (text.isEmpty) {\n      setState(() {\n        autoCompleteItems = [];\n        isLoadingAutoCompleteItems = false;\n      });\n      return;\n    }\n    setState(() {\n      isLoadingAutoCompleteItems = true;\n    });\n    debouncer.call(() async {\n      var key = ++autoCompleteKey;\n\n      var res = await Network().getAutoCompleteTags(text);\n      if (res.error) {\n        return;\n      }\n      var items = res.data.map((e) {\n        return AutoCompleteItem(\n          title: e.name,\n          subtitle: e.translatedName,\n          onTap: () {\n            textController.text = e.name;\n            widget.onSearch(e.name);\n          },\n        );\n      }).toList();\n\n      if (key != autoCompleteKey) {\n        return; // ignore old request\n      }\n\n      setState(() {\n        autoCompleteItems = items;\n        isLoadingAutoCompleteItems = false;\n      });\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return ConstrainedBox(\n      constraints: const BoxConstraints(maxWidth: 560),\n      child: SizedBox(\n        height: 42,\n        width: double.infinity,\n        child: LayoutBuilder(\n          builder: (context, constrains) {\n            return SizedBox(\n              height: 42,\n              width: constrains.maxWidth,\n              child: Row(\n                children: [\n                  Expanded(\n                    child: SearchField(\n                      enableAutoComplete: widget.searchType != 3 &&\n                          widget.searchType != 4 &&\n                          widget.searchType != 5,\n                      textEditingController: textController,\n                      autoCompleteNoResultsText: \"No results found\".tl,\n                      isLoadingAutoCompleteItems: isLoadingAutoCompleteItems,\n                      autoCompleteItems: autoCompleteItems,\n                      padding: const EdgeInsets.symmetric(horizontal: 12),\n                      placeholder:\n                          '${searchTypes[widget.searchType].tl} / ${\"Open link\".tl}',\n                      onChanged: onTextChanged,\n                      onSubmitted: widget.onSearch,\n                      foregroundDecoration: WidgetStatePropertyAll(\n                        BoxDecoration(\n                          border: Border.all(\n                            color: ColorScheme.of(context)\n                                .outlineVariant\n                                .toOpacity(0.6),\n                          ),\n                          borderRadius: BorderRadius.circular(4),\n                        ),\n                      ),\n                      trailing: MouseRegion(\n                        cursor: SystemMouseCursors.click,\n                        child: GestureDetector(\n                          onTap: () => widget.onSearch(textController.text),\n                          child: const Icon(\n                            FluentIcons.search,\n                            size: 16,\n                          ).paddingHorizontal(12),\n                        ),\n                      ),\n                    ),\n                  ),\n                  const SizedBox(width: 4),\n                  FlyoutTarget(\n                    controller: optionController,\n                    child: Button(\n                      child: const SizedBox(\n                        height: 42,\n                        child: Center(\n                          child: Icon(FluentIcons.chevron_down),\n                        ),\n                      ),\n                      onPressed: () {\n                        optionController.showFlyout(\n                          placementMode: FlyoutPlacementMode.bottomCenter,\n                          builder: buildSearchOption,\n                          barrierColor: Colors.transparent,\n                        );\n                      },\n                    ),\n                  ),\n                  const SizedBox(width: 4),\n                  Button(\n                    child: const SizedBox(\n                      height: 42,\n                      child: Center(\n                        child: Icon(FluentIcons.settings),\n                      ),\n                    ),\n                    onPressed: () {\n                      Navigator.of(context).push(SideBarRoute(SearchSettings(\n                        isNovel: widget.searchType == 1,\n                      )));\n                    },\n                  )\n                ],\n              ),\n            );\n          },\n        ),\n      ).paddingHorizontal(16),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/settings_page.dart",
    "content": "import 'dart:io';\n\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/services.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/keyboard.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/message.dart';\nimport 'package:pixes/components/page_route.dart';\nimport 'package:pixes/components/title_bar.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/pages/main_page.dart';\nimport 'package:pixes/utils/io.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\n\nimport 'logs.dart';\n\nclass SettingsPage extends StatefulWidget {\n  const SettingsPage({super.key});\n\n  @override\n  State<SettingsPage> createState() => _SettingsPageState();\n}\n\nclass _SettingsPageState extends State<SettingsPage> {\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      padding: EdgeInsets.zero,\n      content: CustomScrollView(\n        slivers: [\n          SliverTitleBar(title: \"Settings\".tl),\n          buildHeader(\"Account\".tl),\n          buildAccount(),\n          buildHeader(\"Browse\".tl),\n          buildBrowse(),\n          buildHeader(\"Download\".tl),\n          buildDownload(),\n          buildHeader(\"Appearance\".tl),\n          buildAppearance(),\n          buildHeader(\"About\".tl),\n          buildAbout(),\n          SliverPadding(\n              padding: EdgeInsets.only(bottom: context.padding.bottom)),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader(String text) {\n    return SliverToBoxAdapter(\n      child: Padding(\n        padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),\n        child: Text(text,\n            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),\n      ),\n    );\n  }\n\n  Widget buildItem({required String title, String? subtitle, Widget? action}) {\n    return Card(\n      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n      padding: EdgeInsets.zero,\n      child: ListTile(\n        title: Text(title),\n        subtitle: subtitle == null ? null : Text(subtitle),\n        trailing: action,\n      ),\n    );\n  }\n\n  Widget buildAccount() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildItem(\n            title: \"Logout\".tl,\n            action: Button(\n              onPressed: () {\n                showDialog<String>(\n                  context: App.rootNavigatorKey.currentContext!,\n                  builder: (context) => ContentDialog(\n                    title: Text('Logout'.tl),\n                    content: Text('Are you sure you want to logout?'.tl),\n                    actions: [\n                      Button(\n                        child: Text('Continue'.tl),\n                        onPressed: () {\n                          appdata.account = null;\n                          appdata.writeData();\n                          App.rootNavigatorKey.currentState!.pushAndRemoveUntil(\n                              AppPageRoute(\n                                  builder: (context) => const MainPage()),\n                              (route) => false);\n                        },\n                      ),\n                      FilledButton(\n                        child: Text('Cancel'.tl),\n                        onPressed: () => context.pop(),\n                      ),\n                    ],\n                  ),\n                );\n              },\n              child: Text(\"Continue\".tl).fixWidth(64),\n            ),\n          ),\n          buildItem(\n              title: \"Account Settings\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  launchUrlString(\"https://www.pixiv.net/setting_user.php\");\n                },\n              )),\n        ],\n      ),\n    );\n  }\n\n  Widget buildDownload() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildItem(\n            title: \"Download Path\".tl,\n            subtitle: appdata.settings[\"downloadPath\"],\n            action: App.isMacOS\n                ? _MacosDownloadPathSelectButton(onSelected: (path) {\n                    setState(() {\n                      appdata.settings[\"downloadPath\"] = path;\n                    });\n                    appdata.writeSettings();\n                  })\n                : Button(\n                    child: Text(\"Manage\".tl).fixWidth(64),\n                    onPressed: () {\n                      if (Platform.isIOS) {\n                        showToast(context, message: \"Unsupported platform\".tl);\n                        return;\n                      }\n                      context.to(() => _SetSingleFieldPage(\n                            \"Download Path\".tl,\n                            \"downloadPath\",\n                            check: (text) {\n                              if (!Directory(text).havePermission()) {\n                                return \"No permission\".tl;\n                              } else {\n                                return null;\n                              }\n                            },\n                          ));\n                    }),\n          ),\n          buildItem(\n            title: \"Subpath\".tl,\n            subtitle: appdata.settings[\"downloadSubPath\"],\n            action: Button(\n                child: Text(\"Manage\".tl).fixWidth(64),\n                onPressed: () {\n                  context.to(() => const _SetDownloadSubPathPage());\n                }),\n          ),\n          buildItem(\n            title: \"Max parallels\".tl,\n            action: SizedBox(\n              width: 64,\n              height: 32,\n              child: NumberBox<int>(\n                value: appdata.settings[\"maxParallels\"],\n                autofocus: false,\n                onChanged: (value) {\n                  appdata.settings[\"maxParallels\"] = value;\n                  appdata.writeSettings();\n                },\n                clearButton: false,\n                mode: SpinButtonPlacementMode.none,\n              ),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildAbout() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildItem(title: \"Version\", subtitle: App.version),\n          buildItem(\n              title: \"Check for updates on startup\".tl,\n              action: ToggleSwitch(\n                  checked: appdata.settings[\"checkUpdate\"],\n                  onChanged: (value) {\n                    setState(() {\n                      appdata.settings[\"checkUpdate\"] = value;\n                    });\n                    appdata.writeData();\n                  })),\n          buildItem(\n              title: \"Github\",\n              action: IconButton(\n                icon: const Icon(\n                  MdIcons.open_in_new,\n                  size: 18,\n                ),\n                onPressed: () =>\n                    launchUrlString(\"https://github.com/wgh136/pixes\"),\n              )),\n          buildItem(\n              title: \"Telegram\",\n              action: IconButton(\n                icon: const Icon(\n                  MdIcons.open_in_new,\n                  size: 18,\n                ),\n                onPressed: () => launchUrlString(\"https://t.me/venera_dev\"),\n              )),\n          buildItem(\n              title: \"Logs\",\n              action: IconButton(\n                  icon: const Icon(\n                    MdIcons.open_in_new,\n                    size: 18,\n                  ),\n                  onPressed: () => context.to(() => const LogsPage()))),\n        ],\n      ),\n    );\n  }\n\n  Widget buildBrowse() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildItem(\n              title: \"Initial Page\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  context.to(() => const _SetInitialPageWidget());\n                },\n              )),\n          buildItem(\n              title: \"Proxy\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  context.to(() => _SetSingleFieldPage(\n                        \"Http ${\"Proxy\".tl}\",\n                        \"proxy\",\n                      ));\n                },\n              )),\n          buildItem(\n              title: \"Block(Account)\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  launchUrlString(\"https://www.pixiv.net/setting_mute.php\");\n                },\n              )),\n          buildItem(\n              title: \"Block(Local)\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  context.to(() => const _BlockTagsPage());\n                },\n              )),\n          buildItem(\n              title: \"Shortcuts\".tl,\n              action: Button(\n                child: Text(\"Edit\".tl).fixWidth(64),\n                onPressed: () {\n                  context.to(() => const ShortcutsSettings());\n                },\n              )),\n          buildItem(\n              title: \"Display the original image on the details page\".tl,\n              action: ToggleSwitch(\n                  checked: appdata.settings['showOriginalImage'],\n                  onChanged: (value) {\n                    setState(() {\n                      appdata.settings['showOriginalImage'] = value;\n                    });\n                    appdata.writeData();\n                  })),\n          buildItem(\n              title: \"Emphasize artworks from following artists\".tl,\n              subtitle: \"The border of the artworks will be darker\".tl,\n              action: ToggleSwitch(\n                  checked:\n                      appdata.settings['emphasizeArtworksFromFollowingArtists'],\n                  onChanged: (value) {\n                    setState(() {\n                      appdata.settings[\n                          'emphasizeArtworksFromFollowingArtists'] = value;\n                    });\n                    appdata.writeData();\n                  })),\n        ],\n      ),\n    );\n  }\n\n  Widget buildAppearance() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildItem(\n              title: \"Theme\".tl,\n              action: DropDownButton(\n                  title: Text(appdata.settings[\"theme\"] ?? \"System\".tl),\n                  items: [\n                    MenuFlyoutItem(\n                        text: Text(\"System\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"System\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: Text(\"light\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"Light\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: Text(\"dark\".tl),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"theme\"] = \"Dark\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                  ])),\n          buildItem(\n              title: \"Language\".tl,\n              action: DropDownButton(\n                  title: Text(appdata.settings[\"language\"] ?? \"System\"),\n                  items: [\n                    MenuFlyoutItem(\n                        text: const Text(\"System\"),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"language\"] = \"System\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: const Text(\"English\"),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"language\"] = \"English\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: const Text(\"简体中文\"),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"language\"] = \"简体中文\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                    MenuFlyoutItem(\n                        text: const Text(\"繁體中文\"),\n                        onPressed: () {\n                          setState(() {\n                            appdata.settings[\"language\"] = \"繁體中文\";\n                          });\n                          appdata.writeData();\n                          StateController.findOrNull(tag: \"MyApp\")?.update();\n                        }),\n                  ])),\n        ],\n      ),\n    );\n  }\n}\n\nclass _SetSingleFieldPage extends StatefulWidget {\n  const _SetSingleFieldPage(this.title, this.field, {this.check});\n\n  final String title;\n\n  final String field;\n\n  final String? Function(String)? check;\n\n  @override\n  State<_SetSingleFieldPage> createState() => _SetSingleFieldPageState();\n}\n\nclass _SetSingleFieldPageState extends State<_SetSingleFieldPage> {\n  late final controller =\n      TextEditingController(text: appdata.settings[widget.field]);\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        TitleBar(title: widget.title),\n        TextBox(\n          controller: controller,\n        ).paddingHorizontal(16),\n        const SizedBox(\n          height: 8,\n        ),\n        Button(\n          child: Text(\"Confirm\".tl),\n          onPressed: () {\n            var text = controller.text;\n            var checkRes = widget.check?.call(text);\n            if (checkRes == null) {\n              appdata.settings[widget.field] = text;\n              appdata.writeData();\n              context.pop();\n            } else {\n              showToast(context, message: checkRes);\n            }\n          },\n        ).toAlign(Alignment.centerRight).paddingRight(16),\n      ],\n    );\n  }\n}\n\nclass _SetDownloadSubPathPage extends StatefulWidget {\n  const _SetDownloadSubPathPage();\n\n  @override\n  State<_SetDownloadSubPathPage> createState() =>\n      __SetDownloadSubPathPageState();\n}\n\nclass __SetDownloadSubPathPageState extends State<_SetDownloadSubPathPage> {\n  final controller =\n      TextEditingController(text: appdata.settings[\"downloadSubPath\"]);\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          TitleBar(title: \"Download subpath\".tl),\n          Text(\"Rule\".tl)\n              .padding(const EdgeInsets.symmetric(vertical: 8, horizontal: 16)),\n          TextBox(\n            controller: controller,\n          ).paddingHorizontal(16),\n          const SizedBox(\n            height: 8,\n          ),\n          Button(\n            child: Text(\"Confirm\".tl),\n            onPressed: () {\n              var text = controller.text;\n              if (check(text)) {\n                appdata.settings[\"downloadSubPath\"] = text;\n                appdata.writeData();\n                context.pop();\n              } else {\n                showToast(context, message: \"Invalid\".tl);\n              }\n            },\n          ).toAlign(Alignment.centerRight).paddingRight(16),\n          const SizedBox(\n            height: 16,\n          ),\n          SelectableText(_instruction).paddingHorizontal(16)\n        ],\n      ),\n    );\n  }\n\n  bool check(String text) {\n    if (text.startsWith('/') || text.startsWith('\\\\')) {\n      return true;\n    }\n    return false;\n  }\n\n  String get _instruction => \"\"\"\n${\"Edit the rule for where to save an image.\".tl}\n${\"Note: The rule should include the filename.\".tl}\n\n${\"Some keywords will be replaced by the following rule:\".tl}\n  \\${title} -> ${\"Title of the work\".tl}\n  \\${author} -> ${\"Name of the author\".tl}\n  \\${id} -> ${\"Artwork ID\".tl}\n  \\${index} -> ${\"Index of the image in the artwork\".tl}\n  \\${page} -> ${\"Replace with '-p\\${index}' if the work have more than one images, otherwise replace with blank.\".tl}\n  \\${ext} -> ${\"File extension\".tl}\n  \\${AI} -> ${\"Replace with 'AI' if the work was generated by AI, otherwise replace with blank\".tl}\n  \\${tag(*)} -> ${\"Replace with * if the work have tag *, otherwise replace with blank.\".tl}\n\n${\"Multiple path separators will be automatically replaced with a single\".tl}\n\"\"\";\n}\n\nclass _BlockTagsPage extends StatefulWidget {\n  const _BlockTagsPage();\n\n  @override\n  State<_BlockTagsPage> createState() => __BlockTagsPageState();\n}\n\nclass __BlockTagsPageState extends State<_BlockTagsPage> {\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      children: [\n        TitleBar(\n          title: \"Block\".tl,\n          action: FilledButton(\n            child: Text(\"Add\".tl),\n            onPressed: () {\n              var controller = TextEditingController();\n\n              void finish(BuildContext context) {\n                var text = controller.text;\n                if (text.isNotEmpty &&\n                    !(appdata.settings[\"blockTags\"] as List).contains(text)) {\n                  setState(() {\n                    appdata.settings[\"blockTags\"].add(text);\n                  });\n                  appdata.writeSettings();\n                }\n                context.pop();\n              }\n\n              showDialog(\n                  context: context,\n                  barrierDismissible: true,\n                  builder: (context) {\n                    return ContentDialog(\n                      title: Text(\"Add\".tl),\n                      content: SizedBox(\n                        width: 300,\n                        height: 32,\n                        child: TextBox(\n                          controller: controller,\n                          onSubmitted: (v) => finish(context),\n                        ),\n                      ),\n                      actions: [\n                        FilledButton(\n                            child: Text(\"Submit\".tl),\n                            onPressed: () {\n                              finish(context);\n                            })\n                      ],\n                    );\n                  });\n            },\n          ),\n        ),\n        Expanded(\n          child: ListView.builder(\n            itemCount: appdata.settings[\"blockTags\"].length,\n            itemBuilder: (context, index) {\n              return Card(\n                margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),\n                padding: EdgeInsets.zero,\n                child: ListTile(\n                  title: Text(appdata.settings[\"blockTags\"][index]),\n                  trailing: Button(\n                    child: Text(\"Delete\".tl),\n                    onPressed: () {\n                      setState(() {\n                        (appdata.settings[\"blockTags\"] as List).removeAt(index);\n                      });\n                      appdata.writeSettings();\n                    },\n                  ),\n                ),\n              );\n            },\n          ),\n        )\n      ],\n    );\n  }\n}\n\nclass ShortcutsSettings extends StatefulWidget {\n  const ShortcutsSettings({super.key});\n\n  @override\n  State<ShortcutsSettings> createState() => _ShortcutsSettingsState();\n}\n\nclass _ShortcutsSettingsState extends State<ShortcutsSettings> {\n  int listening = -1;\n\n  KeyEventListenerState? listener;\n\n  @override\n  void initState() {\n    listener = KeyEventListener.of(context);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    listener?.removeAll();\n    super.dispose();\n  }\n\n  final settings = <String>[\n    \"Page down\",\n    \"Page up\",\n    \"Next work\",\n    \"Previous work\",\n    \"Add to favorites\",\n    \"Download\",\n    \"Follow the artist\",\n    \"Show comments\",\n    \"Show original image\"\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return SingleChildScrollView(\n      child: Column(children: [\n        TitleBar(title: \"Shortcuts\".tl),\n        ...settings.map((e) => buildItem(e, settings.indexOf(e)))\n      ]),\n    );\n  }\n\n  Widget buildItem(String text, int index) {\n    var keyText = listening == index\n        ? \"Waiting...\"\n        : LogicalKeyboardKey(appdata.settings['shortcuts'][index]).keyLabel;\n    return Card(\n      padding: EdgeInsets.zero,\n      margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),\n      child: ListTile(\n        title: Text(text.tl),\n        trailing: Button(\n          child: Text(keyText),\n          onPressed: () {\n            if (listening != -1) {\n              listener?.removeAll();\n            }\n            setState(() {\n              listening = index;\n            });\n            listener?.addHandler((key) {\n              if (key == LogicalKeyboardKey.escape) return;\n              setState(() {\n                appdata.settings['shortcuts'][index] = key.keyId;\n                listening = -1;\n                appdata.writeData();\n              });\n              Future.microtask(() => listener?.removeAll());\n            });\n          },\n        ),\n      ),\n    );\n  }\n}\n\nclass _SetInitialPageWidget extends StatefulWidget {\n  const _SetInitialPageWidget();\n\n  @override\n  State<_SetInitialPageWidget> createState() => _SetInitialPageWidgetState();\n}\n\nclass _SetInitialPageWidgetState extends State<_SetInitialPageWidget> {\n  int index = appdata.settings[\"initialPage\"] ?? 4;\n\n  static const pageNames = [\n    \"Search\",\n    \"Downloading\",\n    \"Downloaded\",\n    \"Explore\",\n    \"Bookmarks\",\n    \"Following\",\n    \"History\",\n    \"Ranking\",\n    \"Recommendation\",\n    \"Bookmarks\",\n    \"Following\",\n    \"Ranking\",\n  ];\n\n  @override\n  Widget build(BuildContext context) {\n    return ScaffoldPage(\n      header: TitleBar(title: \"Initial Page\".tl),\n      content: ListView.builder(\n        itemCount: pageNames.length + 2,\n        itemBuilder: (context, index) {\n          if (index == 3) {\n            return Text('${\"Illustrations\".tl}/${\"Manga\".tl}')\n                .paddingHorizontal(16)\n                .paddingVertical(8);\n          } else if (index > 3) {\n            index--;\n          }\n          if (index == 8) {\n            return Text(\"Novel\".tl).paddingHorizontal(16).paddingVertical(8);\n          } else if (index > 8) {\n            index--;\n          }\n\n          return Card(\n            margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),\n            padding: EdgeInsets.zero,\n            child: ListTile(\n              title: Text(pageNames[index].tl),\n              trailing: RadioButton(\n                checked: this.index - 1 == index,\n                onChanged: (value) {\n                  setState(() {\n                    this.index = index + 1;\n                    appdata.settings[\"initialPage\"] = index + 1;\n                    appdata.writeData();\n                  });\n                },\n              ),\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n\nclass _MacosDownloadPathSelectButton extends StatefulWidget {\n  const _MacosDownloadPathSelectButton({this.onSelected});\n\n  final void Function(String)? onSelected;\n\n  @override\n  State<_MacosDownloadPathSelectButton> createState() =>\n      _MacosDownloadPathSelectButtonState();\n}\n\nclass _MacosDownloadPathSelectButtonState\n    extends State<_MacosDownloadPathSelectButton> {\n  static const _channel = MethodChannel(\"pixes/macos/download_path\");\n\n  bool _selecting = false;\n\n  @override\n  Widget build(BuildContext context) {\n    return Button(\n      onPressed: _selecting\n          ? null\n          : () async {\n              setState(() {\n                _selecting = true;\n              });\n              try {\n                final selectedPath = await _channel.invokeMethod<String>(\n                  \"selectDownloadDirectory\",\n                  {\"initialPath\": appdata.settings[\"downloadPath\"]},\n                );\n                if (!context.mounted || selectedPath == null) {\n                  return;\n                }\n                widget.onSelected?.call(selectedPath);\n              } on PlatformException catch (e) {\n                if (context.mounted) {\n                  showToast(context, message: e.message ?? e.code);\n                }\n              } finally {\n                if (context.mounted) {\n                  setState(() {\n                    _selecting = false;\n                  });\n                }\n              }\n            },\n      child: Text(_selecting ? \"...\" : \"Manage\".tl).fixWidth(64),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/pages/user_info_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:flutter/gestures.dart';\nimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/components/batch_download.dart';\nimport 'package:pixes/components/grid.dart';\nimport 'package:pixes/components/loading.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:pixes/components/novel.dart';\nimport 'package:pixes/components/segmented_button.dart';\nimport 'package:pixes/components/user_preview.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/image_provider.dart';\nimport 'package:pixes/network/network.dart';\nimport 'package:pixes/pages/following_users_page.dart';\nimport 'package:pixes/utils/block.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\n\nimport '../components/illust_widget.dart';\nimport 'illust_page.dart';\n\nclass UserInfoPage extends StatefulWidget {\n  const UserInfoPage(this.id, {super.key});\n\n  final String id;\n\n  static Map<String, UpdateFollowCallback> followCallbacks = {};\n\n  @override\n  State<UserInfoPage> createState() => _UserInfoPageState();\n}\n\nclass _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {\n  @override\n  void initState() {\n    UserInfoPage.followCallbacks[widget.id] = (v) {\n      if (data == null) return;\n      setState(() {\n        data!.isFollowed = v;\n      });\n    };\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    UserInfoPage.followCallbacks.remove(widget.id);\n    super.dispose();\n  }\n\n  int page = 0;\n\n  @override\n  Widget buildContent(BuildContext context, UserDetails data) {\n    return ScaffoldPage(\n      content: CustomScrollView(\n        slivers: [\n          buildUser(),\n          SliverToBoxAdapter(\n            child: buildHeader(\"Related users\".tl),\n          ),\n          _RelatedUsers(widget.id),\n          buildInformation(),\n          buildArtworkHeader(),\n          if (page == 4)\n            _UserNovels(widget.id)\n          else\n            _UserArtworks(\n              data.id.toString(),\n              page,\n              key: ValueKey(data.id + page),\n            ),\n          SliverPadding(\n              padding: EdgeInsets.only(bottom: context.padding.bottom)),\n        ],\n      ),\n    );\n  }\n\n  bool isFollowing = false;\n\n  void follow() async {\n    if (isFollowing) return;\n    String type = \"\";\n    if (!data!.isFollowed) {\n      await flyoutController.showFlyout(\n          navigatorKey: App.rootNavigatorKey.currentState,\n          builder: (context) => MenuFlyout(\n                items: [\n                  MenuFlyoutItem(\n                      text: Text(\"Public\".tl),\n                      onPressed: () => type = \"public\"),\n                  MenuFlyoutItem(\n                      text: Text(\"Private\".tl),\n                      onPressed: () => type = \"private\"),\n                ],\n              ));\n    }\n    if (type.isEmpty && !data!.isFollowed) {\n      return;\n    }\n    setState(() {\n      isFollowing = true;\n    });\n    var method = data!.isFollowed ? \"delete\" : \"add\";\n    var res = await Network().follow(data!.id.toString(), method, type);\n    if (res.error) {\n      if (mounted) {\n        context.showToast(message: \"Network Error\");\n      }\n    } else {\n      data!.isFollowed = !data!.isFollowed;\n      UserPreviewWidget.followCallbacks[data!.id.toString()]\n          ?.call(data!.isFollowed);\n      IllustPage.updateFollow(data!.id.toString(), data!.isFollowed);\n    }\n    setState(() {\n      isFollowing = false;\n    });\n  }\n\n  var flyoutController = FlyoutController();\n\n  Widget buildUser() {\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          Container(\n            width: 64,\n            height: 64,\n            decoration: BoxDecoration(\n                borderRadius: BorderRadius.circular(64),\n                border: Border.all(\n                    color: ColorScheme.of(context).outlineVariant, width: 0.6)),\n            child: ClipRRect(\n              borderRadius: BorderRadius.circular(64),\n              child: Image(\n                image: CachedImageProvider(data!.avatar),\n                width: 64,\n                height: 64,\n                fit: BoxFit.cover,\n              ),\n            ),\n          ),\n          const SizedBox(height: 8),\n          Text(data!.name,\n              style:\n                  const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),\n          const SizedBox(height: 4),\n          Text.rich(\n            TextSpan(\n              children: [\n                TextSpan(text: 'Follows: '.tl),\n                TextSpan(\n                    text: '${data!.totalFollowUsers}',\n                    recognizer: TapGestureRecognizer()\n                      ..onTap = (() =>\n                          context.to(() => FollowingUsersPage(widget.id))),\n                    style: TextStyle(\n                        fontWeight: FontWeight.bold,\n                        color: FluentTheme.of(context).accentColor)),\n              ],\n            ),\n            style: const TextStyle(fontSize: 14),\n          ),\n          if (widget.id != appdata.account?.user.id)\n            const SizedBox(\n              height: 8,\n            ),\n          if (widget.id != appdata.account?.user.id)\n            if (isFollowing)\n              Button(\n                  onPressed: follow,\n                  child: const SizedBox(\n                    width: 42,\n                    height: 24,\n                    child: Center(\n                      child: SizedBox.square(\n                        dimension: 18,\n                        child: ProgressRing(\n                          strokeWidth: 2,\n                        ),\n                      ),\n                    ),\n                  ))\n            else if (!data!.isFollowed)\n              FlyoutTarget(\n                  controller: flyoutController,\n                  child: Button(onPressed: follow, child: Text(\"Follow\".tl)))\n            else\n              Button(\n                onPressed: follow,\n                child: Text(\n                  \"Unfollow\".tl,\n                  style: TextStyle(color: ColorScheme.of(context).error),\n                ),\n              ),\n        ],\n      ),\n    );\n  }\n\n  Widget buildHeader(String title, {Widget? action}) {\n    return SizedBox(\n            width: double.infinity,\n            height: 38,\n            child: Row(\n              children: [\n                Text(\n                  title,\n                  style: const TextStyle(fontWeight: FontWeight.w600),\n                ).toAlign(Alignment.centerLeft),\n                const Spacer(),\n                if (action != null) action.toAlign(Alignment.centerRight)\n              ],\n            ).paddingHorizontal(16))\n        .paddingTop(8);\n  }\n\n  Widget buildArtworkHeader() {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n              width: double.infinity,\n              height: 38,\n              child: Row(\n                children: [\n                  SegmentedButton<int>(\n                    options: [\n                      SegmentedButtonOption(0, \"Artworks\".tl),\n                      SegmentedButtonOption(1, \"Illustrations\".tl),\n                      SegmentedButtonOption(2, \"Mangas\".tl),\n                      SegmentedButtonOption(3, \"Bookmarks\".tl),\n                      SegmentedButtonOption(4, \"Novels\".tl),\n                    ],\n                    value: page,\n                    onPressed: (value) {\n                      setState(() {\n                        page = value;\n                      });\n                    },\n                  ),\n                  const Spacer(),\n                  if (page != 4)\n                    BatchDownloadButton(\n                      request: () {\n                        switch (page) {\n                          case 0:\n                            return Network()\n                                .getUserIllusts(data!.id.toString(), null);\n                          case 1:\n                            return Network()\n                                .getUserIllusts(data!.id.toString(), \"illust\");\n                          case 2:\n                            return Network()\n                                .getUserIllusts(data!.id.toString(), \"manga\");\n                          case 3:\n                            return Network()\n                                .getUserBookmarks(data!.id.toString());\n                        }\n                        throw \"Invalid page\";\n                      },\n                    ),\n                ],\n              ).paddingHorizontal(16))\n          .paddingTop(12),\n    );\n  }\n\n  Widget buildInformation() {\n    Widget buildItem(\n        {IconData? icon,\n        required String title,\n        required String? content,\n        Widget? trailing}) {\n      if (content == null || content.isEmpty) {\n        return const SizedBox.shrink();\n      }\n      return Card(\n        margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),\n        padding: EdgeInsets.zero,\n        child: ListTile(\n          title: Row(\n            children: [\n              Icon(icon, size: 20),\n              const SizedBox(width: 8),\n              Text(title)\n            ],\n          ),\n          subtitle: SelectableText(content).paddingLeft(icon == null ? 0 : 28),\n          trailing: trailing,\n        ),\n      );\n    }\n\n    return SliverToBoxAdapter(\n      child: Column(\n        children: [\n          buildHeader(\"Information\".tl),\n          buildItem(\n              icon: MdIcons.comment_outlined,\n              title: \"Introduction\".tl,\n              content: data!.comment),\n          buildItem(\n              icon: MdIcons.cake_outlined,\n              title: \"Birthday\".tl,\n              content: data!.birth),\n          buildItem(\n              icon: MdIcons.location_city_outlined,\n              title: \"Region\",\n              content: data!.region),\n          buildItem(\n              icon: MdIcons.work_outline, title: \"Job\".tl, content: data!.job),\n          buildItem(\n              icon: MdIcons.person_2_outlined,\n              title: \"Gender\".tl,\n              content: data!.gender),\n          buildHeader(\"Social Network\".tl),\n          buildItem(\n              title: \"Webpage\",\n              content: data!.webpage,\n              trailing: IconButton(\n                  icon: const Icon(MdIcons.open_in_new, size: 18),\n                  onPressed: () => launchUrlString(data!.twitterUrl!))),\n          buildItem(\n              title: \"Twitter\",\n              content: data!.twitterUrl,\n              trailing: IconButton(\n                  icon: const Icon(MdIcons.open_in_new, size: 18),\n                  onPressed: () => launchUrlString(data!.twitterUrl!))),\n          buildItem(\n              title: \"pawoo\",\n              content: data!.pawooUrl,\n              trailing: IconButton(\n                  icon: const Icon(\n                    MdIcons.open_in_new,\n                    size: 18,\n                  ),\n                  onPressed: () => launchUrlString(data!.pawooUrl!))),\n        ],\n      ),\n    );\n  }\n\n  @override\n  Future<Res<UserDetails>> loadData() {\n    return Network().getUserDetails(widget.id);\n  }\n}\n\nclass _UserArtworks extends StatefulWidget {\n  const _UserArtworks(this.uid, this.type, {super.key});\n\n  final String uid;\n\n  final int type;\n\n  @override\n  State<_UserArtworks> createState() => _UserArtworksState();\n}\n\nclass _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {\n  @override\n  Widget buildLoading(BuildContext context) {\n    return const SliverToBoxAdapter(\n      child: SizedBox(\n        child: Center(\n          child: ProgressRing(),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget buildError(context, error) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        child: Center(\n          child: Row(\n            children: [\n              const Icon(FluentIcons.info),\n              const SizedBox(\n                width: 4,\n              ),\n              Text(error)\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, List<Illust> data) {\n    checkIllusts(data);\n    return SliverMasonryGrid(\n      gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(\n        maxCrossAxisExtent: 240,\n      ),\n      delegate: SliverChildBuilderDelegate(\n        (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return IllustWidget(data[index], onTap: () {\n            context.to(() => IllustGalleryPage(\n                illusts: data, initialPage: index, nextUrl: nextUrl));\n          });\n        },\n        childCount: data.length,\n      ),\n    ).sliverPaddingHorizontal(8);\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Illust>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = nextUrl == null\n        ? (widget.type != 3\n            ? await Network().getUserIllusts(\n                widget.uid, [null, \"illust\", \"manga\"][widget.type])\n            : await Network().getUserBookmarks(widget.uid))\n        : await Network().getIllustsWithNextUrl(nextUrl!);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass _UserNovels extends StatefulWidget {\n  const _UserNovels(this.uid);\n\n  final String uid;\n\n  @override\n  State<_UserNovels> createState() => _UserNovelsState();\n}\n\nclass _UserNovelsState extends MultiPageLoadingState<_UserNovels, Novel> {\n  @override\n  Widget buildLoading(BuildContext context) {\n    return const SliverToBoxAdapter(\n      child: SizedBox(\n        child: Center(\n          child: ProgressRing(),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget buildError(context, error) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        child: Center(\n          child: Row(\n            children: [\n              const Icon(FluentIcons.info),\n              const SizedBox(\n                width: 4,\n              ),\n              Text(error)\n            ],\n          ),\n        ),\n      ),\n    );\n  }\n\n  @override\n  Widget buildContent(BuildContext context, List<Novel> data) {\n    return SliverGridViewWithFixedItemHeight(\n      itemHeight: 164,\n      minCrossAxisExtent: 400,\n      delegate: SliverChildBuilderDelegate(\n        (context, index) {\n          if (index == data.length - 1) {\n            nextPage();\n          }\n          return NovelWidget(data[index]);\n        },\n        childCount: data.length,\n      ),\n    ).sliverPaddingHorizontal(8);\n  }\n\n  String? nextUrl;\n\n  @override\n  Future<Res<List<Novel>>> loadData(page) async {\n    if (nextUrl == \"end\") {\n      return Res.error(\"No more data\");\n    }\n    var res = nextUrl == null\n        ? await Network().getUserNovels(widget.uid)\n        : await Network().getNovelsWithNextUrl(nextUrl!);\n    if (!res.error) {\n      nextUrl = res.subData;\n      nextUrl ??= \"end\";\n    }\n    return res;\n  }\n}\n\nclass _RelatedUsers extends StatefulWidget {\n  const _RelatedUsers(this.uid);\n\n  final String uid;\n\n  @override\n  State<_RelatedUsers> createState() => _RelatedUsersState();\n}\n\nclass _RelatedUsersState\n    extends LoadingState<_RelatedUsers, List<UserPreview>> {\n  @override\n  Widget buildFrame(BuildContext context, Widget child) {\n    return SliverToBoxAdapter(\n      child: SizedBox(\n        height: 146,\n        width: double.infinity,\n        child: child,\n      ),\n    );\n  }\n\n  final ScrollController _controller = ScrollController();\n\n  @override\n  Widget buildContent(BuildContext context, List<UserPreview> data) {\n    Widget content = Scrollbar(\n        controller: _controller,\n        child: ListView.builder(\n          controller: _controller,\n          padding: const EdgeInsets.only(bottom: 8, left: 8),\n          primary: false,\n          scrollDirection: Axis.horizontal,\n          itemCount: data.length,\n          itemBuilder: (context, index) {\n            return UserPreviewWidget(data[index]).fixWidth(342);\n          },\n        ));\n    if (App.isDesktop) {\n      content = ScrollbarTheme.merge(\n          data: const ScrollbarThemeData(\n              thickness: 6,\n              hoveringThickness: 6,\n              mainAxisMargin: 4,\n              hoveringPadding: EdgeInsets.zero,\n              padding: EdgeInsets.zero,\n              hoveringMainAxisMargin: 4,\n              crossAxisMargin: 0,\n              hoveringCrossAxisMargin: 0),\n          child: content);\n    } else {\n      content = ScrollbarTheme.merge(\n          data: const ScrollbarThemeData(\n              thickness: 4,\n              hoveringThickness: 4,\n              mainAxisMargin: 4,\n              hoveringPadding: EdgeInsets.zero,\n              padding: EdgeInsets.zero,\n              hoveringMainAxisMargin: 4,\n              crossAxisMargin: 0,\n              hoveringCrossAxisMargin: 0),\n          child: content);\n    }\n    return MediaQuery.removePadding(\n        context: context, removeBottom: true, child: content);\n  }\n\n  @override\n  Future<Res<List<UserPreview>>> loadData() {\n    return Network().relatedUsers(widget.uid);\n  }\n}\n"
  },
  {
    "path": "lib/pages/webview_page.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/components/md.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\nimport 'package:webview_flutter/webview_flutter.dart';\n\nimport '../foundation/app.dart';\n\ndouble get _appBarHeight => App.isDesktop ? 36.0 : 48.0;\n\nclass WebviewPage extends StatefulWidget {\n  const WebviewPage(this.url, {this.onNavigation, super.key});\n\n  final String url;\n\n  final bool Function(NavigationRequest req)? onNavigation;\n\n  @override\n  State<WebviewPage> createState() => _WebviewPageState();\n}\n\nclass _WebviewPageState extends State<WebviewPage> {\n  WebViewController? controller;\n\n  @override\n  void initState() {\n    super.initState();\n  }\n\n  NavigationDecision handleNavigation(NavigationRequest req) {\n    if (widget.onNavigation != null) {\n      return widget.onNavigation!(req)\n          ? NavigationDecision.navigate\n          : NavigationDecision.prevent;\n    }\n    return NavigationDecision.navigate;\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    controller ??= WebViewController()\n      ..setJavaScriptMode(JavaScriptMode.unrestricted)\n      ..setBackgroundColor(FluentTheme.of(context).brightness == Brightness.light\n          ? Colors.white\n          : Colors.black)\n      ..setNavigationDelegate(\n        NavigationDelegate(\n          onProgress: (int progress) {\n            // Update loading bar.\n          },\n          onPageStarted: (String url) {},\n          onPageFinished: (String url) {},\n          onWebResourceError: (WebResourceError error) {},\n          onNavigationRequest: handleNavigation,\n        ),\n      )\n      ..loadRequest(Uri.parse(widget.url));\n    return Column(\n      children: [\n        SizedBox(\n          height: _appBarHeight,\n          child: Row(\n            children: [\n              const Text(\"Webview\"),\n              const Spacer(),\n              IconButton(\n                icon: const Icon(MdIcons.open_in_new, size: 20,),\n                onPressed: () {\n                  launchUrlString(widget.url);\n                  context.pop();\n                },\n              ),\n            ],\n          ).paddingHorizontal(16),\n        ).paddingTop(MediaQuery.of(context).padding.top),\n        Expanded(\n          child: WebViewWidget(controller: controller!,),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/utils/app_links.dart",
    "content": "import 'dart:io';\n\nimport 'package:app_links/app_links.dart';\nimport 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/foundation/log.dart';\nimport 'package:pixes/pages/illust_page.dart';\nimport 'package:pixes/pages/novel_page.dart';\nimport 'package:pixes/pages/search_page.dart';\nimport 'package:pixes/pages/user_info_page.dart';\nimport 'package:pixes/utils/ext.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:win32_registry/win32_registry.dart';\n\nFuture<void> _register(String scheme) async {\n  String appPath = Platform.resolvedExecutable;\n\n  String protocolRegKey = 'Software\\\\Classes\\\\$scheme';\n  RegistryValue protocolRegValue = const RegistryValue.string(\n    'URL Protocol',\n    '',\n  );\n  String protocolCmdRegKey = 'shell\\\\open\\\\command';\n  RegistryValue protocolCmdRegValue = RegistryValue.string(\n    '',\n    '\"$appPath\" \"%1\"',\n  );\n\n  final regKey = Registry.currentUser.createKey(protocolRegKey);\n  regKey.createValue(protocolRegValue);\n  regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue);\n}\n\nvoid _registerPixiv() async {\n  try {\n    await _register(\"pixiv\");\n  } catch (e) {\n    // 注册失败会导致登录不可用\n    while (App.mainNavigatorKey == null) {\n      await Future.delayed(const Duration(milliseconds: 100));\n    }\n    Future.delayed(const Duration(seconds: 1), () async {\n      showDialog(\n          context: App.rootNavigatorKey.currentContext!,\n          builder: (context) => ContentDialog(\n                title: Text(\"Error\".tl),\n                content: Text(\"${\"Failed to register URL scheme.\".tl}\\n$e\"),\n                actions: [\n                  FilledButton(\n                      child: Text(\"Retry\".tl),\n                      onPressed: () {\n                        context.pop();\n                        _registerPixiv();\n                      })\n                ],\n              ));\n    });\n  }\n}\n\nbool Function(Uri uri)? onLink;\n\nbool _firstLink = true;\n\nvoid handleLinks() async {\n  if (App.isWindows) {\n    _registerPixiv();\n  }\n  AppLinks().uriLinkStream.listen((uri) async {\n    if (_firstLink) {\n      await Future.delayed(const Duration(milliseconds: 200));\n    }\n    _firstLink = false;\n    Log.info(\"App Link\", uri.toString());\n    if (onLink?.call(uri) == true) {\n      return;\n    }\n    handleLink(uri);\n  });\n}\n\nbool handleLink(Uri uri) {\n  if (uri.scheme == \"pixiv\") {\n    var path = uri.toString().split(\"/\").sublist(2);\n    if (path.isEmpty) {\n      return false;\n    }\n    switch (path[0]) {\n      case \"users\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext?.to(() => UserInfoPage(path[1]));\n          return true;\n        }\n      case \"novels\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext\n              ?.to(() => NovelPageWithId(path[1]));\n          return true;\n        }\n      case \"illusts\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext\n              ?.to(() => IllustPageWithId(path[1]));\n          return true;\n        }\n    }\n    return false;\n  } else if (uri.scheme == \"https\") {\n    var path = uri.toString().split(\"/\").sublist(3);\n    switch (path[0]) {\n      case \"users\":\n        if (path.length >= 2) {\n          App.mainNavigatorKey?.currentContext?.to(() => UserInfoPage(path[1]));\n          return true;\n        }\n      case \"novel\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext\n              ?.to(() => NovelPageWithId(path[1].nums));\n          return true;\n        }\n      case \"artworks\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext\n              ?.to(() => IllustPageWithId(path[1]));\n          return true;\n        }\n      case \"tags\":\n        if (path.length == 2) {\n          App.mainNavigatorKey?.currentContext\n              ?.to(() => SearchResultPage(path[1]));\n          return true;\n        }\n    }\n  }\n  return false;\n}\n"
  },
  {
    "path": "lib/utils/block.dart",
    "content": "import 'package:pixes/appdata.dart';\nimport 'package:pixes/network/models.dart';\n\nList<Illust> checkIllusts(List<Illust> illusts) {\n  illusts.removeWhere((illust) {\n    if (illust.isBlocked) {\n      return true;\n    }\n    if (appdata.settings[\"blockTags\"] == null) {\n      return false;\n    }\n    if (appdata.settings[\"blockTags\"].contains(\"user:${illust.author.id}\")) {\n      return true;\n    }\n    for (var tag in illust.tags) {\n      if ((appdata.settings[\"blockTags\"] as List).contains(tag.name)) {\n        return true;\n      }\n    }\n    return false;\n  });\n  return illusts;\n}\n"
  },
  {
    "path": "lib/utils/debounce.dart",
    "content": "import 'dart:async';\nimport 'dart:ui';\n\nclass Debounce {\n  final Duration delay;\n  VoidCallback? _action;\n  Timer? _timer;\n\n  Debounce({required this.delay});\n\n  void call(VoidCallback action) {\n    _action = action;\n    _timer?.cancel();\n    _timer = Timer(delay, _execute);\n  }\n\n  void _execute() {\n    if (_action != null) {\n      _action!();\n      _action = null;\n    }\n  }\n\n  void cancel() {\n    _timer?.cancel();\n    _action = null;\n  }\n}"
  },
  {
    "path": "lib/utils/debug.dart",
    "content": "/// function used for debug\nvoid debug() {\n\n}"
  },
  {
    "path": "lib/utils/ext.dart",
    "content": "extension ListExt<T> on List<T>{\n  /// Remove all blank value and return the list.\n  List<T> getNoBlankList(){\n    List<T> newList = [];\n    for(var value in this){\n      if(value.toString() != \"\"){\n        newList.add(value);\n      }\n    }\n    return newList;\n  }\n\n  T? firstWhereOrNull(bool Function(T element) test){\n    for(var element in this){\n      if(test(element)){\n        return element;\n      }\n    }\n    return null;\n  }\n\n  void addIfNotNull(T? value){\n    if(value != null){\n      add(value);\n    }\n  }\n}\n\nextension StringExt on String{\n  ///Remove all value that would display blank on the screen.\n  String get removeAllBlank => replaceAll(\"\\n\", \"\").replaceAll(\" \", \"\").replaceAll(\"\\t\", \"\");\n\n  /// convert this to a one-element list.\n  List<String> toList() => [this];\n\n  String _nums(){\n    String res = \"\";\n    for(int i=0; i<length; i++){\n      res += this[i].isNum?this[i]:\"\";\n    }\n    return res;\n  }\n\n  String get nums => _nums();\n\n  String setValueAt(String value, int index){\n    return replaceRange(index, index+1, value);\n  }\n\n  String? subStringOrNull(int start, [int? end]){\n    if(start < 0 || (end != null && end > length)){\n      return null;\n    }\n    return substring(start, end);\n  }\n\n  String replaceLast(String from, String to) {\n    if (isEmpty || from.isEmpty) {\n      return this;\n    }\n\n    final lastIndex = lastIndexOf(from);\n    if (lastIndex == -1) {\n      return this;\n    }\n\n    final before = substring(0, lastIndex);\n    final after = substring(lastIndex + from.length);\n    return '$before$to$after';\n  }\n\n  static bool hasMatch(String? value, String pattern) {\n    return (value == null) ? false : RegExp(pattern).hasMatch(value);\n  }\n\n  bool _isURL(){\n    final regex = RegExp(\n        r'^((http|https|ftp)://)?[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-|]*[\\w@?^=%&/~+#-])?$',\n        caseSensitive: false);\n    return regex.hasMatch(this);\n  }\n\n  bool get isURL => _isURL();\n\n  bool get isNum => double.tryParse(this) != null;\n}"
  },
  {
    "path": "lib/utils/io.dart",
    "content": "import 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:file_selector/file_selector.dart';\nimport 'package:flutter_file_dialog/flutter_file_dialog.dart';\nimport 'package:pixes/foundation/app.dart';\n\nextension FSExt on FileSystemEntity {\n  Future<void> deleteIfExists() async {\n    if (await exists()) {\n      await delete();\n    }\n  }\n\n  Future<void> deleteIgnoreError() async {\n    try {\n      await delete();\n    } catch (e) {\n      // ignore\n    }\n  }\n\n  int get size {\n    if (this is File) {\n      return (this as File).lengthSync();\n    } else if(this is Directory){\n      var size = 0;\n      for(var file in (this as Directory).listSync()){\n        size += file.size;\n      }\n      return size;\n    }\n    return 0;\n  }\n}\n\nextension DirectoryExt on Directory {\n  bool havePermission() {\n    if(!existsSync()) return false;\n    if(App.isMacOS) {\n      return true;\n    }\n    try {\n      listSync();\n      return true;\n    } catch (e) {\n      return false;\n    }\n  }\n}\n\nString bytesToText(int bytes) {\n  if(bytes < 1024) {\n    return \"$bytes B\";\n  } else if(bytes < 1024 * 1024) {\n    return \"${(bytes / 1024).toStringAsFixed(2)} KB\";\n  } else if(bytes < 1024 * 1024 * 1024) {\n    return \"${(bytes / 1024 / 1024).toStringAsFixed(2)} MB\";\n  } else {\n    return \"${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB\";\n  }\n}\n\nvoid saveFile(File file, [String? name]) async{\n  if(App.isDesktop) {\n    var fileName = file.path.split('/').last;\n    final FileSaveLocation? result =\n    await getSaveLocation(suggestedName: name ?? fileName);\n    if (result == null) {\n      return;\n    }\n\n    final Uint8List fileData = await file.readAsBytes();\n    String mimeType = 'image/${fileName.split('.').last}';\n    final XFile textFile = XFile.fromData(\n        fileData, mimeType: mimeType, name: name ?? fileName);\n    await textFile.saveTo(result.path);\n  } else {\n    final params = SaveFileDialogParams(sourceFilePath: file.path, fileName: name);\n    await FlutterFileDialog.saveFile(params: params);\n  }\n}"
  },
  {
    "path": "lib/utils/loop.dart",
    "content": "import 'dart:async';\n\nclass Loop {\n  static final List<void Function()> _callbacks = [];\n\n  static void start() {\n    Timer.periodic(const Duration(milliseconds: 100), (timer) {\n      for(var func in _callbacks) {\n        func.call();\n      }\n    });\n  }\n\n  static void register(void Function() func) {\n    _callbacks.add(func);\n  }\n\n  static void remove(void Function() func) {\n    _callbacks.remove(func);\n  }\n}"
  },
  {
    "path": "lib/utils/mouse_listener.dart",
    "content": "import 'package:flutter/services.dart';\nimport 'package:flutter/widgets.dart';\nimport '../foundation/app.dart';\n\nvoid mouseSideButtonCallback(GlobalKey<NavigatorState> key){\n  if(App.rootNavigatorKey.currentState?.canPop() ?? false) {\n    App.rootNavigatorKey.currentState?.pop();\n    return;\n  }\n  if(key.currentState?.canPop() ?? false){\n    key.currentState?.pop();\n  }\n}\n\n///监听鼠标侧键, 若为下键, 则调用返回\nvoid listenMouseSideButtonToBack(GlobalKey<NavigatorState> key) async{\n  if(!App.isWindows){\n    return;\n  }\n  const channel = EventChannel(\"pixes/mouse\");\n  await for(var res in channel.receiveBroadcastStream()){\n    if(res == 0){\n      mouseSideButtonCallback(key);\n    }\n  }\n}"
  },
  {
    "path": "lib/utils/translation.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/services.dart';\nimport 'package:pixes/foundation/app.dart';\n\nextension Translation on String {\n  String get tl {\n    var locale = App.locale;\n    return translation[\"${locale.languageCode}_${locale.countryCode}\"]?[this] ??\n        this;\n  }\n\n  static late final Map<String, Map<String, dynamic>> translation;\n\n  static Future<void> init() async{\n    var data = await rootBundle.loadString(\"assets/tr.json\");\n    translation = Map<String, Map<String, dynamic>>.from(jsonDecode(data));\n  }\n}\n"
  },
  {
    "path": "lib/utils/update.dart",
    "content": "import 'package:fluent_ui/fluent_ui.dart';\nimport 'package:pixes/appdata.dart';\nimport 'package:pixes/foundation/app.dart';\nimport 'package:pixes/network/app_dio.dart';\nimport 'package:pixes/utils/translation.dart';\nimport 'package:url_launcher/url_launcher_string.dart';\n\nFuture<String> getLatestVersion() async {\n  var dio = AppDio();\n  var res = await dio\n      .get(\"https://raw.githubusercontent.com/wgh136/pixes/refs/heads/master/pubspec.yaml\");\n  var lines = (res.data as String).split(\"\\n\");\n  for (var line in lines) {\n    if (line.startsWith(\"version:\")) {\n      return line.split(\":\")[1].split('+')[0].trim();\n    }\n  }\n  throw \"Failed to get latest version\";\n}\n\n/// Compare two versions.\n/// Return `true` if `a` is greater than `b`.\nbool compareVersion(String a, String b) {\n  var aList = a.split(\".\").map(int.parse).toList();\n  var bList = b.split(\".\").map(int.parse).toList();\n  for (var i = 0; i < aList.length; i++) {\n    if (aList[i] > bList[i]) {\n      return true;\n    } else if (aList[i] < bList[i]) {\n      return false;\n    }\n  }\n  return false;\n}\n\nFuture<void> checkUpdate() async {\n  if (appdata.account == null) return;\n  try {\n    var latestVersion = await getLatestVersion();\n    if (compareVersion(latestVersion, App.version)) {\n      showDialog(\n          context: App.rootNavigatorKey.currentContext!,\n          builder: (context) => ContentDialog(\n                title: Text(\"New version available\".tl),\n                content: Text(\n                  \"A new version of Pixes is available. Do you want to update now?\"\n                      .tl,\n                ),\n                actions: [\n                  Button(\n                    child: Text(\"Cancel\".tl),\n                    onPressed: () {\n                      Navigator.of(context).pop();\n                    },\n                  ),\n                  FilledButton(\n                      child: Text(\"Update\".tl),\n                      onPressed: () {\n                        Navigator.of(context).pop();\n                        launchUrlString(\n                            \"https://github.com/wgh136/pixes/releases/latest\");\n                      })\n                ],\n              ));\n    }\n  } catch (e) {\n    // ignore\n  }\n}\n"
  },
  {
    "path": "lib/utils/window.dart",
    "content": "import 'dart:convert';\nimport 'dart:ui';\nimport 'dart:io';\n\nimport 'package:pixes/foundation/app.dart';\nimport 'package:window_manager/window_manager.dart';\n\nclass WindowPlacement {\n  final Rect rect;\n\n  final bool isMaximized;\n\n  const WindowPlacement(this.rect, this.isMaximized);\n\n  Future<void> applyToWindow() async {\n    await windowManager.setBounds(rect);\n\n    if(!validate(rect)){\n      await windowManager.center();\n    }\n\n    if (isMaximized) {\n      await windowManager.maximize();\n    }\n  }\n\n  Future<void> writeToFile() async {\n    var file = File(\"${App.dataPath}/window_placement\");\n    await file.writeAsString(jsonEncode({\n      'width': rect.width,\n      'height': rect.height,\n      'x': rect.topLeft.dx,\n      'y': rect.topLeft.dy,\n      'isMaximized': isMaximized\n    }));\n  }\n\n  static Future<WindowPlacement> loadFromFile() async {\n    var file = File(\"${App.dataPath}/window_placement\");\n    if (!file.existsSync()) {\n      return defaultPlacement;\n    }\n    var json = jsonDecode(await file.readAsString());\n    var rect =\n        Rect.fromLTWH(json['x'], json['y'], json['width'], json['height']);\n    return WindowPlacement(rect, json['isMaximized']);\n  }\n\n  static Future<WindowPlacement> get current async {\n    var rect = await windowManager.getBounds();\n    var isMaximized = await windowManager.isMaximized();\n    return WindowPlacement(rect, isMaximized);\n  }\n\n  static const defaultPlacement =\n      WindowPlacement(Rect.fromLTWH(10, 10, 900, 600), false);\n\n  static WindowPlacement cache = defaultPlacement;\n\n  static void loop() async {\n    var placement = await WindowPlacement.current;\n    if(!validate(placement.rect)){\n      return;\n    }\n    if (placement.rect != cache.rect ||\n        placement.isMaximized != cache.isMaximized) {\n      cache = placement;\n      await placement.writeToFile();\n    }\n  }\n\n  static bool validate(Rect rect){\n    return rect.topLeft.dx >= 0 && rect.topLeft.dy >= 0;\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"pixes\")\n# The unique GTK application identifier for this application. See:\n# https://wiki.gnome.org/HowDoI/ChooseApplicationID\nset(APPLICATION_ID \"com.github.wgh136.pixes\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(SET CMP0063 NEW)\n\n# Load bundled libraries from the lib/ directory relative to the binary.\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Define build configuration options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Define the application target. To change its name, change BINARY_NAME above,\n# not the value here, or `flutter run` will no longer work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add dependency libraries. Add any application-specific dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nforeach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})\n  install(FILES \"${bundled_library}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendforeach(bundled_library)\n\n# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/linux/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\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": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "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 <dynamic_color/dynamic_color_plugin.h>\n#include <file_selector_linux/file_selector_plugin.h>\n#include <gtk/gtk_plugin.h>\n#include <screen_retriever_linux/screen_retriever_linux_plugin.h>\n#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>\n#include <url_launcher_linux/url_launcher_plugin.h>\n#include <window_manager/window_manager_plugin.h>\n\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) dynamic_color_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"DynamicColorPlugin\");\n  dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);\n  g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"FileSelectorPlugin\");\n  file_selector_plugin_register_with_registrar(file_selector_linux_registrar);\n  g_autoptr(FlPluginRegistrar) gtk_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"GtkPlugin\");\n  gtk_plugin_register_with_registrar(gtk_registrar);\n  g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"ScreenRetrieverLinuxPlugin\");\n  screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);\n  g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"Sqlite3FlutterLibsPlugin\");\n  sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);\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  g_autoptr(FlPluginRegistrar) window_manager_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"WindowManagerPlugin\");\n  window_manager_plugin_register_with_registrar(window_manager_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  dynamic_color\n  file_selector_linux\n  gtk\n  screen_retriever_linux\n  sqlite3_flutter_libs\n  url_launcher_linux\n  window_manager\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\n  GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));\n  if (windows) {\n    gtk_window_present(GTK_WINDOW(windows->data));\n    return;\n  }\n\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\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, \"pixes\");\n    gtk_header_bar_set_show_close_button(header_bar, TRUE);\n    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\n  } else {\n    gtk_window_set_title(window, \"pixes\");\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 FALSE;\n}\n\n// Implements GApplication::startup.\nstatic void my_application_startup(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application startup.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->startup(application);\n}\n\n// Implements GApplication::shutdown.\nstatic void my_application_shutdown(GApplication* application) {\n  //MyApplication* self = MY_APPLICATION(object);\n\n  // Perform any actions required at application shutdown.\n\n  G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);\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_APPLICATION_CLASS(klass)->startup = my_application_startup;\n  G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,\n                                     nullptr));\n}\n"
  },
  {
    "path": "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**/dgph\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 app_links\nimport device_info_plus\nimport dynamic_color\nimport file_selector_macos\nimport path_provider_foundation\nimport screen_retriever_macos\nimport share_plus\nimport sqlite3_flutter_libs\nimport url_launcher_macos\nimport webview_flutter_wkwebview\nimport window_manager\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: \"AppLinksMacosPlugin\"))\n  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: \"DeviceInfoPlusMacosPlugin\"))\n  DynamicColorPlugin.register(with: registry.registrar(forPlugin: \"DynamicColorPlugin\"))\n  FileSelectorPlugin.register(with: registry.registrar(forPlugin: \"FileSelectorPlugin\"))\n  PathProviderPlugin.register(with: registry.registrar(forPlugin: \"PathProviderPlugin\"))\n  ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: \"ScreenRetrieverMacosPlugin\"))\n  SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: \"SharePlusMacosPlugin\"))\n  Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: \"Sqlite3FlutterLibsPlugin\"))\n  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: \"UrlLauncherPlugin\"))\n  WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: \"WebViewFlutterPlugin\"))\n  WindowManagerPlugin.register(with: registry.registrar(forPlugin: \"WindowManagerPlugin\"))\n}\n"
  },
  {
    "path": "macos/Podfile",
    "content": "platform :osx, '14.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', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\n#  target 'RunnerTests' do\n#    inherit! :search_paths\n#  end\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n    target.build_configurations.each do |config|\n      config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '14.0'\n    end\n  end\nend\n"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@main\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n\n  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "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                <menuItem title=\"Help\" id=\"EPT-qC-fAb\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Help\" systemMenu=\"help\" id=\"rJ0-wn-3NY\"/>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "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 = pixes\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = dev.nyne.pixes\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2026 dev.nyne. 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\t<key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <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>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>pixiv</string>\n\t\t\t<key>CFBundleURLSchemes</key>\n\t\t\t<array>\n\t\t\t\t<string>pixiv</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>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\t<key>com.apple.security.files.user-selected.read-write</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\nimport IOKit.ps\n\nclass MainFlutterWindow: NSWindow {\n  private static let downloadPathBookmarkKey = \"pixes.download.path.bookmark\"\n  private static var scopedDownloadDirectoryURL: URL?\n\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    let proxyChannel = FlutterMethodChannel(\n      name: \"pixes/proxy\",\n      binaryMessenger: flutterViewController.engine.binaryMessenger)\n      proxyChannel.setMethodCallHandler { (call, result) in\n      // 获取代理设置\n        if let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as NSDictionary?,\n          let dict = proxySettings.object(forKey: kCFNetworkProxiesHTTPProxy) as? NSDictionary,\n          let host = dict.object(forKey: kCFNetworkProxiesHTTPProxy) as? String,\n          let port = dict.object(forKey: kCFNetworkProxiesHTTPPort) as? Int {\n          let proxyConfig = \"\\(host):\\(port)\"\n          result(proxyConfig)\n        } else {\n          result(\"No proxy\")\n        }\n    }\n\n    let downloadPathChannel = FlutterMethodChannel(\n      name: \"pixes/macos/download_path\",\n      binaryMessenger: flutterViewController.engine.binaryMessenger)\n    downloadPathChannel.setMethodCallHandler { [weak self] call, result in\n      guard let self else {\n        result(FlutterError(code: \"window_deallocated\", message: nil, details: nil))\n        return\n      }\n      switch call.method {\n      case \"selectDownloadDirectory\":\n        self.handleSelectDownloadDirectory(call: call, result: result)\n      case \"restoreDownloadDirectoryAccess\":\n        self.handleRestoreDownloadDirectoryAccess(call: call, result: result)\n      default:\n        result(FlutterMethodNotImplemented)\n      }\n    }\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n\n  override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {\n    super.order(place, relativeTo: otherWin)\n    hiddenWindowAtLaunch()\n  }\n\n  private func handleSelectDownloadDirectory(call: FlutterMethodCall, result: @escaping FlutterResult) {\n    let args = call.arguments as? [String: Any]\n    let initialPath = args?[\"initialPath\"] as? String\n\n    let panel = NSOpenPanel()\n    panel.canChooseDirectories = true\n    panel.canChooseFiles = false\n    panel.allowsMultipleSelection = false\n    panel.canCreateDirectories = true\n    panel.prompt = \"Select\"\n    if let initialPath {\n      panel.directoryURL = URL(fileURLWithPath: (initialPath as NSString).expandingTildeInPath)\n    }\n\n    if panel.runModal() != .OK {\n      result(nil)\n      return\n    }\n\n    guard let url = panel.url else {\n      result(nil)\n      return\n    }\n\n    do {\n      try persistScopedDirectoryAccess(url: url)\n      result(url.path)\n    } catch {\n      result(FlutterError(\n        code: \"persist_directory_access_failed\",\n        message: \"Failed to persist selected directory access.\",\n        details: \"\\(error)\"\n      ))\n    }\n  }\n\n  private func handleRestoreDownloadDirectoryAccess(call: FlutterMethodCall, result: @escaping FlutterResult) {\n    let args = call.arguments as? [String: Any]\n    let expectedPath = args?[\"path\"] as? String\n    do {\n      let restoredPath = try restoreScopedDirectoryAccess(expectedPath: expectedPath)\n      result(restoredPath)\n    } catch {\n      result(FlutterError(\n        code: \"restore_directory_access_failed\",\n        message: \"Failed to restore persisted directory access.\",\n        details: \"\\(error)\"\n      ))\n    }\n  }\n\n  private func persistScopedDirectoryAccess(url: URL) throws {\n    if let previousURL = MainFlutterWindow.scopedDownloadDirectoryURL {\n      previousURL.stopAccessingSecurityScopedResource()\n      MainFlutterWindow.scopedDownloadDirectoryURL = nil\n    }\n\n    guard url.startAccessingSecurityScopedResource() else {\n      throw NSError(\n        domain: \"pixes.macos.download_path\",\n        code: 1,\n        userInfo: [NSLocalizedDescriptionKey: \"Unable to access selected directory.\"]\n      )\n    }\n\n    let bookmark = try url.bookmarkData(\n      options: [.withSecurityScope],\n      includingResourceValuesForKeys: nil,\n      relativeTo: nil\n    )\n    UserDefaults.standard.set(bookmark, forKey: MainFlutterWindow.downloadPathBookmarkKey)\n    MainFlutterWindow.scopedDownloadDirectoryURL = url\n  }\n\n  private func restoreScopedDirectoryAccess(expectedPath: String?) throws -> String? {\n    guard let bookmark = UserDefaults.standard.data(forKey: MainFlutterWindow.downloadPathBookmarkKey) else {\n      return nil\n    }\n\n    var stale = false\n    var url = try URL(\n      resolvingBookmarkData: bookmark,\n      options: [.withSecurityScope],\n      relativeTo: nil,\n      bookmarkDataIsStale: &stale\n    )\n\n    if stale {\n      let refreshedBookmark = try url.bookmarkData(\n        options: [.withSecurityScope],\n        includingResourceValuesForKeys: nil,\n        relativeTo: nil\n      )\n      UserDefaults.standard.set(refreshedBookmark, forKey: MainFlutterWindow.downloadPathBookmarkKey)\n    }\n\n    if let expectedPath {\n      let normalizedExpectedPath = URL(fileURLWithPath: (expectedPath as NSString).expandingTildeInPath)\n        .standardizedFileURL.path\n      let normalizedRestoredPath = url.standardizedFileURL.path\n      if normalizedExpectedPath != normalizedRestoredPath {\n        return nil\n      }\n    }\n\n    if let previousURL = MainFlutterWindow.scopedDownloadDirectoryURL,\n       previousURL.standardizedFileURL.path != url.standardizedFileURL.path {\n      previousURL.stopAccessingSecurityScopedResource()\n      MainFlutterWindow.scopedDownloadDirectoryURL = nil\n    }\n\n    guard url.startAccessingSecurityScopedResource() else {\n      throw NSError(\n        domain: \"pixes.macos.download_path\",\n        code: 2,\n        userInfo: [NSLocalizedDescriptionKey: \"Unable to restore directory access.\"]\n      )\n    }\n    MainFlutterWindow.scopedDownloadDirectoryURL = url\n    return url.path\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    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n\t<key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.network.server</key>\n    <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\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n\t\t39D12B27D56F92B56301B5E6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D9BEDD12A9EEA48BD7AD6F6 /* Pods_Runner.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC10EC2044A3C60003C045;\n\t\t\tremoteInfo = Runner;\n\t\t};\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = \"<group>\"; };\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* pixes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = pixes.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\t33FA50D2B72F3068C99BE42F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.release.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t7D9BEDD12A9EEA48BD7AD6F6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\tA9C44FD6EAE343E5444EA62B /* 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\tF963BF80484FC956BFD713E0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t331C80D2294CF70F00263BE5 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t39D12B27D56F92B56301B5E6 /* 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\t0767BAD717E3548FC133DA04 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tF963BF80484FC956BFD713E0 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t33FA50D2B72F3068C99BE42F /* Pods-Runner.release.xcconfig */,\n\t\t\t\tA9C44FD6EAE343E5444EA62B /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t331C80D6294CF71000263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t331C80D7294CF71000263BE5 /* RunnerTests.swift */,\n\t\t\t);\n\t\t\tpath = RunnerTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t331C80D6294CF71000263BE5 /* RunnerTests */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t\t0767BAD717E3548FC133DA04 /* 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 /* pixes.app */,\n\t\t\t\t331C80D5294CF71000263BE5 /* RunnerTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t7D9BEDD12A9EEA48BD7AD6F6 /* 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\t331C80D4294CF70F00263BE5 /* RunnerTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t331C80D1294CF70F00263BE5 /* Sources */,\n\t\t\t\t331C80D2294CF70F00263BE5 /* Frameworks */,\n\t\t\t\t331C80D3294CF70F00263BE5 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = RunnerTests;\n\t\t\tproductName = RunnerTests;\n\t\t\tproductReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t3958901F24F3B98635BD9E6E /* [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\t89461D3DBDA3C84845C71DB5 /* [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 /* pixes.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 1510;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t331C80D4294CF70F00263BE5 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 14.0;\n\t\t\t\t\t\tTestTargetID = 33CC10EC2044A3C60003C045;\n\t\t\t\t\t};\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t331C80D4294CF70F00263BE5 /* RunnerTests */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t331C80D3294CF70F00263BE5 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\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\t3958901F24F3B98635BD9E6E /* [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\t89461D3DBDA3C84845C71DB5 /* [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\t331C80D1294CF70F00263BE5 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC10EC2044A3C60003C045 /* Runner */;\n\t\t\ttargetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;\n\t\t};\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t331C80DB294CF71000263BE5 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t331C80DC294CF71000263BE5 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t331C80DD294CF71000263BE5 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.pixes.RunnerTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/pixes.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/pixes\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14.6;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = 2TBFGA9C47;\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\tMACOSX_DEPLOYMENT_TARGET = 14.6;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dev.nyne.pixes;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14.6;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEAD_CODE_STRIPPING = YES;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.14.6;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = 2TBFGA9C47;\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\tMACOSX_DEPLOYMENT_TARGET = 14.6;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dev.nyne.pixes;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=macosx*]\" = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tDEVELOPMENT_TEAM = 2TBFGA9C47;\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\tMACOSX_DEPLOYMENT_TARGET = 14.6;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = dev.nyne.pixes;\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget \"RunnerTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t331C80DB294CF71000263BE5 /* Debug */,\n\t\t\t\t331C80DC294CF71000263BE5 /* Release */,\n\t\t\t\t331C80DD294CF71000263BE5 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "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 = \"1510\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"pixes.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 = \"pixes.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <Testables>\n         <TestableReference\n            skipped = \"NO\"\n            parallelizable = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"331C80D4294CF70F00263BE5\"\n               BuildableName = \"RunnerTests.xctest\"\n               BlueprintName = \"RunnerTests\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      enableGPUValidationMode = \"1\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"pixes.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 = \"pixes.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": "macos/RunnerTests/RunnerTests.swift",
    "content": "import FlutterMacOS\nimport Cocoa\nimport XCTest\n\nclass RunnerTests: XCTestCase {\n\n  func testExample() {\n    // If you add code to the Runner application, consider adding tests here.\n    // See https://developer.apple.com/documentation/xctest for more information about using XCTest.\n  }\n\n}\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: pixes\ndescription: \"A new Flutter project.\"\n# The following line prevents the package from being accidentally published to\n# pub.dev using `flutter pub publish`. This is preferred for private packages.\npublish_to: 'none' # Remove this line if you wish to publish to pub.dev\n\n# The following defines the version and build number for your application.\n# A version number is three numbers separated by dots, like 1.2.43\n# followed by an optional build number separated by a +.\n# Both the version and the builder number may be overridden in flutter\n# build by specifying --build-name and --build-number, respectively.\n# In Android, build-name is used as versionName while build-number used as versionCode.\n# Read more about Android versioning at https://developer.android.com/studio/publish/versioning\n# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.\n# Read more about iOS versioning at\n# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html\n# In Windows, build-name is used as the major, minor, and patch parts\n# of the product and file versions while build-number is used as the build suffix.\nversion: 1.2.1+121\n\nenvironment:\n  sdk: '>=3.3.4 <4.0.0'\n  flutter: 3.41.6\n\n# Dependencies specify other packages that your package needs in order to work.\n# To automatically upgrade your package dependencies to the latest versions\n# consider running `flutter pub upgrade --major-versions`. Alternatively,\n# dependencies can be manually updated by changing the version numbers below to\n# the latest version available on pub.dev. To see which dependencies have newer\n# versions available, run `flutter pub outdated`.\ndependencies:\n  flutter:\n    sdk: flutter\n\n\n  # The following adds the Cupertino Icons font to your application.\n  # Use with the CupertinoIcons class for iOS style icons.\n  window_manager: ^0.5.1\n  fluent_ui: ^4.13.0\n  dynamic_color: ^1.7.0\n  dio: ^5.8.0\n  crypto: ^3.0.6\n  intl:\n  path_provider: ^2.1.5\n  url_launcher: ^6.3.1\n  url_launcher_ios: ^6.3.2\n  app_links: ^6.4.0\n  win32_registry: ^2.1.0\n  flutter_staggered_grid_view: ^0.7.0\n  sqlite3: ^2.7.6\n  sqlite3_flutter_libs: any\n  photo_view:\n    git:\n      url: https://github.com/venera-app/photo_view\n      ref: main\n  share_plus: ^10.1.3\n  file_selector: ^1.0.1\n  flutter_file_dialog: ^3.0.2\n  archive: ^4.0.7\n  webview_flutter: ^4.13.0\n  device_info_plus: ^11.5.0\n  image_gallery_saver_plus: ^4.0.1\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n\n  # The \"flutter_lints\" package below contains a set of recommended lints to\n  # encourage good coding practices. The lint set provided by the package is\n  # activated in the `analysis_options.yaml` file located at the root of your\n  # package. See that file for information about deactivating specific lint\n  # rules and activating additional ones.\n  flutter_lints: ^3.0.0\n  flutter_to_arch: ^1.0.1\n  msix: ^3.16.13\n\n# For information on the generic Dart part of this file, see the\n# following page: https://dart.dev/tools/pub/pubspec\n\nflutter_to_arch:\n  name: Pixes\n  icon: debian/gui/pixes.png\n  categories: Utility\n  keywords: Flutter;pixiv;images;\n  url: https://github.com/wgh136/pixes\n  depends:\n    - gtk3\n\n# The following section is specific to Flutter packages.\nflutter:\n\n  # The following line ensures that the Material Icons font is\n  # included with your application, so that you can use the icons in\n  # the material Icons class.\n  uses-material-design: true\n\n  # To add assets to your application, add an assets section, like this:\n  assets:\n    - assets/tr.json\n  #   - images/a_dot_ham.jpeg\n\nmsix_config:\n  display_name: Pixes\n  publisher_display_name: nyne.dev\n  identity_name: dev.nyne.pixes\n  logo_path: ios\\Runner\\Assets.xcassets\\AppIcon.appiconset\\AppIcon@3x.png\n  certificate_path: windows/app.pfx\n  capabilities: internetClient"
  },
  {
    "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 in the flutter_test package. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:pixes/main.dart';\n\nvoid main() {\n  testWidgets('Counter increments smoke test', (WidgetTester tester) async {\n    // Build our app and trigger a frame.\n    await tester.pumpWidget(const MyApp());\n\n    // Verify that our counter starts at 0.\n    expect(find.text('0'), findsOneWidget);\n    expect(find.text('1'), findsNothing);\n\n    // Tap the '+' icon and trigger a frame.\n    await tester.tap(find.byIcon(Icons.add));\n    await tester.pump();\n\n    // Verify that our counter has incremented.\n    expect(find.text('0'), findsNothing);\n    expect(find.text('1'), findsOneWidget);\n  });\n}\n"
  },
  {
    "path": "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": "# Project-level configuration.\ncmake_minimum_required(VERSION 3.14)\nproject(pixes LANGUAGES CXX)\n\n# The name of the executable created for the application. Change this to change\n# the on-disk name of your application.\nset(BINARY_NAME \"pixes\")\n\n# Explicitly opt in to modern CMake behaviors to avoid warnings with recent\n# versions of CMake.\ncmake_policy(VERSION 3.14...3.25)\n\n# Define build configuration option.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n# Define settings for the Profile build mode.\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\n#\n# Be cautious about adding new options here, as plugins use this function by\n# default. In most cases, you should add new options to specific targets instead\n# of modifying this function.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\n# Flutter library and tool build rules.\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build; see runner/CMakeLists.txt.\nadd_subdirectory(\"runner\")\n\n\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# Copy the native assets provided by the build.dart from all packages.\nset(NATIVE_ASSETS_DIR \"${PROJECT_BUILD_DIR}native_assets/windows/\")\ninstall(DIRECTORY \"${NATIVE_ASSETS_DIR}\"\n   DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n   COMPONENT Runtime)\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/build.iss",
    "content": "; Script generated by the Inno Setup Script Wizard.\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!\n\n#define MyAppName \"pixes\"\n#define MyAppVersion \"{{version}}\"\n#define MyAppPublisher \"Nyne\"\n#define MyAppURL \"https://github.com/wgh136/pixes\"\n#define MyAppExeName \"pixes.exe\"\n#define RootPath \"{{root_path}}\"\n\n[Setup]\n; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\nAppId={{88521115-48B7-4AF3-BF49-2BC6AF90B8D3}\nAppName={#MyAppName}\nAppVersion={#MyAppVersion}\n;AppVerName={#MyAppName} {#MyAppVersion}\nAppPublisher={#MyAppPublisher}\nAppPublisherURL={#MyAppURL}\nAppSupportURL={#MyAppURL}\nAppUpdatesURL={#MyAppURL}\nDefaultDirName={autopf}\\{#MyAppName}\nDisableProgramGroupPage=yes\n; Uncomment the following line to run in non administrative install mode (install for current user only.)\n;PrivilegesRequired=lowest\nPrivilegesRequiredOverridesAllowed=dialog\nOutputDir={#RootPath}\\build\\windows\nOutputBaseFilename=pixes-{#MyAppVersion}-windows-installer\nSetupIconFile={#RootPath}\\windows\\runner\\resources\\app_icon.ico\nCompression=lzma\nSolidCompression=yes\nWizardStyle=modern\nArchitecturesInstallIn64BitMode=x64 arm64\nArchitecturesAllowed=x64 arm64\n\n[Languages]\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\nName: \"chinesesimplified\"; MessagesFile: \"{#RootPath}\\windows\\ChineseSimplified.isl\"\n\n[Tasks]\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\n\n[Files]\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\{#MyAppExeName}\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\app_links_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\dynamic_color_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\file_selector_windows_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\flutter_windows.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\screen_retriever_windows_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\share_plus_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\sqlite3.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\sqlite3_flutter_libs_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\url_launcher_windows_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\window_manager_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\flutter_acrylic_plugin.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\nSource: \"{#RootPath}\\build\\windows\\x64\\runner\\Release\\data\\*\"; DestDir: \"{app}\\data\"; Flags: ignoreversion recursesubdirs createallsubdirs\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\n\n[Icons]\nName: \"{autoprograms}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"\nName: \"{autodesktop}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon\n\n[Run]\nFilename: \"{app}\\{#MyAppExeName}\"; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; Flags: nowait postinstall"
  },
  {
    "path": "windows/build.py",
    "content": "import subprocess\nimport os\nimport httpx\n\nfile = open('pubspec.yaml', 'r')\ncontent = file.read()\nfile.close()\n\nsubprocess.run([\"flutter\", \"build\", \"windows\"], shell=True)\n\nversion = str.split(str.split(content, 'version: ')[1], '+')[0]\n\nsubprocess.run([\"tar\", \"-a\", \"-c\", \"-f\", f\"build/windows/pixes-{version}-windows.zip\", \"-C\", \"build/windows/x64/runner/Release\", \"*\"]\n               , shell=True)\n\nissContent = \"\"\nfile = open('windows/build.iss', 'r')\nissContent = file.read()\nnewContent = issContent\nnewContent = newContent.replace(\"{{version}}\", version)\nnewContent = newContent.replace(\"{{root_path}}\", os.getcwd())\nfile.close()\nfile = open('windows/build.iss', 'w')\nfile.write(newContent)\nfile.close()\n\nif not os.path.exists(\"windows/ChineseSimplified.isl\"):\n    # download ChineseSimplified.isl\n    url = \"https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl\"\n    response = httpx.get(url)\n    with open('windows/ChineseSimplified.isl', 'wb') as file:\n        file.write(response.content)\n\nsubprocess.run([\"iscc\", \"windows/build.iss\"], shell=True)\n\nwith open('windows/build.iss', 'w') as file:\n    file.write(issContent)"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "# This file controls Flutter-level build steps. It should not be edited.\ncmake_minimum_required(VERSION 3.14)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# Set fallback configurations for older versions of the flutter tool.\nif (NOT DEFINED FLUTTER_TARGET_PLATFORM)\n  set(FLUTTER_TARGET_PLATFORM \"windows-x64\")\nendif()\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      ${FLUTTER_TARGET_PLATFORM} $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "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 <app_links/app_links_plugin_c_api.h>\n#include <dynamic_color/dynamic_color_plugin_c_api.h>\n#include <file_selector_windows/file_selector_windows.h>\n#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>\n#include <share_plus/share_plus_windows_plugin_c_api.h>\n#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>\n#include <url_launcher_windows/url_launcher_windows.h>\n#include <window_manager/window_manager_plugin.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  AppLinksPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"AppLinksPluginCApi\"));\n  DynamicColorPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"DynamicColorPluginCApi\"));\n  FileSelectorWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"FileSelectorWindows\"));\n  ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"ScreenRetrieverWindowsPluginCApi\"));\n  SharePlusWindowsPluginCApiRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"SharePlusWindowsPluginCApi\"));\n  Sqlite3FlutterLibsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"Sqlite3FlutterLibsPlugin\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n  WindowManagerPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"WindowManagerPlugin\"));\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  app_links\n  dynamic_color\n  file_selector_windows\n  screen_retriever_windows\n  share_plus\n  sqlite3_flutter_libs\n  url_launcher_windows\n  window_manager\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.14)\nproject(runner LANGUAGES CXX)\n\n# Define the application target. To change its name, change BINARY_NAME in the\n# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer\n# work.\n#\n# Any new source files that you add to the application should be added here.\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\n\n# Apply the standard set of build settings. This can be removed for applications\n# that need different build settings.\napply_standard_settings(${BINARY_NAME})\n\n# Add preprocessor definitions for the build version.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION=\\\"${FLUTTER_VERSION}\\\"\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}\")\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}\")\n\n# Disable Windows macros that collide with C++ standard library functions.\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\n\n# Add dependency libraries and include directories. Add any application-specific\n# dependencies here.\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_link_libraries(${BINARY_NAME} PRIVATE \"dwmapi.lib\")\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\n\n# Run the Flutter tool portions of the build. This must not be removed.\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)\n#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD\n#else\n#define VERSION_AS_NUMBER 1,0,0,0\n#endif\n\n#if defined(FLUTTER_VERSION)\n#define VERSION_AS_STRING FLUTTER_VERSION\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.github.wgh136\" \"\\0\"\n            VALUE \"FileDescription\", \"pixes\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"pixes\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2024 com.github.wgh136. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"pixes.exe\" \"\\0\"\n            VALUE \"ProductName\", \"pixes\" \"\\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": "#pragma comment(lib, \"winhttp.lib\")\n#include \"flutter_window.h\"\n#include <flutter/method_channel.h>\n#include <flutter/event_channel.h>\n#include <flutter/event_sink.h>\n#include <flutter/event_stream_handler_functions.h>\n#include <flutter/standard_method_codec.h>\n#include <optional>\n#include <ShlObj.h>\n#include <winhttp.h>\n#include \"flutter/generated_plugin_registrant.h\"\n#include \"utils.h\"\n\nstd::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& mouseEvents = nullptr;\n\nstatic std::string getProxy() {\n    _WINHTTP_CURRENT_USER_IE_PROXY_CONFIG net;\n    WinHttpGetIEProxyConfigForCurrentUser(&net);\n    if (net.lpszProxy == nullptr) {\n        GlobalFree(net.lpszAutoConfigUrl);\n        GlobalFree(net.lpszProxyBypass);\n        return \"No Proxy\";\n    }\n    else {\n        GlobalFree(net.lpszAutoConfigUrl);\n        GlobalFree(net.lpszProxyBypass);\n        return Utf8FromUtf16(net.lpszProxy);\n    }\n}\n\nstatic std::string getPicturePath() {\n    PWSTR picturesPath;\n    HRESULT result = SHGetKnownFolderPath(FOLDERID_Pictures, 0, NULL, &picturesPath);\n    if (SUCCEEDED(result)) {\n        auto res = Utf8FromUtf16(picturesPath);\n        CoTaskMemFree(picturesPath);\n        return res;\n    }\n    else {\n        return \"error\";\n    }\n}\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n\n  const auto channelName = \"pixes/mouse\";\n  flutter::EventChannel<> channel2(\n        flutter_controller_->engine()->messenger(), channelName,\n        &flutter::StandardMethodCodec::GetInstance()\n  );\n  auto eventHandler = std::make_unique<\n      flutter::StreamHandlerFunctions<flutter::EncodableValue>>(\n    [](\n        const flutter::EncodableValue* arguments,\n        std::unique_ptr<flutter::EventSink<flutter::EncodableValue>>&& events){\n            mouseEvents = std::move(events);\n            return nullptr;\n    },\n    [](const flutter::EncodableValue* arguments)\n        -> std::unique_ptr<flutter::StreamHandlerError<flutter::EncodableValue>> {\n            mouseEvents = nullptr;\n            return nullptr;\n  });\n  channel2.SetStreamHandler(std::move(eventHandler));\n\n  const auto pictureFolderChannel = \"pixes/picture_folder\";\n  flutter::MethodChannel<> channel3(\n      flutter_controller_->engine()->messenger(), pictureFolderChannel,\n      &flutter::StandardMethodCodec::GetInstance()\n  );\n  channel3.SetMethodCallHandler([](\n      const flutter::MethodCall<>& call, const std::unique_ptr<flutter::MethodResult<>>& result) {\n          result->Success(getPicturePath());\n      });\n\n  const flutter::MethodChannel<> channel(\n      flutter_controller_->engine()->messenger(), \"pixes/proxy\",\n      &flutter::StandardMethodCodec::GetInstance()\n  );\n  channel.SetMethodCallHandler(\n      [](const flutter::MethodCall<>& call, const std::unique_ptr<flutter::MethodResult<>>& result) {\n          result->Success(getProxy());\n      });\n\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n\n  flutter_controller_->engine()->SetNextFrameCallback([&]() {\n    //this->Show();\n  });\n\n  // Flutter can complete the first frame before the \"show window\" callback is\n  // registered. The following call ensures a frame is pending to ensure the\n  // window is shown. It is a no-op if the first frame hasn't completed yet.\n  flutter_controller_->ForceRedraw();\n\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nvoid mouse_side_button_listener(unsigned int input)\n{\n    if(mouseEvents != nullptr)\n    {\n        mouseEvents->Success(static_cast<int>(input));\n    }\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    UINT button = GET_XBUTTON_WPARAM(wparam);\n    if (button == XBUTTON1 && message == 528)\n    {\n        mouse_side_button_listener(0);\n    }\n    else if (button == XBUTTON2 && message == 528)\n    {\n        mouse_side_button_listener(1);\n    }\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}"
  },
  {
    "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 \"win32_window.h\"\n\n// A window that does nothing but host a Flutter view.\nclass FlutterWindow : public Win32Window {\n public:\n  // Creates a new FlutterWindow hosting a Flutter view running |project|.\n  explicit FlutterWindow(const flutter::DartProject& project);\n  virtual ~FlutterWindow();\n\n protected:\n  // Win32Window:\n  bool OnCreate() override;\n  void OnDestroy() override;\n  LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,\n                         LPARAM const lparam) noexcept override;\n\n private:\n  // The project to run.\n  flutter::DartProject project_;\n\n  // The Flutter instance hosted by this window.\n  std::unique_ptr<flutter::FlutterViewController> flutter_controller_;\n};\n\n#endif  // RUNNER_FLUTTER_WINDOW_H_\n"
  },
  {
    "path": "windows/runner/main.cpp",
    "content": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_window.h\"\n#include \"utils.h\"\n#include \"app_links/app_links_plugin_c_api.h\"\n\nbool SendAppLinkToInstance(const std::wstring& title) {\n  // Find our exact window\n  HWND hwnd = ::FindWindow(L\"FLUTTER_RUNNER_WIN32_WINDOW\", title.c_str());\n\n  if (hwnd) {\n    // Dispatch new link to current window\n    SendAppLink(hwnd);\n\n    // (Optional) Restore our window to front in same state\n    WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };\n    GetWindowPlacement(hwnd, &place);\n\n    switch(place.showCmd) {\n      case SW_SHOWMAXIMIZED:\n          ShowWindow(hwnd, SW_SHOWMAXIMIZED);\n          break;\n      case SW_SHOWMINIMIZED:\n          ShowWindow(hwnd, SW_RESTORE);\n          break;\n      default:\n          ShowWindow(hwnd, SW_NORMAL);\n          break;\n    }\n\n    SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);\n    SetForegroundWindow(hwnd);\n    // END Restore\n\n    // Window has been found, don't create another one.\n    return true;\n  }\n\n  return false;\n}\n\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  if (SendAppLinkToInstance(L\"pixes\")) {\n      return EXIT_SUCCESS;\n  }\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.Create(L\"pixes\", origin, size)) {\n    return EXIT_FAILURE;\n  }\n  window.SetQuitOnClose(true);\n\n  ::MSG msg;\n  while (::GetMessage(&msg, nullptr, 0, 0)) {\n    ::TranslateMessage(&msg);\n    ::DispatchMessage(&msg);\n  }\n\n  ::CoUninitialize();\n  return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "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/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 and Windows 11 -->\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    -1; // remove the trailing null character\n  int input_length = (int)wcslen(utf16_string);\n  std::string utf8_string;\n  if (target_length <= 0 || target_length > utf8_string.max_size()) {\n    return utf8_string;\n  }\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      input_length, utf8_string.data(), 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 <dwmapi.h>\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\n/// Window attribute that enables dark mode window decorations.\n///\n/// Redefined in case the developer's machine has a Windows SDK older than\n/// version 10.0.22000.0.\n/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute\n#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE\n#define DWMWA_USE_IMMERSIVE_DARK_MODE 20\n#endif\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n/// Registry key for app theme preference.\n///\n/// A value of 0 indicates apps should use dark mode. A non-zero or missing\n/// value indicates apps should use light mode.\nconstexpr const wchar_t kGetPreferredBrightnessRegKey[] =\n  L\"Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize\";\nconstexpr const wchar_t kGetPreferredBrightnessRegValue[] = L\"AppsUseLightTheme\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n  }\n  FreeLibrary(user32_module);\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registrar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::Create(const std::wstring& title,\n                         const Point& origin,\n                         const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  UpdateTheme(window);\n\n  return OnCreate();\n}\n\nbool Win32Window::Show() {\n  return ShowWindow(window_handle_, SW_SHOWNORMAL);\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n\n    case WM_DWMCOLORIZATIONCOLORCHANGED:\n      UpdateTheme(hwnd);\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n\nvoid Win32Window::UpdateTheme(HWND const window) {\n  DWORD light_mode;\n  DWORD light_mode_size = sizeof(light_mode);\n  LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,\n                               kGetPreferredBrightnessRegValue,\n                               RRF_RT_REG_DWORD, nullptr, &light_mode,\n                               &light_mode_size);\n\n  if (result == ERROR_SUCCESS) {\n    BOOL enable_dark_mode = light_mode == 0;\n    DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,\n                          &enable_dark_mode, sizeof(enable_dark_mode));\n  }\n}\n"
  },
  {
    "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 a win32 window with |title| that is positioned and sized using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size this function will scale the inputted width and height as\n  // as appropriate for the default monitor. The window is invisible until\n  // |Show| is called. Returns true if the window was created successfully.\n  bool Create(const std::wstring& title, const Point& origin, const Size& size);\n\n  // Show the current window. Returns true if the window was successfully shown.\n  bool Show();\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  // Update the window frame's theme to match the system theme.\n  static void UpdateTheme(HWND const window);\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]