[
  {
    "path": ".github/workflows/Release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n\nenv:\n  go_version: '1.17'\n  flutter_channel: 'stable'\n  GH_TOKEN: ${{ secrets.GH_TOKEN }}\n\njobs:\n\n  ci-pass:\n    name: CI is green\n    runs-on: ubuntu-latest\n    needs:\n      - check_release\n      - build_release_assets\n    steps:\n      - run: exit 0\n\n  check_release:\n    name: Check release\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          repository: ${{ github.event.inputs.repo }}\n          ref: 'master'\n      - name: Checkout submodules\n        run: git submodule update --init --recursive\n      - uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n      - name: Check release\n        run: |\n          cd ci\n          go run ./cmd/check_release\n\n  build_release_assets:\n    name: Build release assets\n    needs:\n      - check_release\n    strategy:\n      fail-fast: false\n      matrix:\n        config:\n          - target: linux\n            host: ubuntu-latest\n            flutter_version: '2.10.3'\n          - target: windows\n            host: windows-latest\n            flutter_version: '2.10.3'\n          - target: macos\n            host: macos-12\n            flutter_version: '2.10.3'\n          - target: ios\n            host: macos-12\n            flutter_version: '3.3.4'\n          - target: android-arm32\n            host: ubuntu-latest\n            flutter_version: '3.3.4'\n          - target: android-arm64\n            host: ubuntu-latest\n            flutter_version: '3.3.4'\n          - target: android-x86_64\n            host: ubuntu-latest\n            flutter_version: '3.3.4'\n\n    runs-on: ${{ matrix.config.host }}\n\n    env:\n      TARGET: ${{ matrix.config.target }}\n      flutter_version: ${{ matrix.config.flutter_version }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Setup golang\n        uses: actions/setup-go@v2\n        with:\n          go-version: ${{ env.go_version }}\n\n      - id: check_asset\n        name: Check asset\n        run: |\n          cd ci\n          go run ./cmd/check_asset\n\n      - name: Setup flutter\n        if: steps.check_asset.outputs.skip_build != 'true'\n        uses: subosito/flutter-action@v2\n        with:\n          channel: ${{ env.flutter_channel }}\n          flutter-version: ${{ env.flutter_version }}\n          architecture: x64\n\n      - name: Setup java (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: actions/setup-java@v3\n        with:\n          java-version: 8\n          distribution: 'zulu'\n\n      - name: Setup android tools (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        uses: maxim-lobanov/setup-android-tools@v1\n        with:\n          packages: |\n            platform-tools\n            platforms;android-32\n            build-tools;30.0.2\n            ndk;22.1.7171670\n\n      - name: Setup msys2 (Windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        uses: msys2/setup-msys2@v2\n        with:\n          install: gcc make\n\n      - name: Install dependencies (Linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        env:\n          ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'\n        run: |\n          curl -JOL https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage\n          chmod a+x appimagetool-x86_64.AppImage\n          mkdir -p ${GITHUB_WORKSPACE}/bin\n          mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool\n          echo ::add-path::${GITHUB_WORKSPACE}/bin\n          sudo apt-get update\n          sudo apt-get install -y libgl1-mesa-dev xorg-dev\n\n      - name: Install hover (desktop)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos')\n        run: |\n          go install github.com/go-flutter-desktop/hover@latest\n\n      - name: Install go mobile (mobile)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'ios' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-arm32' || matrix.config.target == 'android-x86_64' )\n        run: |\n          go install golang.org/x/mobile/cmd/gomobile@latest\n\n      - name: Set-Version (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          cd ci\n          cp version.code.txt ../lib/assets/version.txt\n\n      - name: Upgrade deps version (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && !startsWith(matrix.config.host, 'macos') && startsWith(matrix.config.flutter_version, '3')\n        run: |\n          sed -i \"s/another_xlider: 1.0.1+2/another_xlider: ^1.0.1+2/g\" pubspec.yaml\n          sed -i \"s/flutter_styled_toast: 2.0.0/flutter_styled_toast: ^2.0.0/g\" pubspec.yaml\n          sed -i \"s/filesystem_picker: 2.0.0/filesystem_picker: ^2.0.0/g\" pubspec.yaml\n\n      - name: Build (windows)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'\n        run: |\n          hover build windows\n          cd go\\build\\outputs\\windows-release\n          DEL flutter_engine.pdb\n          DEL flutter_engine.exp\n          DEL flutter_engine.lib\n          Compress-Archive * ../../../../build/build.zip\n\n      - name: Build (macos)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos'\n        run: |\n          hover build darwin-dmg\n          mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg\n\n      - name: Build (linux)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'\n        run: |\n          curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf\n          mkdir -p fonts\n          mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf\n          cat ci/linux_font.yaml >> pubspec.yaml\n          hover build linux-appimage\n          mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage\n\n      - name: Build (ios)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'\n        run: |\n          /usr/libexec/PlistBuddy -c 'Add :application-identifier string niuhuan.nhentai' ios/Runner/Info.plist\n          sh scripts/build-ipa.sh\n\n      - name: Build (android-arm32)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32'\n        run: |\n          sh scripts/build-apk-arm.sh\n\n      - name: Build (android-arm64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64'\n        run: |\n          sh scripts/build-apk-arm64.sh\n\n      - name: Build (android-x86_64)\n        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64'\n        run: |\n          sh scripts/build-apk-x64.sh\n\n      - name: Sign APK (Android)\n        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )\n        env:\n          KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }}\n          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}\n        run: |\n          sh scripts/sign-apk-github-actions.sh\n\n      - name: Upload Asset (All)\n        if: steps.check_asset.outputs.skip_build != 'true'\n        run: |\n          cd ci\n          go run ./cmd/upload_asset\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# Miscellaneous\n*.class\n*.log\n*.pyc\n*.swp\n.DS_Store\n.atom/\n.buildlog/\n.history\n.svn/\n\n# IntelliJ related\n*.iml\n*.ipr\n*.iws\n.idea/\n\n# The .vscode folder contains launch configuration and tasks you configure in\n# VS Code which you may wish to be included in version control, so this line\n# is commented out by default.\n#.vscode/\n\n# Flutter/Dart/Pub related\n**/doc/api/\n**/ios/Flutter/.last_build_id\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.packages\n.pub-cache/\n.pub/\n/build/\n\n# Web related\nlib/generated_plugin_registrant.dart\n\n# Symbolication related\napp.*.symbols\n\n# Obfuscation related\napp.*.map.json\n\n# Android Studio will place build artifacts here\n/android/app/debug\n/android/app/profile\n/android/app/release\n\n/go/mobile/lib/*.jar\n/go/mobile/lib/*.aar\n/go/mobile/lib/*.xcframework/\n\n/lib/assets/version.txt\ndesiredFileName.txt\n"
  },
  {
    "path": ".metadata",
    "content": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrades etc.\n#\n# This file should be version controlled and should not be manually edited.\n\nversion:\n  revision: 18116933e77adc82f80866c928266a5b4f1ed645\n  channel: stable\n\nproject_type: app\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2021-2022 niuhuan\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README-zh.md",
    "content": "# NHENTAI-CROSS\n\n## [English](README.md) | 简体中文\n\n[![license](https://img.shields.io/github/license/niuhuan/nhentai-cross)](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)\n[![releases](https://img.shields.io/github/v/release/niuhuan/nhentai-cross)](https://github.com/niuhuan/nhentai-cross/releases)\n[![downloads](https://img.shields.io/github/downloads/niuhuan/nhentai-cross/total)](https://github.com/niuhuan/nhentai-cross/releases)\n\n一个美观且跨平台的*NHentai客户端*，支持桌面端与移动端(Mac/Windows/Linux/Android/IOS)。\n\n内置DNS拦截器，可以让部分国家和地区免代理使用网络加速，请您在遵守当地法律的情况下使用。(需要在设置中开启）\n\n如果您觉得此软件对您有帮助，可以star进行支持。同时欢迎您issue，一起让软件变得更好。\n\n仓库地址 [https://github.com/niuhuan/nhentai-cross](https://github.com/niuhuan/nhentai-cross)\n\n## 使用前必读\n\n- 根据地区的不同DNS拦截器不一定有效, 请使用科学上网测试本软件的可用性\n\n## 软件截图\n\n#### 漫画列表\n\n![](images/comic_list.png)\n\n#### 漫画详情\n\n![](images/comic_info.png)\n\n#### 漫画阅读器\n\n![](images/comic_reader.png)\n\n## 技术架构\n\n![](images/technologies.png)\n"
  },
  {
    "path": "README.md",
    "content": "# NHENTAI-CROSS\n\n##  English | [简体中文](README-zh.md)\n\n[![license](https://img.shields.io/github/license/niuhuan/nhentai-cross)](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)\n[![releases](https://img.shields.io/github/v/release/niuhuan/nhentai-cross)](https://github.com/niuhuan/nhentai-cross/releases)\n[![downloads](https://img.shields.io/github/downloads/niuhuan/nhentai-cross/total)](https://github.com/niuhuan/nhentai-cross/releases)\n\nA beautiful and cross platform *NHentai Client*. Support desktop and mobile phone (Mac/Windows/Linux/Android/IOS).\n\n## Must readme\n\nThe official website uses the page to prevent DDoS attacks.\n\n## Captures\n\n#### Comic list\n\n![](images/comic_list.png)\n\n#### Comic info\n\n![](images/comic_info.png)\n\n#### Comic reader\n\n![](images/comic_reader.png)\n\n\n## Technical architecture\n\n![](images/technologies.png)\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\n  # https://dart-lang.github.io/linter/lints/index.html.\n  #\n  # Instead of disabling a lint rule for the entire project in the\n  # section below, it can also be suppressed for a single line of code\n  # or a specific dart file by using the `// ignore: name_of_lint` and\n  # `// ignore_for_file: name_of_lint` syntax on the line or in the file\n  # producing the lint.\n  rules:\n    # avoid_print: false  # Uncomment to disable the `avoid_print` rule\n    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule\n\n# Additional information about this file can be found at\n# https://dart.dev/guides/language/analysis-options\n"
  },
  {
    "path": "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": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertiesFile.exists()) {\n    localPropertiesFile.withReader('UTF-8') { reader ->\n        localProperties.load(reader)\n    }\n}\n\ndef flutterRoot = localProperties.getProperty('flutter.sdk')\nif (flutterRoot == null) {\n    throw new GradleException(\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\")\n}\n\ndef flutterVersionCode = localProperties.getProperty('flutter.versionCode')\nif (flutterVersionCode == null) {\n    flutterVersionCode = '1'\n}\n\ndef flutterVersionName = localProperties.getProperty('flutter.versionName')\nif (flutterVersionName == null) {\n    flutterVersionName = '1.0'\n}\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply from: \"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\"\n\nandroid {\n    compileSdkVersion 33\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    kotlinOptions {\n        jvmTarget = '1.8'\n    }\n\n    sourceSets {\n        main.java.srcDirs += 'src/main/kotlin'\n    }\n\n    defaultConfig {\n        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\n        applicationId \"niuhuan.nhentai\"\n        minSdkVersion 19\n        targetSdkVersion 31\n        versionCode flutterVersionCode.toInteger()\n        versionName flutterVersionName\n    }\n\n    buildTypes {\n        release {\n            // TODO: Add your own signing config for the release build.\n            // Signing with the debug keys for now, so `flutter run --release` works.\n            signingConfig signingConfigs.debug\n        }\n    }\n}\n\nflutter {\n    source '../..'\n}\n\ndependencies {\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'\n    implementation fileTree(dir: \"../../go/mobile/lib\", include: [\"*.jar\", \"*.aar\"])\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.READ_INTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_INTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.MANAGE_EXTERNAL_STORAGE\" />\n\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.VIEW\" />\n            <data android:scheme=\"https\" />\n        </intent>\n    </queries>\n\n    <application\n        android:label=\"nhentai\"\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            <!-- Displays an Android View that continues showing the launch screen\n                 Drawable until Flutter paints its first frame, then this splash\n                 screen fades out. A splash screen is useful to avoid any visual\n                 gap between the end of Android's launch screen and the painting of\n                 Flutter's first frame. -->\n            <meta-data\n              android:name=\"io.flutter.embedding.android.SplashScreenDrawable\"\n              android:resource=\"@drawable/launch_background\"\n              />\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n                <!-- Accepts URIs that begin with https://YOUR_HOST -->\n                <data\n                        android:scheme=\"https\"\n                        android:host=\"nhentai.net\"\n                        android:pathPrefix=\"/g\" />\n            </intent-filter>\n        </activity>\n        <!-- Don't delete the meta-data below.\n             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->\n        <meta-data\n            android:name=\"flutterEmbedding\"\n            android:value=\"2\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/kotlin/niuhuan/nhentai/MainActivity.kt",
    "content": "package niuhuan.nhentai\n\nimport android.content.ContentValues\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport android.os.Environment\nimport android.os.Handler\nimport android.os.Looper\nimport android.provider.MediaStore\nimport android.util.DisplayMetrics\nimport android.util.Log\nimport android.view.Display\nimport android.view.KeyEvent\nimport android.view.WindowManager\nimport androidx.annotation.NonNull\nimport androidx.annotation.RequiresApi\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.EventChannel\nimport io.flutter.plugin.common.MethodChannel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.newSingleThreadContext\nimport kotlinx.coroutines.sync.Mutex\nimport mobile.Mobile\nimport java.io.ByteArrayOutputStream\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.nio.file.Files\nimport java.util.concurrent.Executors\n\nclass MainActivity: FlutterActivity() {\n\n    // 为什么换成换成线程池而不继续使用携程 : 下载图片速度慢会占满携程造成拥堵, 接口无法请求\n    private val pool = Executors.newCachedThreadPool { runnable ->\n        Thread(runnable).also { it.isDaemon = true }\n    }\n    private val uiThreadHandler = Handler(Looper.getMainLooper())\n    private val scope = CoroutineScope(newSingleThreadContext(\"worker-scope\"))\n\n    private val notImplementedToken = Any()\n    private fun MethodChannel.Result.withCoroutine(exec: () -> Any?) {\n        pool.submit {\n            try {\n                val data = exec()\n                uiThreadHandler.post {\n                    when (data) {\n                        notImplementedToken -> {\n                            notImplemented()\n                        }\n                        is Unit, null -> {\n                            success(null)\n                        }\n                        else -> {\n                            success(data)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                Log.e(\"Method\", \"Exception\", e)\n                uiThreadHandler.post {\n                    error(\"\", e.message, \"\")\n                }\n            }\n\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.KITKAT)\n    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n        Mobile.initApplication(androidDataLocal())\n        // Method Channel\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"nhentai\").setMethodCallHandler { call, result ->\n            result.withCoroutine {\n                when (call.method) {\n                    \"flatInvoke\" -> {\n                        Mobile.flatInvoke(\n                                call.argument(\"method\")!!,\n                                call.argument(\"params\")!!\n                        )\n                    }\n                    \"saveFileToImage\" -> {\n                        saveFileToImage(call.argument(\"path\")!!)\n                    }\n                    \"androidGetModes\" -> {\n                        modes()\n                    }\n                    \"androidSetMode\" -> {\n                        setMode(call.argument(\"mode\")!!)\n                    }\n                    \"androidGetVersion\" -> Build.VERSION.SDK_INT\n                    // 现在的文件储存路径, 默认路径返回空字符串 \"\"\n                    \"dataLocal\" -> androidDataLocal()\n                    // 迁移到那个地方, 如果是空字符串则迁移会默认位置\n                    \"migrate\" -> androidMigrate(call.argument(\"path\")!!)\n                    // 获取可以迁移数据地址\n                    \"androidGetExtendDirs\" -> androidGetExtendDirs()\n                    \"androidSecureFlag\" -> androidSecureFlag(call.argument(\"flag\")!!)\n                    \"convertToPNG\" -> convertToPNG(call.argument(\"path\")!!)\n                    else -> {\n                        notImplementedToken\n                    }\n                }\n            }\n        }\n\n        //\n        val eventMutex = Mutex()\n        var eventSink: EventChannel.EventSink? = null\n        EventChannel(flutterEngine.dartExecutor.binaryMessenger, \"flatEvent\")\n                .setStreamHandler(object : EventChannel.StreamHandler {\n                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n                        events?.let { events ->\n                            scope.launch {\n                                eventMutex.lock()\n                                eventSink = events\n                                eventMutex.unlock()\n                            }\n                        }\n                    }\n\n                    override fun onCancel(arguments: Any?) {\n                        scope.launch {\n                            eventMutex.lock()\n                            eventSink = null\n                            eventMutex.unlock()\n                        }\n                    }\n                })\n        Mobile.eventNotify { message ->\n            scope.launch {\n                eventMutex.lock()\n                try {\n                    eventSink?.let {\n                        uiThreadHandler.post {\n                            it.success(message)\n                        }\n                    }\n                } finally {\n                    eventMutex.unlock()\n                }\n            }\n        }\n\n        //\n        EventChannel(flutterEngine.dartExecutor.binaryMessenger, \"volume_button\")\n                .setStreamHandler(volumeStreamHandler)\n\n    }\n\n    // save_image\n    private fun saveFileToImage(path: String) {\n        BitmapFactory.decodeFile(path)?.let { bitmap ->\n            val contentValues = ContentValues().apply {\n                put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())\n                put(MediaStore.MediaColumns.MIME_TYPE, \"image/jpeg\")\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one\n                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)\n                    put(MediaStore.MediaColumns.IS_PENDING, 1)\n                }\n            }\n            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)?.let { uri ->\n                contentResolver.openOutputStream(uri)?.use { fos ->\n                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)\n                }\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one\n                    contentValues.clear()\n                    contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)\n                    contentResolver.update(uri, contentValues, null, null)\n                }\n            }\n        }\n    }\n\n    private fun androidDataLocal(): String {\n        val localFile = File(context!!.filesDir.absolutePath, \"data.local\")\n        if (localFile.exists()) {\n            val path = String(FileInputStream(localFile).use { it.readBytes() })\n            if (File(path).isDirectory) {\n                return path\n            }\n        }\n        return context!!.filesDir.absolutePath\n    }\n\n    private fun androidGetExtendDirs(): String {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            val result = context!!.getExternalFilesDirs(\"\")?.toMutableList()?.also {\n                it.add(context!!.filesDir.absoluteFile)\n            }?.joinToString(\"|\")\n            if (result != null) {\n                return result\n            }\n        }\n        throw Exception(\"System version too low\")\n    }\n\n    private fun androidMigrate(path: String) {\n        val current = androidDataLocal()\n        if (current == path) {\n            return\n        }\n        // 删除位置配置文件\n        if (File(current, \"data.local\").exists()) {\n            File(current, \"data.local\").delete()\n        }\n        // 目标位置文件夹不存在就创建，存在则清理\n        val target = File(path)\n        if (!target.exists()) {\n            target.mkdirs()\n        }\n        target.listFiles().forEach { delete(it) }\n        // 移动所有文件夹\n\n        File(current).listFiles().forEach {\n            move(it, File(target, it.name))\n        }\n        val localFile = File(context!!.filesDir.absolutePath, \"data.local\")\n        if (path == context!!.filesDir.absolutePath) {\n            localFile.delete()\n        } else {\n            FileOutputStream(localFile).use { it.write(path.toByteArray()) }\n        }\n    }\n\n    private fun delete(f: File) {\n        f.delete()\n    }\n\n    private fun move(f: File, t: File) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            if (f.isDirectory) {\n                Files.createDirectories(t.toPath())\n                f.listFiles().forEach { move(it, File(t, it.name)) }\n                Files.delete(f.toPath())\n            } else {\n                Files.move(f.toPath(), t.toPath())\n            }\n        } else {\n            if (f.isDirectory) {\n                t.mkdirs()\n                f.listFiles().forEach { move(it, File(t, it.name)) }\n                f.delete()\n            } else {\n                FileOutputStream(t).use { o ->\n                    FileInputStream(f).use { i ->\n                        o.write(i.readBytes())\n                    }\n                }\n                f.delete()\n            }\n        }\n    }\n\n    // fps mods\n    private fun mixDisplay(): Display? {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n            display?.let {\n                return it\n            }\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            windowManager.defaultDisplay?.let {\n                return it\n            }\n        }\n        return null\n    }\n\n    private fun modes(): List<String> {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            mixDisplay()?.let { display ->\n                return display.supportedModes.map { mode ->\n                    mode.toString()\n                }\n            }\n        }\n        return ArrayList()\n    }\n\n    private fun setMode(string: String) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            mixDisplay()?.let { display ->\n                if (string == \"\") {\n                    uiThreadHandler.post {\n                        window.attributes = window.attributes.also { attr ->\n                            attr.preferredDisplayModeId = 0\n                        }\n                    }\n                    return\n                }\n                return display.supportedModes.forEach { mode ->\n                    if (mode.toString() == string) {\n                        uiThreadHandler.post {\n                            window.attributes = window.attributes.also { attr ->\n                                attr.preferredDisplayModeId = mode.modeId\n                            }\n                        }\n                        return\n                    }\n                }\n            }\n        }\n    }\n\n// volume_buttons\n\n    private var volumeEvents: EventChannel.EventSink? = null\n\n    private val volumeStreamHandler = object : EventChannel.StreamHandler {\n\n        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n            volumeEvents = events\n        }\n\n        override fun onCancel(arguments: Any?) {\n            volumeEvents = null\n        }\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {\n        volumeEvents?.let {\n            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {\n                uiThreadHandler.post {\n                    it.success(\"DOWN\")\n                }\n                return true\n            }\n            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {\n                uiThreadHandler.post {\n                    it.success(\"UP\")\n                }\n                return true\n            }\n        }\n        return super.onKeyDown(keyCode, event)\n    }\n\n    private fun androidSecureFlag(flag: Boolean) {\n        uiThreadHandler.post {\n            if (flag) {\n                window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)\n            } else {\n                window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)\n            }\n        }\n    }\n\n    private fun convertToPNG(path: String): ByteArray {\n        BitmapFactory.decodeFile(path)?.let { bitmap ->\n            val maxWidth =\n                    when {\n                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> windowManager.currentWindowMetrics.bounds.width()\n                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {\n                            val displayMetrics = DisplayMetrics()\n                            windowManager.defaultDisplay.getRealMetrics(displayMetrics)\n                            displayMetrics.widthPixels\n                        }\n                        else -> throw Exception(\"not support\")\n                    }\n            if (bitmap.width > maxWidth) {\n                val newHeight = maxWidth * bitmap.height / bitmap.width\n                val newImage = Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true)\n                return compressBitMap(newImage)\n            }\n            return compressBitMap(bitmap)\n        }\n        throw Exception(\"error pic\")\n    }\n\n    private fun compressBitMap(bitmap: Bitmap): ByteArray {\n        val bos = ByteArrayOutputStream()\n        bos.use { bos ->\n            Log.d(\"BITMAP\", bitmap.width.toString())\n            bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)\n        }\n        return bos.toByteArray()\n    }\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/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Light.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:ignore=\"NewApi\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->\n    <style name=\"LaunchTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <!-- Show a splash screen on the activity. Automatically removed when\n             Flutter draws its first frame -->\n        <item name=\"android:windowBackground\">@drawable/launch_background</item>\n    </style>\n    <!-- Theme applied to the Android Window as soon as the process has started.\n         This theme determines the color of the Android Window while your\n         Flutter UI initializes, as well as behind your Flutter UI while its\n         running.\n         \n         This Theme is only used starting with V2 of Flutter's Android embedding. -->\n    <style name=\"NormalTheme\" parent=\"@android:style/Theme.Black.NoTitleBar\">\n        <item name=\"android:windowBackground\">?android:colorBackground</item>\n        <item name=\"android:windowLayoutInDisplayCutoutMode\" tools:ignore=\"NewApi\">shortEdges</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n    <!-- Flutter needs it to communicate with the running application\n         to allow setting breakpoints, to provide hot reload, etc.\n    -->\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n</manifest>\n"
  },
  {
    "path": "android/build.gradle",
    "content": "buildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nrootProject.buildDir = '../build'\nsubprojects {\n    project.buildDir = \"${rootProject.buildDir}/${project.name}\"\n    project.evaluationDependsOn(':app')\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "#Fri Jun 23 08:50:38 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.7-all.zip\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Properties()\n\nassert localPropertiesFile.exists()\nlocalPropertiesFile.withReader(\"UTF-8\") { reader -> properties.load(reader) }\n\ndef flutterSdkPath = properties.getProperty(\"flutter.sdk\")\nassert flutterSdkPath != null, \"flutter.sdk not set in local.properties\"\napply from: \"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle\"\n"
  },
  {
    "path": "ci/cmd/check_asset/main.go",
    "content": "package main\n\nimport (\n        \"ci/commons\"\n        \"encoding/json\"\n        \"fmt\"\n        \"io/ioutil\"\n        \"net/http\"\n        \"os\"\n        \"strings\"\n)\n\nconst owner = \"niuhuan\"\nconst repo = \"nhentai-cross\"\nconst ua = \"niuhuan nhentai-cross ci\"\n\nfunc main() {\n        // get ghToken\n        ghToken := os.Getenv(\"GH_TOKEN\")\n        if ghToken == \"\" {\n                println(\"Env ${GH_TOKEN} is not set\")\n                os.Exit(1)\n        }\n        // get version\n        var version commons.Version\n        codeFile, err := ioutil.ReadFile(\"version.code.txt\")\n        if err != nil {\n                panic(err)\n        }\n        version.Code = strings.TrimSpace(string(codeFile))\n        infoFile, err := ioutil.ReadFile(\"version.info.txt\")\n        if err != nil {\n                panic(err)\n        }\n        version.Info = strings.TrimSpace(string(infoFile))\n        // get target\n        target := os.Getenv(\"TARGET\")\n        if target == \"\" {\n                println(\"Env ${TARGET} is not set\")\n                os.Exit(1)\n        }\n        //\n        var releaseFileName string\n        switch target {\n        case \"macos\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-macos-intel.dmg\", version.Code)\n        case \"ios\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-ios-nosign.ipa\", version.Code)\n        case \"windows\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-windows-x86_64.zip\", version.Code)\n        case \"linux\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-linux-x86_64.AppImage\", version.Code)\n        case \"android-arm32\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-arm32.apk\", version.Code)\n        case \"android-arm64\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-arm64.apk\", version.Code)\n        case \"android-x86_64\":\n                releaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-x86_64.apk\", version.Code)\n        }\n        // get version\n        getReleaseRequest, err := http.NewRequest(\n                \"GET\",\n                fmt.Sprintf(\"https://api.github.com/repos/%v/%v/releases/tags/%v\", owner, repo, version.Code),\n                nil,\n        )\n        if err != nil {\n                panic(err)\n        }\n        getReleaseRequest.Header.Set(\"User-Agent\", ua)\n        getReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n        getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n        if err != nil {\n                panic(err)\n        }\n        defer getReleaseResponse.Body.Close()\n        if getReleaseResponse.StatusCode == 404 {\n                panic(\"NOT FOUND RELEASE\")\n        }\n        buff, err := ioutil.ReadAll(getReleaseResponse.Body)\n        if err != nil {\n                panic(err)\n        }\n        var release commons.Release\n        err = json.Unmarshal(buff, &release)\n        if err != nil {\n                println(string(buff))\n                panic(err)\n        }\n        for _, asset := range release.Assets {\n                if asset.Name == releaseFileName {\n                        println(\"::set-output name=skip_build::true\")\n                        os.Exit(0)\n                }\n        }\n        print(\"::set-output name=skip_build::false\")\n}\n"
  },
  {
    "path": "ci/cmd/check_release/main.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\nconst owner = \"niuhuan\"\nconst repo = \"nhentai-cross\"\nconst ua = \"niuhuan nhentai-cross ci\"\nconst mainBranch = \"master\"\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GH_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GH_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tvar version commons.Version\n\tcodeFile, err := ioutil.ReadFile(\"version.code.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Code = strings.TrimSpace(string(codeFile))\n\tinfoFile, err := ioutil.ReadFile(\"version.info.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Info = strings.TrimSpace(string(infoFile))\n\t// get version\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/%v/releases/tags/%v\", owner, repo, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(nil)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(nil)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\turl := fmt.Sprintf(\"https://api.github.com/repos/%v/%v/releases\", owner, repo)\n\t\tbody := map[string]interface{}{\n\t\t\t\"tag_name\":         version.Code,\n\t\t\t\"target_commitish\": mainBranch,\n\t\t\t\"name\":             version.Code,\n\t\t\t\"body\":             version.Info,\n\t\t}\n\t\tvar buff []byte\n\t\tbuff, err = json.Marshal(&body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tvar createReleaseRequest *http.Request\n\t\tcreateReleaseRequest, err = http.NewRequest(\"POST\", url, bytes.NewBuffer(buff))\n\t\tif err != nil {\n\t\t\tpanic(nil)\n\t\t}\n\t\tcreateReleaseRequest.Header.Set(\"User-Agent\", ua)\n\t\tcreateReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\t\tvar createReleaseResponse *http.Response\n\t\tcreateReleaseResponse, err = http.DefaultClient.Do(createReleaseRequest)\n\t\tif err != nil {\n\t\t\tpanic(nil)\n\t\t}\n\t\tdefer createReleaseResponse.Body.Close()\n\t\tif createReleaseResponse.StatusCode != 201 {\n\t\t\tbuff, err = ioutil.ReadAll(createReleaseResponse.Body)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tprintln(string(buff))\n\t\t\tpanic(\"NOT 201\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "ci/cmd/upload_asset/main.go",
    "content": "package main\n\nimport (\n\t\"ci/commons\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n)\n\nconst owner = \"niuhuan\"\nconst repo = \"nhentai-cross\"\nconst ua = \"niuhuan nhentai-cross ci\"\n\nfunc main() {\n\t// get ghToken\n\tghToken := os.Getenv(\"GH_TOKEN\")\n\tif ghToken == \"\" {\n\t\tprintln(\"Env ${GH_TOKEN} is not set\")\n\t\tos.Exit(1)\n\t}\n\t// get version\n\tvar version commons.Version\n\tcodeFile, err := ioutil.ReadFile(\"version.code.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Code = strings.TrimSpace(string(codeFile))\n\tinfoFile, err := ioutil.ReadFile(\"version.info.txt\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Info = strings.TrimSpace(string(infoFile))\n\t// get target\n\ttarget := os.Getenv(\"TARGET\")\n\tif target == \"\" {\n\t\tprintln(\"Env ${TARGET} is not set\")\n\t\tos.Exit(1)\n\t}\n\t//\n\tvar releaseFilePath string\n\tvar releaseFileName string\n\tvar contentType string\n\tvar contentLength int64\n\tswitch target {\n\tcase \"macos\":\n\t\treleaseFilePath = \"build/build.dmg\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-macos-intel.dmg\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"ios\":\n\t\treleaseFilePath = \"build/nosign.ipa\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-ios-nosign.ipa\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"windows\":\n\t\treleaseFilePath = \"build/build.zip\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-windows-x86_64.zip\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"linux\":\n\t\treleaseFilePath = \"build/build.AppImage\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-linux-x86_64.AppImage\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-arm32\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-arm32.apk\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-arm64\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-arm64.apk\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\tcase \"android-x86_64\":\n\t\treleaseFilePath = \"build/app/outputs/flutter-apk/app-release.apk\"\n\t\treleaseFileName = fmt.Sprintf(\"nhentai-cross-%v-android-x86_64.apk\", version.Code)\n\t\tcontentType = \"application/octet-stream\"\n\t}\n\treleaseFilePath = path.Join(\"..\", releaseFilePath)\n\tinfo, err := os.Stat(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tcontentLength = info.Size()\n\t// get version\n\tgetReleaseRequest, err := http.NewRequest(\n\t\t\"GET\",\n\t\tfmt.Sprintf(\"https://api.github.com/repos/%v/%v/releases/tags/%v\", owner, repo, version.Code),\n\t\tnil,\n\t)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgetReleaseRequest.Header.Set(\"User-Agent\", ua)\n\tgetReleaseRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tgetReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer getReleaseResponse.Body.Close()\n\tif getReleaseResponse.StatusCode == 404 {\n\t\tpanic(\"NOT FOUND RELEASE\")\n\t}\n\tbuff, err := ioutil.ReadAll(getReleaseResponse.Body)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar release commons.Release\n\terr = json.Unmarshal(buff, &release)\n\tif err != nil {\n\t\tprintln(string(buff))\n\t\tpanic(err)\n\t}\n\tfile, err := os.Open(releaseFilePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer file.Close()\n\tuploadUrl := fmt.Sprintf(\"https://uploads.github.com/repos/%v/%v/releases/%v/assets?name=%v\", owner, repo, release.Id, releaseFileName)\n\tuploadRequest, err := http.NewRequest(\"POST\", uploadUrl, file)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tuploadRequest.Header.Set(\"User-Agent\", ua)\n\tuploadRequest.Header.Set(\"Authorization\", \"token \"+ghToken)\n\tuploadRequest.Header.Set(\"Content-Type\", contentType)\n\tuploadRequest.ContentLength = contentLength\n\tuploadResponse, err := http.DefaultClient.Do(uploadRequest)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif uploadResponse.StatusCode != 201 {\n\t\tbuff, err = ioutil.ReadAll(uploadResponse.Body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tprintln(string(buff))\n\t\tpanic(\"NOT 201\")\n\t}\n}\n"
  },
  {
    "path": "ci/commons/types.go",
    "content": "package commons\n\nimport \"time\"\n\ntype Version struct {\n\tCode string `json:\"code\"`\n\tInfo string `json:\"info\"`\n}\n\ntype Release struct {\n\tUrl             string    `json:\"url\"`\n\tHtmlUrl         string    `json:\"html_url\"`\n\tAssetsUrl       string    `json:\"assets_url\"`\n\tUploadUrl       string    `json:\"upload_url\"`\n\tTarballUrl      string    `json:\"tarball_url\"`\n\tZipballUrl      string    `json:\"zipball_url\"`\n\tDiscussionUrl   string    `json:\"discussion_url\"`\n\tId              int       `json:\"id\"`\n\tNodeId          string    `json:\"node_id\"`\n\tTagName         string    `json:\"tag_name\"`\n\tTargetCommitish string    `json:\"target_commitish\"`\n\tName            string    `json:\"name\"`\n\tBody            string    `json:\"body\"`\n\tDraft           bool      `json:\"draft\"`\n\tPrerelease      bool      `json:\"prerelease\"`\n\tCreatedAt       time.Time `json:\"created_at\"`\n\tPublishedAt     time.Time `json:\"published_at\"`\n\tAuthor          struct {\n\t\tLogin             string `json:\"login\"`\n\t\tId                int    `json:\"id\"`\n\t\tNodeId            string `json:\"node_id\"`\n\t\tAvatarUrl         string `json:\"avatar_url\"`\n\t\tGravatarId        string `json:\"gravatar_id\"`\n\t\tUrl               string `json:\"url\"`\n\t\tHtmlUrl           string `json:\"html_url\"`\n\t\tFollowersUrl      string `json:\"followers_url\"`\n\t\tFollowingUrl      string `json:\"following_url\"`\n\t\tGistsUrl          string `json:\"gists_url\"`\n\t\tStarredUrl        string `json:\"starred_url\"`\n\t\tSubscriptionsUrl  string `json:\"subscriptions_url\"`\n\t\tOrganizationsUrl  string `json:\"organizations_url\"`\n\t\tReposUrl          string `json:\"repos_url\"`\n\t\tEventsUrl         string `json:\"events_url\"`\n\t\tReceivedEventsUrl string `json:\"received_events_url\"`\n\t\tType              string `json:\"type\"`\n\t\tSiteAdmin         bool   `json:\"site_admin\"`\n\t} `json:\"author\"`\n\tAssets []struct {\n\t\tUrl                string    `json:\"url\"`\n\t\tBrowserDownloadUrl string    `json:\"browser_download_url\"`\n\t\tId                 int       `json:\"id\"`\n\t\tNodeId             string    `json:\"node_id\"`\n\t\tName               string    `json:\"name\"`\n\t\tLabel              string    `json:\"label\"`\n\t\tState              string    `json:\"state\"`\n\t\tContentType        string    `json:\"content_type\"`\n\t\tSize               int       `json:\"size\"`\n\t\tDownloadCount      int       `json:\"download_count\"`\n\t\tCreatedAt          time.Time `json:\"created_at\"`\n\t\tUpdatedAt          time.Time `json:\"updated_at\"`\n\t\tUploader           struct {\n\t\t\tLogin             string `json:\"login\"`\n\t\t\tId                int    `json:\"id\"`\n\t\t\tNodeId            string `json:\"node_id\"`\n\t\t\tAvatarUrl         string `json:\"avatar_url\"`\n\t\t\tGravatarId        string `json:\"gravatar_id\"`\n\t\t\tUrl               string `json:\"url\"`\n\t\t\tHtmlUrl           string `json:\"html_url\"`\n\t\t\tFollowersUrl      string `json:\"followers_url\"`\n\t\t\tFollowingUrl      string `json:\"following_url\"`\n\t\t\tGistsUrl          string `json:\"gists_url\"`\n\t\t\tStarredUrl        string `json:\"starred_url\"`\n\t\t\tSubscriptionsUrl  string `json:\"subscriptions_url\"`\n\t\t\tOrganizationsUrl  string `json:\"organizations_url\"`\n\t\t\tReposUrl          string `json:\"repos_url\"`\n\t\t\tEventsUrl         string `json:\"events_url\"`\n\t\t\tReceivedEventsUrl string `json:\"received_events_url\"`\n\t\t\tType              string `json:\"type\"`\n\t\t\tSiteAdmin         bool   `json:\"site_admin\"`\n\t\t} `json:\"uploader\"`\n\t} `json:\"assets\"`\n}"
  },
  {
    "path": "ci/go.mod",
    "content": "module \"ci\""
  },
  {
    "path": "ci/linux_font.yaml",
    "content": "\n  fonts:\n  - family: Roboto\n    fonts:\n      - asset: fonts/Roboto.ttf\n\n"
  },
  {
    "path": "ci/version.code.txt",
    "content": "v0.0.9"
  },
  {
    "path": "ci/version.info.txt",
    "content": "- Cross DDoS (only phone)\n- 通过ddos (仅手机端)\n"
  },
  {
    "path": "go/.gitignore",
    "content": "build\n.last_goflutter_check\n.last_go-flutter_check\n"
  },
  {
    "path": "go/cmd/init.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"nhentai/nhentai\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n)\n\nfunc init() {\n\tnhentai.InitNHentai(documentPath())\n}\n\nfunc documentPath() string {\n\tapplicationDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\t// applicationDir = winDocumentPath.Join(applicationDir, \"AppData\", \"Roaming\", \"nhentai\")\n\t\tfile, err := exec.LookPath(os.Args[0])\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\twinDocumentPath, err := filepath.Abs(file)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\ti := strings.LastIndex(winDocumentPath, \"/\")\n\t\tif i < 0 {\n\t\t\ti = strings.LastIndex(winDocumentPath, \"\\\\\")\n\t\t}\n\t\tif i < 0 {\n\t\t\tpanic(errors.New(\" can't find \\\"/\\\" or \\\"\\\\\\\"\"))\n\t\t}\n\t\tapplicationDir = path.Join(winDocumentPath[0:i+1], \"data\")\n\tcase \"darwin\":\n\t\tapplicationDir = path.Join(applicationDir, \"Library\", \"Application Support\", \"nhentai\")\n\tcase \"linux\":\n\t\tapplicationDir = path.Join(applicationDir, \".nhentai\")\n\tdefault:\n\t\tpanic(errors.New(\"not supported system\"))\n\t}\n\tif _, err = os.Stat(applicationDir); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(applicationDir, os.FileMode(0700))\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn applicationDir\n}\n"
  },
  {
    "path": "go/cmd/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/go-flutter-desktop/go-flutter\"\n\t\"github.com/pkg/errors\"\n\t\"image\"\n\t_ \"image/gif\"\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\t\"nhentai/nhentai/database/properties\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// vmArguments may be set by hover at compile-time\nvar vmArguments string\n\nfunc main() {\n\t// DO NOT EDIT, add options in options.go\n\tmainOptions := []flutter.Option{\n\t\tflutter.OptionVMArguments(strings.Split(vmArguments, \";\")),\n\t\tflutter.WindowIcon(iconProvider),\n\t}\n\t// 窗口初始化大小的处理\n\twidthStr, _ := properties.LoadProperty(\"window_width\", \"600\")\n\theightStr, _ := properties.LoadProperty(\"window_height\", \"900\")\n\twidth, _ := strconv.Atoi(widthStr)\n\theight, _ := strconv.Atoi(heightStr)\n\tif width <= 0 {\n\t\twidth = 600\n\t}\n\tif height <= 0 {\n\t\theight = 900\n\t}\n\tvar runOptions []flutter.Option\n\trunOptions = append(runOptions, flutter.WindowInitialDimensions(width, height))\n\tfullScreen, _ := properties.LoadBoolProperty(\"full_screen\", false)\n\tif fullScreen {\n\t\trunOptions = append(runOptions, flutter.WindowMode(flutter.WindowModeMaximize))\n\t}\n\t// ------\n\terr := flutter.Run(append(append(runOptions, options...), mainOptions...)...)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc iconProvider() ([]image.Image, error) {\n\texecPath, err := os.Executable()\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to resolve executable path\")\n\t}\n\texecPath, err = filepath.EvalSymlinks(execPath)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to eval symlinks for executable path\")\n\t}\n\timgFile, err := os.Open(filepath.Join(filepath.Dir(execPath), \"assets\", \"icon.png\"))\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to open assets/icon.png\")\n\t}\n\timg, _, err := image.Decode(imgFile)\n\tif err != nil {\n\t\treturn nil, errors.Wrap(err, \"failed to decode image\")\n\t}\n\treturn []image.Image{img}, nil\n}\n"
  },
  {
    "path": "go/cmd/options.go",
    "content": "package main\n\nimport (\n\t\"github.com/go-flutter-desktop/go-flutter\"\n\t\"github.com/go-flutter-desktop/plugins/url_launcher\"\n\t\"github.com/miguelpruivo/flutter_file_picker/go\"\n)\n\nvar options = []flutter.Option{\n\tflutter.AddPlugin(&file_picker.FilePickerPlugin{}),\n\tflutter.AddPlugin(&url_launcher.UrlLauncherPlugin{}),\n\tflutter.AddPlugin(&Plugin{}),\n}\n"
  },
  {
    "path": "go/cmd/plugin.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"github.com/go-flutter-desktop/go-flutter/plugin\"\n\t\"github.com/go-gl/glfw/v3.3/glfw\"\n\t\"nhentai/nhentai\"\n\t\"nhentai/nhentai/database/properties\"\n\t\"strconv\"\n)\n\ntype Plugin struct {\n}\n\nfunc (p *Plugin) InitPlugin(messenger plugin.BinaryMessenger) error {\n\tchannel := plugin.NewMethodChannel(messenger, \"nhentai\", plugin.StandardMethodCodec{})\n\tchannel.HandleFunc(\"flatInvoke\", func(arguments interface{}) (interface{}, error) {\n\t\tif argumentsMap, ok := arguments.(map[interface{}]interface{}); ok {\n\t\t\tif method, ok := argumentsMap[\"method\"].(string); ok {\n\t\t\t\tif params, ok := argumentsMap[\"params\"].(string); ok {\n\t\t\t\t\treturn nhentai.FlatInvoke(method, params)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn \"\", errors.New(\"method not found (nhentai channel)\")\n\t})\n\treturn nil\n}\n\nfunc (p *Plugin) InitPluginGLFW(window *glfw.Window) error {\n\twindow.SetSizeCallback(func(w *glfw.Window, width int, height int) {\n\t\tgo func() {\n\t\t\tproperties.SaveProperty(\"window_width\", strconv.Itoa(width))\n\t\t\tproperties.SaveProperty(\"window_height\", strconv.Itoa(height))\n\t\t}()\n\t})\n\twindow.SetMaximizeCallback(func(w *glfw.Window, iconified bool) {\n\t\tgo func() {\n\t\t\tproperties.SaveProperty(\"full_screen\", strconv.FormatBool(iconified))\n\t\t}()\n\t})\n\treturn nil\n}\n"
  },
  {
    "path": "go/go.mod",
    "content": "module nhentai\n\ngo 1.17\n\nrequire (\n\tgithub.com/go-flutter-desktop/go-flutter v0.44.0\n\tgithub.com/go-flutter-desktop/plugins/url_launcher v0.1.3\n\tgithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958\n\tgithub.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35\n\tgithub.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d\n\tgithub.com/pkg/errors v0.9.1\n\tgolang.org/x/image v0.0.0-20220321031419-a8550c1d254a\n\tgorm.io/driver/sqlite v1.3.1\n\tgorm.io/gorm v1.23.4\n)\n\nrequire (\n\tgithub.com/PuerkitoBio/goquery v1.8.0 // indirect\n\tgithub.com/Xuanwo/go-locale v1.1.0 // indirect\n\tgithub.com/andybalholm/cascadia v1.3.1 // indirect\n\tgithub.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7 // indirect\n\tgithub.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 // indirect\n\tgithub.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.4 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/mattn/go-sqlite3 v1.14.9 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgolang.org/x/mobile v0.0.0-20221012134814-c746ac228303 // indirect\n\tgolang.org/x/mod v0.4.2 // indirect\n\tgolang.org/x/net v0.0.0-20220615171555-694bf12d69de // indirect\n\tgolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect\n\tgolang.org/x/text v0.3.7 // indirect\n\tgolang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect\n\tgolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n)\n"
  },
  {
    "path": "go/go.sum",
    "content": "github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=\ngithub.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=\ngithub.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg=\ngithub.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs=\ngithub.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=\ngithub.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7 h1:qA8Mdjwrlv/r/aMqArqO0IMHUiy6ApdW4+8DtKr7PvA=\ngithub.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU=\ngithub.com/go-flutter-desktop/go-flutter v0.30.0/go.mod h1:NCryd/AqiRbYSd8pMzQldYkgH1tZIFGt2ToUghZcWGA=\ngithub.com/go-flutter-desktop/go-flutter v0.44.0 h1:0ab9WP2qxZCy2egXyjD6T7cBkORz2f9/LkETJ0F2PN8=\ngithub.com/go-flutter-desktop/go-flutter v0.44.0/go.mod h1:Q1oxIBX2aX6rC+bWhN9aC4C05uJLXfnSNN2aNKNyBUM=\ngithub.com/go-flutter-desktop/plugins/url_launcher v0.1.3 h1:28/xUmm3ZMqLtnlJF7zB9ZdJmF74h+cYKt65evU0pnQ=\ngithub.com/go-flutter-desktop/plugins/url_launcher v0.1.3/go.mod h1:R8dUEKFt7nuCgeJ9UtZ3Apg1Ib9i20GDsVJl/7uma5E=\ngithub.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=\ngithub.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 h1:1Zi56D0LNfvkzM+BdoxKryvUEdyWO7LP8oRT+oSYJW0=\ngithub.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f h1:TyqzGm2z1h3AGhjOoRYyeLcW4WlW81MDQkWa+rx/000=\ngithub.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=\ngithub.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=\ngithub.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=\ngithub.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=\ngithub.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35 h1:LoiQwzAk4gm6SSechIAywbcaBhkc/NABfLJ94JxOU8Y=\ngithub.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35/go.mod h1:csuW+TFyYKtiUwNvcvhcpyX4quPI7Pvv0SUogdqCW4I=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d h1:+V2zonBI5M7UmqbBk1szY6RaGx5RMx71hO0TrQ3WLZ0=\ngithub.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d/go.mod h1:4Vj8GRJFi4AAQgz3dRefIcPezroV0/a5Bu2oTgashQc=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=\ngithub.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg=\ngolang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk=\ngolang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=\ngolang.org/x/mobile v0.0.0-20221012134814-c746ac228303 h1:K4fp1rDuJBz0FCPAWzIJwnzwNEM7S6yobdZzMrZ/Zws=\ngolang.org/x/mobile v0.0.0-20221012134814-c746ac228303/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220615171555-694bf12d69de h1:ogOG2+P6LjO2j55AkRScrkB2BFpd+Z8TY2wcM0Z3MGo=\ngolang.org/x/net v0.0.0-20220615171555-694bf12d69de/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 h1:YuekqPskqwCCPM79F1X5Dhv4ezTCj+Ki1oNwiafxkA0=\ngolang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=\ngorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=\ngorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=\ngorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=\ngorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=\n"
  },
  {
    "path": "go/hover.yaml",
    "content": "#application-name: \"nhentai\" # Uncomment to modify this value.\n#executable-name: \"nhentai\" # Uncomment to modify this value. Only lowercase a-z, numbers, underscores and no spaces\n#package-name: \"nhentai\" # Uncomment to modify this value. Only lowercase a-z, numbers and no underscores or spaces\norganization-name: \"com.nhentai\"\nlicense: \"\" # MANDATORY: Fill in your SPDX license name: https://spdx.org/licenses\ntarget: lib/main_desktop.dart\n# opengl: \"none\" # Uncomment this line if you have trouble with your OpenGL driver (https://github.com/go-flutter-desktop/go-flutter/issues/272)\ndocker: false\nengine-version: \"\" # change to a engine version commit\n"
  },
  {
    "path": "go/mobile/bind-android-debug.sh",
    "content": "gomobile bind -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./\n"
  },
  {
    "path": "go/mobile/bind-android.sh",
    "content": "gomobile bind -target=android/arm -o lib/Mobile.aar ./\n"
  },
  {
    "path": "go/mobile/bind-ios-debug.sh",
    "content": "gomobile bind -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "go/mobile/bind-ios.sh",
    "content": "gomobile bind -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "go/mobile/lib/.keep",
    "content": ""
  },
  {
    "path": "go/mobile/mobile.go",
    "content": "package mobile\n\nimport (\n\t\"errors\"\n\t\"nhentai/nhentai\"\n\t\"nhentai/nhentai/constant\"\n\t\"os\"\n\t\"path\"\n)\n\nfunc Migration(source, target string) {\n\tconstant.ObtainDir(source)\n\tconstant.ObtainDir(target)\n\tcacheDIr := path.Join(source, \"cache\")\n\tdownloadDir := path.Join(source, \"download\")\n\tdatabaseDir := path.Join(source, \"database\")\n\n\tcacheE, _ := exists(cacheDIr)\n\tdownloadE, _ := exists(downloadDir)\n\tdatabaseE, _ := exists(databaseDir)\n\n\tif cacheE {\n\t\tos.Rename(cacheDIr, path.Join(target, \"cache\"))\n\t}\n\n\tif downloadE {\n\t\tos.Rename(downloadDir, path.Join(target, \"download\"))\n\t}\n\n\tif databaseE {\n\t\tos.Rename(databaseDir, path.Join(target, \"database\"))\n\t}\n\n}\n\nfunc exists(name string) (bool, error) {\n\t_, err := os.Stat(name)\n\tif err == nil {\n\t\treturn true, nil\n\t}\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn false, nil\n\t}\n\treturn false, err\n}\n\nfunc InitApplication(application string) {\n\tnhentai.InitNHentai(application)\n}\n\nfunc FlatInvoke(method string, params string) (string, error) {\n\treturn nhentai.FlatInvoke(method, params)\n}\n\nfunc EventNotify(notify EventNotifyHandler) {\n\t// controller.EventNotify = notify.OnNotify\n}\n\ntype EventNotifyHandler interface {\n\tOnNotify(message string)\n}\n"
  },
  {
    "path": "go/nhentai/client.go",
    "content": "package nhentai\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"fmt\"\n\tsource \"github.com/niuhuan/nhentai-go\"\n\t\"io/ioutil\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"nhentai/nhentai/database/active\"\n\t\"nhentai/nhentai/database/cache\"\n\t\"nhentai/nhentai/database/properties\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"time\"\n)\n\nvar dialer = &net.Dialer{\n\tTimeout:   30 * time.Second,\n\tKeepAlive: 30 * time.Second,\n}\n\nvar client = &source.Client{\n\tClient: http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tProxy:                 nil,\n\t\t\tTLSHandshakeTimeout:   time.Second * 20,\n\t\t\tExpectContinueTimeout: time.Second * 20,\n\t\t\tResponseHeaderTimeout: time.Second * 20,\n\t\t\tIdleConnTimeout:       time.Second * 20,\n\t\t},\n\t},\n}\n\nfunc initClient() {\n\tproxy, _ := properties.LoadProperty(\"proxy\", \"\")\n\tsetProxy(proxy)\n}\n\nfunc setProxy(proxyUrlString string) (string, error) {\n\tvar proxy func(_ *http.Request) (*url.URL, error)\n\tif proxyUrlString != \"\" {\n\t\tproxyUrl, proxyErr := url.Parse(proxyUrlString)\n\t\tif proxyErr != nil {\n\t\t\treturn \"\", proxyErr\n\t\t}\n\t\tproxy = func(_ *http.Request) (*url.URL, error) {\n\t\t\treturn proxyUrl, proxyErr\n\t\t}\n\t}\n\tproperties.SaveProperty(\"proxy\", proxyUrlString)\n\tclient.Client.Transport.(*http.Transport).Proxy = proxy\n\treturn \"\", nil\n}\n\nfunc getProxy(_ string) (string, error) {\n\treturn properties.LoadProperty(\"proxy\", \"\")\n}\n\nfunc comics(params string) (string, error) {\n\tpage, err := strconv.Atoi(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cacheable(\n\t\tfmt.Sprintf(\"COMICS$%d\", page),\n\t\ttime.Hour,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn client.Comics(page)\n\t\t},\n\t)\n}\n\nfunc comicsByTagName(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tTagName string `json:\"tag_name\"`\n\t\tPage    int    `json:\"page\"`\n\t}\n\terr := json.Unmarshal([]byte(params), &paramsStruct)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cacheable(\n\t\tfmt.Sprintf(\"COMICS_BY_TAG$%s$%d\", paramsStruct.TagName, paramsStruct.Page),\n\t\ttime.Hour,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn client.ComicsByTagName(paramsStruct.TagName, paramsStruct.Page)\n\t\t},\n\t)\n}\n\nfunc comicsBySearchRaw(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tRaw  string `json:\"raw\"`\n\t\tPage int    `json:\"page\"`\n\t}\n\terr := json.Unmarshal([]byte(params), &paramsStruct)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cacheable(\n\t\tfmt.Sprintf(\"COMICS_BY_SEARCH_RAW$%s$%d\", paramsStruct.Raw, paramsStruct.Page),\n\t\ttime.Hour,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn client.ComicByRawCondition(paramsStruct.Raw, paramsStruct.Page)\n\t\t},\n\t)\n}\n\nfunc comicInfo(params string) (string, error) {\n\tid, err := strconv.Atoi(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cacheable(\n\t\tfmt.Sprintf(\"COMIC_INFO$%d\", id),\n\t\ttime.Hour,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn client.ComicInfo(id)\n\t\t},\n\t)\n}\n\nfunc cacheImageByUrlPath(url string) (string, error) {\n\tlock := HashLock(url)\n\tlock.Lock()\n\tdefer lock.Unlock()\n\t// downloadPage\n\tp1 := active.FindDownloadPageByUrl(url)\n\tif p1 != nil {\n\t\treturn path.Join(downloadPath, p1.DownloadLocalPath), nil\n\t}\n\t// downloadPageThumb\n\tp2 := active.FindDownloadPageThumbByUrl(url)\n\tif p2 != nil {\n\t\treturn path.Join(downloadPath, p2.DownloadLocalPath), nil\n\t}\n\t// downloadCover\n\tp3 := active.FindDownloadCoverByUrl(url)\n\tif p3 != nil {\n\t\treturn path.Join(downloadPath, p3.DownloadLocalPath), nil\n\t}\n\t// downloadCoverThumb\n\tp4 := active.FindDownloadCoverThumbByUrl(url)\n\tif p4 != nil {\n\t\treturn path.Join(downloadPath, p4.DownloadLocalPath), nil\n\t}\n\t// cache\n\tcache := cache.FindImageCache(url)\n\t// no cache\n\tif cache == nil {\n\t\tremote, err := decodeAndSaveImage(url)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tcache = remote\n\t}\n\treturn cacheImagePath(cache.LocalPath), nil\n}\n\nfunc decodeAndSaveImage(url string) (*cache.ImageCache, error) {\n\tbuff, err := decodeFromUrl(url)\n\tif err != nil {\n\t\tprintln(fmt.Sprintf(\"decode error : %s : %s\", url, err.Error()))\n\t\treturn nil, err\n\t}\n\tlocal := fmt.Sprintf(\"%x\", md5.Sum([]byte(url)))\n\treal := cacheImagePath(local)\n\terr = ioutil.WriteFile(\n\t\treal,\n\t\tbuff, os.FileMode(0600),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\timageCache := cache.ImageCache{\n\t\tUrl:       url,\n\t\tLocalPath: local,\n\t}\n\terr = cache.SaveImageCache(&imageCache)\n\treturn &imageCache, err\n}\n"
  },
  {
    "path": "go/nhentai/common.go",
    "content": "package nhentai\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"image\"\n\t\"image/jpeg\"\n\t\"io/ioutil\"\n\t\"nhentai/nhentai/database/cache\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n)\n\n// PING\n\n// @\n// \"104.27.195.88:443\"\n\n// t.nhentai.net\n//185.177.127.78 (3115417422)\n//185.177.127.77 (3115417421)\n//23.237.126.122 (401440378)\n\n// t5.nhentai.net\n// 185.177.127.77 (3115417421)\n\n// i.nhentai.net\n//185.177.127.78 (3115417422)\n//23.237.126.122 (401440378)\n//185.177.127.77 (3115417421)\n\nfunc availableWebAddresses(_ string) (string, error) {\n\treturn serialize([]string{\n\t\t\"104.21.66.123:443\",\n\t\t\"172.67.159.231:443\",\n\t}, nil)\n}\n\nfunc availableImgAddresses(_ string) (string, error) {\n\treturn serialize([]string{\n\t\t\"185.107.44.3:443\",\n\t\t\"185.177.127.78:443\",\n\t\t\"185.177.127.77:443\",\n\t}, nil)\n}\n\nfunc cacheable(key string, expire time.Duration, reload func() (interface{}, error)) (string, error) {\n\t// CACHE\n\tcacheable, err := cache.LoadCache(key, expire)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif cacheable != \"\" {\n\t\treturn cacheable, nil\n\t}\n\t// RELOAD\n\tcacheable, err = serialize(reload())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// push to cache (if cache error )\n\t_ = cache.SaveCache(key, cacheable)\n\t// return\n\treturn cacheable, nil\n}\n\n// 将interface序列化成字符串, 方便与flutter通信\nfunc serialize(point interface{}, err error) (string, error) {\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuff, err := json.Marshal(point)\n\treturn string(buff), nil\n}\n\nfunc convertImageToJPEG100(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tPath string `json:\"path\"`\n\t\tDir  string `json:\"dir\"`\n\t}\n\terr := json.Unmarshal([]byte(params), &paramsStruct)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tbuff, err := ioutil.ReadFile(paramsStruct.Path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treader := bytes.NewReader(buff)\n\ti, _, err := image.Decode(reader)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tto := path.Join(paramsStruct.Dir, path.Base(paramsStruct.Path)+\".jpg\")\n\tstream, err := os.Create(to)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer stream.Close()\n\treturn \"\", jpeg.Encode(stream, i, &jpeg.Options{Quality: 100})\n}\n"
  },
  {
    "path": "go/nhentai/constant/constant.go",
    "content": "package constant\n\nimport (\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/logger\"\n\t\"gorm.io/gorm/schema\"\n\t\"hash/fnv\"\n\t\"os\"\n\t\"sync\"\n)\n\nvar (\n\tCreateDirMode  = os.FileMode(0700)\n\tCreateFileMode = os.FileMode(0600)\n\tGormConfig     = &gorm.Config{\n\t\tLogger: logger.Default.LogMode(logger.Info),\n\t\tNamingStrategy: schema.NamingStrategy{\n\t\t\tSingularTable: true,\n\t\t},\n\t}\n)\n\nvar hashMutex []*sync.Mutex\n\nfunc init() {\n\tfor i := 0; i < 32; i++ {\n\t\thashMutex = append(hashMutex, &sync.Mutex{})\n\t}\n}\n\n// HashLock Hash一样的图片不同时处理\nfunc HashLock(key string) *sync.Mutex {\n\thash := fnv.New32()\n\thash.Write([]byte(key))\n\treturn hashMutex[int(hash.Sum32()%uint32(len(hashMutex)))]\n}\n"
  },
  {
    "path": "go/nhentai/constant/os.go",
    "content": "package constant\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\nfunc ObtainDir(dir string) {\n\tif _, err := os.Stat(dir); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(dir, CreateDirMode)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc ReasonableFileName(title string) string {\n\ttitle = strings.ReplaceAll(title, \"\\\\\", \"_\")\n\ttitle = strings.ReplaceAll(title, \"/\", \"_\")\n\ttitle = strings.ReplaceAll(title, \"*\", \"_\")\n\ttitle = strings.ReplaceAll(title, \"?\", \"_\")\n\ttitle = strings.ReplaceAll(title, \"<\", \"_\")\n\ttitle = strings.ReplaceAll(title, \">\", \"_\")\n\ttitle = strings.ReplaceAll(title, \"|\", \"_\")\n\treturn title\n}\n"
  },
  {
    "path": "go/nhentai/constant/time.go",
    "content": "package constant\n\nimport \"time\"\n\n// Timestamp 获取当前的Unix时间戳\nfunc Timestamp() int64 {\n\treturn time.Now().UnixNano() / int64(time.Millisecond)\n}\n"
  },
  {
    "path": "go/nhentai/database/active/active.go",
    "content": "package active\n\nimport (\n\t\"errors\"\n\t\"github.com/niuhuan/nhentai-go\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"nhentai/nhentai/constant\"\n\t\"path\"\n\t\"sync\"\n)\n\nvar client = nhentai.Client{}\nvar mutex = sync.Mutex{}\nvar db *gorm.DB\n\nfunc Init(databaseDir string) {\n\tvar err error\n\tdb, err = gorm.Open(sqlite.Open(path.Join(databaseDir, \"download.db\")), constant.GormConfig)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdb.AutoMigrate(&ViewLog{})\n\tdb.AutoMigrate(&ViewLogTag{})\n\tdb.AutoMigrate(&Download{})\n\tdb.AutoMigrate(&DownloadTag{})\n\tdb.AutoMigrate(&DownloadCover{})\n\tdb.AutoMigrate(&DownloadCoverThumb{})\n\tdb.AutoMigrate(&DownloadPage{})\n\tdb.AutoMigrate(&DownloadPageThumb{})\n}\n\ntype ViewLog struct {\n\tID            int    `gorm:\"primarykey\" json:\"id\"`\n\tMediaId       int    `json:\"media_id\"`\n\tTitleEnglish  string `json:\"title_english\"`\n\tTitleJapanese string `json:\"title_japanese\"`\n\tTitlePretty   string `json:\"title_pretty\"`\n\tScanlator     string `json:\"scanlator\"`\n\tUploadDate    int    `json:\"upload_date\"`\n\tNumPages      int    `json:\"num_pages\"`\n\tNumFavorites  int    `json:\"num_favorites\"`\n\tLastViewTime  int64  `json:\"last_view_time\"`\n\tLastViewIndex int    `json:\"last_view_index\"`\n}\n\ntype ViewLogTag struct {\n\tComicId int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tID      int    `gorm:\"primarykey\" json:\"id\"`\n\tName    string `json:\"name\"`\n\tCount   int    `json:\"count\"`\n\tType    string `json:\"type\"`\n\tUrl     string `json:\"url\"`\n}\n\ntype Download struct {\n\tID                  int    `gorm:\"primarykey\" json:\"id\"`\n\tMediaId             int    `json:\"media_id\"`\n\tTitleEnglish        string `json:\"title_english\"`\n\tTitleJapanese       string `json:\"title_japanese\"`\n\tTitlePretty         string `json:\"title_pretty\"`\n\tScanlator           string `json:\"scanlator\"`\n\tUploadDate          int    `json:\"upload_date\"`\n\tNumPages            int    `json:\"num_pages\"`\n\tNumFavorites        int    `json:\"num_favorites\"`\n\tDownloadCreatedTime int64  `json:\"download_created_time\"`\n\tDownloadStatus      int    `json:\"download_status\"` // 未完成, 1成功, 2失败\n}\n\ntype DownloadTag struct {\n\tComicId int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tID      int    `gorm:\"primarykey\" json:\"id\"`\n\tName    string `json:\"name\"`\n\tCount   int    `json:\"count\"`\n\tType    string `json:\"type\"`\n\tUrl     string `json:\"url\"`\n}\n\ntype DownloadCover struct {\n\tComicId           int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tUrl               string `gorm:\"index:idx_url\" json:\"url\"`\n\tT                 string `json:\"t\"`\n\tW                 int    `json:\"w\"`\n\tH                 int    `json:\"h\"`\n\tDownloadStatus    int    `json:\"download_status\"` // 未完成, 1成功, 2失败\n\tDownloadLocalPath string\n}\n\ntype DownloadCoverThumb struct {\n\tComicId           int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tUrl               string `gorm:\"index:idx_url\" json:\"url\"`\n\tT                 string `json:\"t\"`\n\tW                 int    `json:\"w\"`\n\tH                 int    `json:\"h\"`\n\tDownloadStatus    int    `json:\"download_status\"` // 未完成, 1成功, 2失败\n\tDownloadLocalPath string\n}\n\ntype DownloadPage struct {\n\tComicId           int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tPageIndex         int    `gorm:\"primarykey\" json:\"page_index\"`\n\tNum               int    `json:\"num\"`\n\tUrl               string `gorm:\"index:idx_url\" json:\"url\"`\n\tT                 string `json:\"t\"`\n\tW                 int    `json:\"w\"`\n\tH                 int    `json:\"h\"`\n\tDownloadStatus    int    `json:\"download_status\"` // 未完成, 1成功, 2失败\n\tDownloadLocalPath string\n}\n\ntype DownloadPageThumb struct {\n\tComicId           int    `gorm:\"primarykey\" json:\"comic_id\"`\n\tPageIndex         int    `gorm:\"primarykey\" json:\"page_index\"`\n\tNum               int    `json:\"num\"`\n\tUrl               string `gorm:\"index:idx_url\" json:\"url\"`\n\tDownloadStatus    int    `json:\"download_status\"` // 未完成, 1成功, 2失败\n\tDownloadLocalPath string\n}\n\nfunc SaveViewInfo(info nhentai.ComicInfo) error {\n\tviewLog := takeViewLog(info, 0)\n\ttags := takeViewLogTags(info.Tags, info.Id)\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Transaction(func(tx *gorm.DB) error {\n\t\terr := tx.Clauses(clause.OnConflict{\n\t\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\t\tDoUpdates: clause.AssignmentColumns([]string{\n\t\t\t\t\"id\",\n\t\t\t\t\"media_id\",\n\t\t\t\t\"title_english\",\n\t\t\t\t\"title_japanese\",\n\t\t\t\t\"title_pretty\",\n\t\t\t\t\"scanlator\",\n\t\t\t\t\"upload_date\",\n\t\t\t\t\"num_pages\",\n\t\t\t\t\"num_favorites\",\n\t\t\t\t\"last_view_time\",\n\t\t\t}),\n\t\t}).Create(&viewLog).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn saveViewTagInTx(tx, tags)\n\t})\n}\n\nfunc SaveViewIndex(info nhentai.ComicInfo, index int) error {\n\tviewLog := takeViewLog(info, index)\n\ttags := takeViewLogTags(info.Tags, info.Id)\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Transaction(func(tx *gorm.DB) error {\n\t\terr := tx.Clauses(clause.OnConflict{\n\t\t\tColumns: []clause.Column{{Name: \"id\"}},\n\t\t\tDoUpdates: clause.AssignmentColumns([]string{\n\t\t\t\t\"id\",\n\t\t\t\t\"media_id\",\n\t\t\t\t\"title_english\",\n\t\t\t\t\"title_japanese\",\n\t\t\t\t\"title_pretty\",\n\t\t\t\t\"scanlator\",\n\t\t\t\t\"upload_date\",\n\t\t\t\t\"num_pages\",\n\t\t\t\t\"num_favorites\",\n\t\t\t\t\"last_view_time\",\n\t\t\t\t\"last_view_index\",\n\t\t\t}),\n\t\t}).Create(&viewLog).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn saveViewTagInTx(tx, tags)\n\t})\n}\n\nfunc saveViewTagInTx(tx *gorm.DB, tags []ViewLogTag) error {\n\tvar err error\n\tfor i := 0; i < len(tags); i++ {\n\t\terr = tx.Clauses(clause.OnConflict{\n\t\t\tColumns: []clause.Column{\n\t\t\t\t{Name: \"comic_id\"},\n\t\t\t\t{Name: \"id\"},\n\t\t\t},\n\t\t\tDoUpdates: clause.AssignmentColumns([]string{\n\t\t\t\t\"comic_id\",\n\t\t\t\t\"id\",\n\t\t\t\t\"type\",\n\t\t\t\t\"count\",\n\t\t\t\t\"name\",\n\t\t\t\t\"url\",\n\t\t\t}),\n\t\t}).Create(&tags[i]).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc takeViewLogTags(infoTags []nhentai.ComicInfoTag, comicId int) []ViewLogTag {\n\ttags := make([]ViewLogTag, len(infoTags))\n\tfor i := 0; i < len(infoTags); i++ {\n\t\ttags[i] = ViewLogTag{\n\t\t\tComicId: comicId,\n\t\t\tID:      infoTags[i].Id,\n\t\t\tType:    infoTags[i].Type,\n\t\t\tCount:   infoTags[i].Count,\n\t\t\tName:    infoTags[i].Name,\n\t\t\tUrl:     infoTags[i].Url,\n\t\t}\n\t}\n\treturn tags\n}\n\nfunc takeViewLog(info nhentai.ComicInfo, index int) ViewLog {\n\treturn ViewLog{\n\t\tID:            info.Id,\n\t\tMediaId:       info.MediaId,\n\t\tTitleEnglish:  info.Title.English,\n\t\tTitleJapanese: info.Title.Japanese,\n\t\tTitlePretty:   info.Title.Pretty,\n\t\tScanlator:     info.Scanlator,\n\t\tUploadDate:    info.UploadDate,\n\t\tNumPages:      info.NumPages,\n\t\tNumFavorites:  info.NumFavorites,\n\t\tLastViewTime:  constant.Timestamp(),\n\t\tLastViewIndex: index,\n\t}\n}\n\nfunc LoadLastViewIndexByComicId(comicId int) (int, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar viewLog ViewLog\n\terr := db.Where(\"id = ?\", comicId).Find(&viewLog).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn 0, nil\n\t\t}\n\t\treturn 0, err\n\t}\n\treturn viewLog.LastViewIndex, nil\n}\n\nfunc CreateDownload(info nhentai.ComicInfo) error {\n\tdownload := Download{\n\t\tID:                  info.Id,\n\t\tMediaId:             info.MediaId,\n\t\tTitleEnglish:        info.Title.English,\n\t\tTitleJapanese:       info.Title.Japanese,\n\t\tTitlePretty:         info.Title.Pretty,\n\t\tScanlator:           info.Scanlator,\n\t\tUploadDate:          info.UploadDate,\n\t\tNumPages:            info.NumPages,\n\t\tNumFavorites:        info.NumFavorites,\n\t\tDownloadCreatedTime: constant.Timestamp(),\n\t\tDownloadStatus:      0,\n\t}\n\ttags := takeDownloadTags(info.Tags, info.Id)\n\tcover := DownloadCover{\n\t\tComicId:           info.Id,\n\t\tUrl:               client.CoverUrl(info.MediaId, info.Images.Cover.T),\n\t\tT:                 info.Images.Cover.T,\n\t\tW:                 info.Images.Cover.W,\n\t\tH:                 info.Images.Cover.H,\n\t\tDownloadStatus:    0,\n\t\tDownloadLocalPath: \"\",\n\t}\n\tcoverThumb := DownloadCoverThumb{\n\t\tComicId:           info.Id,\n\t\tUrl:               client.ThumbnailUrl(info.MediaId, info.Images.Thumbnail.T),\n\t\tT:                 info.Images.Thumbnail.T,\n\t\tW:                 info.Images.Thumbnail.W,\n\t\tH:                 info.Images.Thumbnail.H,\n\t\tDownloadStatus:    0,\n\t\tDownloadLocalPath: \"\",\n\t}\n\tpages := make([]DownloadPage, len(info.Images.Pages))\n\tpagesThumbs := make([]DownloadPageThumb, len(info.Images.Pages))\n\tfor i := 0; i < len(info.Images.Pages); i++ {\n\t\tpages[i] = DownloadPage{\n\t\t\tComicId:           info.Id,\n\t\t\tPageIndex:         i,\n\t\t\tNum:               i + 1,\n\t\t\tUrl:               client.PageUrl(info.MediaId, i+1, info.Images.Pages[i].T),\n\t\t\tT:                 info.Images.Pages[i].T,\n\t\t\tW:                 info.Images.Pages[i].W,\n\t\t\tH:                 info.Images.Pages[i].H,\n\t\t\tDownloadStatus:    0,\n\t\t\tDownloadLocalPath: \"\",\n\t\t}\n\t\tpagesThumbs[i] = DownloadPageThumb{\n\t\t\tComicId:           info.Id,\n\t\t\tPageIndex:         i,\n\t\t\tNum:               i + 1,\n\t\t\tUrl:               client.PageThumbnailUrl(info.MediaId, i+1, info.Images.Pages[i].T),\n\t\t\tDownloadStatus:    0,\n\t\t\tDownloadLocalPath: \"\",\n\t\t}\n\t}\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Transaction(func(tx *gorm.DB) error {\n\t\terr := tx.Where(\"id = ?\", download.ID).First(&Download{}).Error\n\t\tif err == nil {\n\t\t\treturn errors.New(\"download exists\")\n\t\t}\n\t\tif err != gorm.ErrRecordNotFound {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Save(&download).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i := 0; i < len(tags); i++ {\n\t\t\ttx.Save(&(tags[i]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\terr = tx.Save(&cover).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Save(&coverThumb).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor i := 0; i < len(pages); i++ {\n\t\t\ttx.Save(&(pages[i]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tfor i := 0; i < len(pagesThumbs); i++ {\n\t\t\ttx.Save(&(pagesThumbs[i]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc takeDownloadTags(infoTags []nhentai.ComicInfoTag, comicId int) []DownloadTag {\n\ttags := make([]DownloadTag, len(infoTags))\n\tfor i := 0; i < len(infoTags); i++ {\n\t\ttags[i] = DownloadTag{\n\t\t\tComicId: comicId,\n\t\t\tID:      infoTags[i].Id,\n\t\t\tType:    infoTags[i].Type,\n\t\t\tCount:   infoTags[i].Count,\n\t\t\tName:    infoTags[i].Name,\n\t\t\tUrl:     infoTags[i].Url,\n\t\t}\n\t}\n\treturn tags\n}\n\nfunc LoadFirstNeedDownload() (*Download, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tdownload := Download{}\n\terr := db.Where(\"download_status = 0\").Order(\"download_created_time DESC\").First(&download).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &download, nil\n}\n\nfunc TheDownloadCover(id int) (*DownloadCover, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tcover := DownloadCover{}\n\terr := db.Where(\"comic_id = ?\", id).First(&cover).Error\n\treturn &cover, err\n}\n\nfunc TheDownloadCoverThumb(id int) (*DownloadCoverThumb, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tcoverThumb := DownloadCoverThumb{}\n\terr := db.Where(\"comic_id = ?\", id).First(&coverThumb).Error\n\treturn &coverThumb, err\n}\n\nfunc TheDownloadNeedDownloadPages(id int, limit int) ([]DownloadPage, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar pages []DownloadPage\n\treturn pages, db.Where(\"comic_id = ? AND download_status = 0\", id).\n\t\tOrder(\"page_index ASC\").Limit(limit).\n\t\tFind(&pages).Error\n}\n\nfunc TheDownloadNeedDownloadPageThumbs(id int, limit int) ([]DownloadPageThumb, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar pages []DownloadPageThumb\n\treturn pages, db.Where(\"comic_id = ? AND download_status = 0\", id).\n\t\tOrder(\"page_index ASC\").Limit(limit).\n\t\tFind(&pages).Error\n}\n\nfunc SaveDownloadCoverStatus(comicId int, status int, path string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Model(&DownloadCover{}).Where(\"comic_id = ?\", comicId).Updates(map[string]interface{}{\n\t\t\"download_status\":     status,\n\t\t\"download_local_path\": path,\n\t}).Error\n}\n\nfunc SaveDownloadCoverThumbStatus(comicId int, status int, path string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Model(&DownloadCoverThumb{}).Where(\"comic_id = ?\", comicId).Updates(map[string]interface{}{\n\t\t\"download_status\":     status,\n\t\t\"download_local_path\": path,\n\t}).Error\n}\n\nfunc SaveDownloadPageStatus(comicId int, pageIndex, status int, path string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Model(&DownloadPage{}).Where(\"comic_id = ? AND page_index = ?\", comicId, pageIndex).Updates(map[string]interface{}{\n\t\t\"download_status\":     status,\n\t\t\"download_local_path\": path,\n\t}).Error\n}\n\nfunc SaveDownloadPageThumbStatus(comicId int, pageIndex, status int, path string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Model(&DownloadPageThumb{}).Where(\"comic_id = ? AND page_index = ?\", comicId, pageIndex).Updates(map[string]interface{}{\n\t\t\"download_status\":     status,\n\t\t\"download_local_path\": path,\n\t}).Error\n}\n\nfunc DownloadCoverOk(comicId int) bool {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar covers []DownloadCover\n\terr := db.Where(\"comic_id = ?\", comicId).Group(\"download_status\").Find(&covers).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn len(covers) == 1 && covers[0].DownloadStatus == 1\n}\n\nfunc DownloadCoverThumbOk(comicId int) bool {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar covers []DownloadCoverThumb\n\terr := db.Where(\"comic_id = ?\", comicId).Group(\"download_status\").Find(&covers).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn len(covers) == 1 && covers[0].DownloadStatus == 1\n}\n\nfunc DownloadPageOk(comicId int) bool {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar pages []DownloadPage\n\terr := db.Where(\"comic_id = ?\", comicId).Group(\"download_status\").Find(&pages).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn len(pages) == 1 && pages[0].DownloadStatus == 1\n}\n\nfunc DownloadPageThumbOk(comicId int) bool {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar pages []DownloadPageThumb\n\terr := db.Where(\"comic_id = ?\", comicId).Group(\"download_status\").Find(&pages).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn len(pages) == 1 && pages[0].DownloadStatus == 1\n}\n\nfunc SaveDownloadStatus(id int, status int) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Model(&Download{}).Where(\"id = ?\", id).Updates(map[string]interface{}{\n\t\t\"download_status\": status,\n\t}).Error\n}\n\nfunc FindDownloadPageByUrl(url string) *DownloadPage {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tpage := DownloadPage{}\n\terr := db.Where(\"url = ? AND download_status = 1\", url).First(&page).Error\n\tif err != nil {\n\t\tif err != gorm.ErrRecordNotFound {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn &page\n}\n\nfunc FindDownloadPageThumbByUrl(url string) *DownloadPageThumb {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tpage := DownloadPageThumb{}\n\terr := db.Where(\"url = ? AND download_status = 1\", url).First(&page).Error\n\tif err != nil {\n\t\tif err != gorm.ErrRecordNotFound {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn &page\n}\n\nfunc FindDownloadCoverByUrl(url string) *DownloadCover {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tpage := DownloadCover{}\n\terr := db.Where(\"url = ? AND download_status = 1\", url).First(&page).Error\n\tif err != nil {\n\t\tif err != gorm.ErrRecordNotFound {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn &page\n}\n\nfunc FindDownloadCoverThumbByUrl(url string) *DownloadCoverThumb {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tpage := DownloadCoverThumb{}\n\terr := db.Where(\"url = ? AND download_status = 1\", url).First(&page).Error\n\tif err != nil {\n\t\tif err != gorm.ErrRecordNotFound {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn &page\n}\n\nfunc HasDownload(comicId int) bool {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar download Download\n\terr := db.Where(\"id = ?\", comicId).First(&download).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn false\n\t\t}\n\t\tpanic(err)\n\t}\n\treturn true\n}\n\nfunc ListDownloadComicInfo() []DownloadComicInfo {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar err error\n\tvar downloads []Download\n\tvar tags []DownloadTag\n\tvar thumbs []DownloadCoverThumb\n\tvar covers []DownloadCover\n\tvar pages []DownloadPage\n\terr = db.Find(&downloads).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Find(&tags).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Find(&covers).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Find(&thumbs).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Order(\"page_index ASC\").Find(&pages).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar infos = make([]DownloadComicInfo, len(downloads))\n\tfor i := 0; i < len(infos); i++ {\n\t\tinfos[i] = DownloadComicInfo{\n\t\t\tComicInfo: nhentai.ComicInfo{\n\t\t\t\tId:      downloads[i].ID,\n\t\t\t\tMediaId: downloads[i].MediaId,\n\t\t\t\tTitle: nhentai.ComicInfoTitle{\n\t\t\t\t\tEnglish:  downloads[i].TitleEnglish,\n\t\t\t\t\tJapanese: downloads[i].TitleJapanese,\n\t\t\t\t\tPretty:   downloads[i].TitlePretty,\n\t\t\t\t},\n\t\t\t\tImages: nhentai.ComicInfoImages{\n\t\t\t\t\tPages:     filterPages(pages, downloads[i].ID),\n\t\t\t\t\tCover:     filterCover(covers, downloads[i].ID),\n\t\t\t\t\tThumbnail: filterCoverThumb(thumbs, downloads[i].ID),\n\t\t\t\t},\n\t\t\t\tScanlator:    downloads[i].Scanlator,\n\t\t\t\tUploadDate:   downloads[i].UploadDate,\n\t\t\t\tTags:         filterTags(tags, downloads[i].ID),\n\t\t\t\tNumPages:     downloads[i].NumPages,\n\t\t\t\tNumFavorites: downloads[i].NumFavorites,\n\t\t\t},\n\t\t\tDownloadStatus: downloads[i].DownloadStatus,\n\t\t}\n\t}\n\treturn infos\n}\n\nfunc filterPages(pages []DownloadPage, id int) []nhentai.ImageInfo {\n\trsp := make([]nhentai.ImageInfo, 0)\n\tfor i := 0; i < len(pages); i++ {\n\t\tif pages[i].ComicId == id {\n\t\t\trsp = append(rsp, nhentai.ImageInfo{\n\t\t\t\tT: pages[i].T,\n\t\t\t\tW: pages[i].W,\n\t\t\t\tH: pages[i].H,\n\t\t\t})\n\t\t}\n\t}\n\treturn rsp\n}\n\nfunc filterTags(tags []DownloadTag, id int) []nhentai.ComicInfoTag {\n\trsp := make([]nhentai.ComicInfoTag, 0)\n\tfor i := 0; i < len(tags); i++ {\n\t\tif tags[i].ComicId == id {\n\t\t\trsp = append(rsp, nhentai.ComicInfoTag{\n\t\t\t\tId:    tags[i].ID,\n\t\t\t\tName:  tags[i].Name,\n\t\t\t\tCount: tags[i].Count,\n\t\t\t\tType:  tags[i].Type,\n\t\t\t\tUrl:   tags[i].Url,\n\t\t\t})\n\t\t}\n\t}\n\treturn rsp\n}\n\nfunc filterCover(covers []DownloadCover, id int) nhentai.ImageInfo {\n\tfor i := 0; i < len(covers); i++ {\n\t\tif covers[i].ComicId == id {\n\t\t\treturn nhentai.ImageInfo{\n\t\t\t\tT: covers[i].T,\n\t\t\t\tW: covers[i].W,\n\t\t\t\tH: covers[i].H,\n\t\t\t}\n\t\t}\n\t}\n\treturn nhentai.ImageInfo{}\n}\n\nfunc filterCoverThumb(covers []DownloadCoverThumb, id int) nhentai.ImageInfo {\n\tfor i := 0; i < len(covers); i++ {\n\t\tif covers[i].ComicId == id {\n\t\t\treturn nhentai.ImageInfo{\n\t\t\t\tT: covers[i].T,\n\t\t\t\tW: covers[i].W,\n\t\t\t\tH: covers[i].H,\n\t\t\t}\n\t\t}\n\t}\n\treturn nhentai.ImageInfo{}\n}\n\ntype DownloadComicInfo struct {\n\tnhentai.ComicInfo\n\tDownloadStatus int `json:\"download_status\"`\n}\n\nfunc ResetAllDownload() {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar err error\n\terr = db.Exec(\"UPDATE download set download_status = 0 WHERE download_status = 2\").Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Exec(\"UPDATE download_cover set download_status = 0 WHERE download_status = 2\").Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Exec(\"UPDATE download_cover_thumb set download_status = 0 WHERE download_status = 2\").Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Exec(\"UPDATE download_page set download_status = 0 WHERE download_status = 2\").Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\terr = db.Exec(\"UPDATE download_page_thumb set download_status = 0 WHERE download_status = 2\").Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc MarkComicDeleting(id int) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Model(&Download{}).Where(\"id = ?\", id).Update(\"download_status\", 3).Error\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc LoadFirstNeedDelete() *Download {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tdownload := Download{}\n\terr := db.Where(\"download_status = 3\").Order(\"download_created_time ASC\").First(&download).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn nil\n\t\t}\n\t\tpanic(err)\n\t}\n\treturn &download\n}\n\nfunc DeletedComic(id int) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Transaction(func(tx *gorm.DB) error {\n\t\tvar err error\n\t\terr = tx.Unscoped().Delete(&Download{}, \"id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Unscoped().Delete(&DownloadCover{}, \"comic_id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Unscoped().Delete(&DownloadPage{}, \"comic_id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Unscoped().Delete(&DownloadTag{}, \"comic_id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Unscoped().Delete(&DownloadCoverThumb{}, \"comic_id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = tx.Unscoped().Delete(&DownloadPageThumb{}, \"comic_id = ?\", id).Error\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "go/nhentai/database/cache/cache.go",
    "content": "package cache\n\nimport (\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"nhentai/nhentai/constant\"\n\t\"path\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar mutex = sync.Mutex{}\nvar db *gorm.DB\n\ntype NetworkCache struct {\n\tgorm.Model\n\tK string `gorm:\"index:uk_k,unique\"`\n\tV string\n}\n\ntype ImageCache struct {\n\tgorm.Model\n\tUrl       string `gorm:\"index:uk_url,unique\" json:\"fileServer\"`\n\tLocalPath string `json:\"localPath\"`\n\tFileSize  int64  `json:\"fileSize\"`\n}\n\nfunc Init(databaseDir string) {\n\tvar err error\n\tdb, err = gorm.Open(sqlite.Open(path.Join(databaseDir, \"cache.db\")), constant.GormConfig)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdb.AutoMigrate(&NetworkCache{})\n\tdb.AutoMigrate(&ImageCache{})\n}\n\nfunc LoadCache(key string, expire time.Duration) (string, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar cache NetworkCache\n\terr := db.First(&cache, \"k = ? AND updated_at > ?\", key, time.Now().Add(expire*-1)).Error\n\tif err == nil {\n\t\treturn cache.V, nil\n\t}\n\tif gorm.ErrRecordNotFound == err {\n\t\treturn \"\", nil\n\t}\n\treturn \"\", err\n}\n\nfunc SaveCache(key string, value string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"k\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"created_at\", \"updated_at\", \"v\"}),\n\t}).Create(&NetworkCache{\n\t\tK: key,\n\t\tV: value,\n\t}).Error\n}\n\nfunc RemoveCache(key string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Unscoped().Delete(&NetworkCache{}, \"k = ?\", key).Error\n\tif err == gorm.ErrRecordNotFound {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc RemoveCaches(like string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Unscoped().Delete(&NetworkCache{}, \"k LIKE ?\", like).Error\n\tif err == gorm.ErrRecordNotFound {\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc RemoveAllCache() error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Unscoped().Delete(&NetworkCache{}, \"1 = 1\").Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn db.Raw(\"VACUUM\").Error\n}\n\nfunc RemoveEarliestCache(earliest time.Time) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Unscoped().Where(\"strftime('%s',updated_at) < strftime('%s',?)\", earliest).\n\t\tDelete(&NetworkCache{}).Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn db.Raw(\"VACUUM\").Error\n}\n\nfunc SaveImageCache(remote *ImageCache) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\treturn db.Clauses(clause.OnConflict{\n\t\tColumns: []clause.Column{{Name: \"url\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\n\t\t\t\"updated_at\",\n\t\t\t\"file_size\",\n\t\t\t\"local_path\",\n\t\t}),\n\t}).Create(remote).Error\n}\n\nfunc FindImageCache(url string) *ImageCache {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar imageCache ImageCache\n\terr := db.First(&imageCache, \"url = ?\", url).Error\n\tif err != nil {\n\t\tif err == gorm.ErrRecordNotFound {\n\t\t\treturn nil\n\t\t} else {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn &imageCache\n}\n\nfunc RemoveAllImageCache() error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\terr := db.Unscoped().Delete(&ImageCache{}, \"1 = 1\").Error\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn db.Raw(\"VACUUM\").Error\n}\n\nfunc EarliestImageCache(earliest time.Time, pageSize int) ([]ImageCache, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tvar images []ImageCache\n\terr := db.Where(\"strftime('%s',updated_at) < strftime('%s',?)\", earliest).\n\t\tOrder(\"updated_at\").Limit(pageSize).Find(&images).Error\n\treturn images, err\n}\n\nfunc DeleteImageCache(images []ImageCache) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\tif len(images) == 0 {\n\t\treturn nil\n\t}\n\tids := make([]uint, len(images))\n\tfor i := 0; i < len(images); i++ {\n\t\tids[i] = images[i].ID\n\t}\n\treturn db.Unscoped().Model(&ImageCache{}).Delete(\"id in ?\", ids).Error\n}\n"
  },
  {
    "path": "go/nhentai/database/properties/properties.go",
    "content": "package properties\n\nimport (\n\t\"errors\"\n\t\"gorm.io/driver/sqlite\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"nhentai/nhentai/constant\"\n\t\"path\"\n\t\"strconv\"\n)\n\nvar db *gorm.DB\n\nfunc Init(databaseDir string) {\n\tvar err error\n\tdb, err = gorm.Open(sqlite.Open(path.Join(databaseDir, \"properties.db\")), constant.GormConfig)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdb.AutoMigrate(&Property{})\n}\n\ntype Property struct {\n\tgorm.Model\n\tK string `gorm:\"index:uk_k,unique\"`\n\tV string\n}\n\nfunc LoadProperty(name string, defaultValue string) (string, error) {\n\tvar property Property\n\terr := db.First(&property, \"k\", name).Error\n\tif err == nil {\n\t\treturn property.V, nil\n\t}\n\tif gorm.ErrRecordNotFound == err {\n\t\treturn defaultValue, nil\n\t}\n\tpanic(errors.New(\"?\"))\n}\n\nfunc SaveProperty(name string, value string) error {\n\treturn db.Clauses(clause.OnConflict{\n\t\tColumns:   []clause.Column{{Name: \"k\"}},\n\t\tDoUpdates: clause.AssignmentColumns([]string{\"created_at\", \"updated_at\", \"v\"}),\n\t}).Create(&Property{\n\t\tK: name,\n\t\tV: value,\n\t}).Error\n}\n\nfunc LoadBoolProperty(name string, defaultValue bool) (bool, error) {\n\tstringValue, err := LoadProperty(name, strconv.FormatBool(defaultValue))\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn strconv.ParseBool(stringValue)\n}\n\nfunc LoadIntProperty(name string, defaultValue int) (int, error) {\n\tvar property Property\n\terr := db.First(&property, \"k\", name).Error\n\tif err == nil {\n\t\treturn strconv.Atoi(property.V)\n\t}\n\tif gorm.ErrRecordNotFound == err {\n\t\treturn defaultValue, nil\n\t}\n\tpanic(errors.New(\"?\"))\n}\n"
  },
  {
    "path": "go/nhentai/decodes.go",
    "content": "package nhentai\n\nimport (\n\t\"errors\"\n\t_ \"golang.org/x/image/webp\"\n\t_ \"image/gif\"\n\t_ \"image/jpeg\"\n\t_ \"image/png\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"nhentai/nhentai/database/cache\"\n\t\"sync\"\n)\n\nvar mutexCounter = -1\nvar busMutex *sync.Mutex\nvar subMutexes []*sync.Mutex\n\nfunc init() {\n\tbusMutex = &sync.Mutex{}\n\tfor i := 0; i < 5; i++ {\n\t\tsubMutexes = append(subMutexes, &sync.Mutex{})\n\t}\n}\n\n// takeMutex 下载图片获取一个锁, 这样只能同时下载5张图片\nfunc takeMutex() *sync.Mutex {\n\tbusMutex.Lock()\n\tdefer busMutex.Unlock()\n\tmutexCounter = (mutexCounter + 1) % len(subMutexes)\n\treturn subMutexes[mutexCounter]\n}\n\n// 下载图片并decode\nfunc decodeFromUrl(url string) ([]byte, error) {\n\tm := takeMutex()\n\tm.Lock()\n\tdefer m.Unlock()\n\trequest, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresponse, err := client.Do(request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\tbuff, err := ioutil.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif response.StatusCode != 200 {\n\t\tprintln(\"NOT 200\")\n\t\tprintln(string(buff))\n\t\treturn nil, errors.New(\"code is not 200\")\n\t}\n\treturn buff, nil\n}\n\n// decodeFromCache 仅下载使用\nfunc decodeFromRemote(url string) ([]byte, error) {\n\tcache := cache.FindImageCache(url)\n\tif cache != nil {\n\t\treturn ioutil.ReadFile(cacheImagePath(cache.LocalPath))\n\t}\n\treturn nil, errors.New(\"not found\")\n}\n"
  },
  {
    "path": "go/nhentai/download.go",
    "content": "package nhentai\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"nhentai/nhentai/constant\"\n\t\"nhentai/nhentai/database/active\"\n\t\"os\"\n\t\"path\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar initDownloadFlag bool\nvar downloadThreadCount = 1\nvar downloadThreadFetch = 100\nvar downloadRunning = false\nvar downloadRestart = false\n\nfunc initDownload() {\n\tif initDownloadFlag {\n\t\treturn\n\t}\n\tactive.ResetAllDownload()\n\tinitDownloadFlag = true\n\tdownloadRunning = true\n\tgo downloadBegin()\n}\n\n// 下载周期中, 每个下载单元会调用此方法, 如果返回true应该停止当前动作\nfunc downloadHasStop() bool {\n\tif !downloadRunning {\n\t\treturn true\n\t}\n\tif downloadRestart {\n\t\tdownloadRestart = false\n\t\treturn true\n\t}\n\treturn false\n}\n\n// 删除第一个需要删除的漫画, 成功删除返回true\nfunc downloadDelete() bool {\n\tneedDelete := active.LoadFirstNeedDelete()\n\tif needDelete != nil {\n\t\trelativeFolder := fmt.Sprintf(\"%d/%d\", needDelete.ID, needDelete.MediaId)\n\t\tabsoluteFolder := path.Join(downloadPath, relativeFolder)\n\t\terr := os.RemoveAll(absoluteFolder)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tactive.DeletedComic(needDelete.ID)\n\t\treturn true\n\t}\n\treturn false\n}\n\n// 下载启动/重新启动会暂停三秒\nfunc downloadBegin() {\n\ttime.Sleep(time.Second * 3)\n\t// 每次下载完一个漫画, 或者启动的时候, 首先进行删除任务\n\tfor downloadDelete() {\n\t}\n\tif downloadHasStop() {\n\t\treturn\n\t}\n\tgo downloadLoadComic()\n}\n\n// 加载第一个需要下载的漫画\nfunc downloadLoadComic() {\n\tif downloadHasStop() {\n\t\tgo downloadBegin()\n\t\treturn\n\t}\n\t// 找到第一个要下载的漫画, 查库有错误就停止, 因为这些错误很少出现, 一旦出现必然是严重的, 例如数据库文件突然被删除\n\tdownloadingComic, err := active.LoadFirstNeedDownload()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif downloadingComic == nil {\n\t\tprintln(\"没有找到要下载的漫画\")\n\t\tgo downloadBegin()\n\t\treturn\n\t}\n\tgo downloadProcessDownloadingComic(downloadingComic)\n}\n\nfunc downloadProcessDownloadingComic(downloadingComic *active.Download) {\n\tif downloadHasStop() {\n\t\tgo downloadBegin()\n\t\treturn\n\t}\n\t//\n\trelativeFolder := fmt.Sprintf(\"%d/%d\", downloadingComic.ID, downloadingComic.MediaId)\n\tabsoluteFolder := path.Join(downloadPath, relativeFolder)\n\tconstant.ObtainDir(absoluteFolder)\n\t// 下载封面\n\tcover, err := active.TheDownloadCover(downloadingComic.ID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif cover.DownloadStatus == 0 {\n\t\tbuff, err := downloadDecodeUrl(cover.Url)\n\t\tif buff != nil {\n\t\t\tcoverName := \"cover\"\n\t\t\trelativeCover := path.Join(relativeFolder, coverName)\n\t\t\tabsoluteCover := path.Join(absoluteFolder, coverName)\n\t\t\terr = ioutil.WriteFile(absoluteCover, buff, constant.CreateFileMode)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tactive.SaveDownloadCoverStatus(downloadingComic.ID, 1, relativeCover)\n\t\t} else {\n\t\t\tactive.SaveDownloadCoverStatus(downloadingComic.ID, 2, \"\")\n\t\t}\n\t}\n\t// 下载封面缩略图\n\tcoverThumb, err := active.TheDownloadCoverThumb(downloadingComic.ID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif coverThumb.DownloadStatus == 0 {\n\t\tbuff, err := downloadDecodeUrl(coverThumb.Url)\n\t\tif buff != nil {\n\t\t\tcoverName := \"cover_thumb\"\n\t\t\trelativeCoverThumb := path.Join(relativeFolder, coverName)\n\t\t\tabsoluteCoverThumb := path.Join(absoluteFolder, coverName)\n\t\t\terr = ioutil.WriteFile(absoluteCoverThumb, buff, constant.CreateFileMode)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tactive.SaveDownloadCoverThumbStatus(downloadingComic.ID, 1, relativeCoverThumb)\n\t\t} else {\n\t\t\tactive.SaveDownloadCoverThumbStatus(downloadingComic.ID, 2, \"\")\n\t\t}\n\t}\n\t// 暂停检测\n\tif downloadHasStop() {\n\t\tgo downloadBegin()\n\t\treturn\n\t}\n\t// 下载漫画\n\t// WARNING 无限循环\n\tfor {\n\t\tdownloadingPictures, err := active.TheDownloadNeedDownloadPages(downloadingComic.ID, downloadThreadFetch)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif len(downloadingPictures) == 0 {\n\t\t\tbreak\n\t\t}\n\t\t// 多线程下载漫画\n\t\thasStop := func() bool {\n\t\t\tchannel := make(chan int, downloadThreadCount)\n\t\t\tdefer close(channel)\n\t\t\twg := sync.WaitGroup{}\n\t\t\tfor i := 0; i < len(downloadingPictures); i++ {\n\t\t\t\t// 暂停检测\n\t\t\t\tif downloadHasStop() {\n\t\t\t\t\twg.Wait()\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tchannel <- 0\n\t\t\t\twg.Add(1)\n\t\t\t\t// 不放入携程, 防止i已经变化\n\t\t\t\tpagePoint := &(downloadingPictures[i])\n\t\t\t\tgo func() {\n\t\t\t\t\t// 下载漫画\n\t\t\t\t\tbuff, err := downloadDecodeUrl(pagePoint.Url)\n\t\t\t\t\tif buff != nil {\n\t\t\t\t\t\tpageName := fmt.Sprintf(\"p_%d\", pagePoint.PageIndex)\n\t\t\t\t\t\trelativePageName := path.Join(relativeFolder, pageName)\n\t\t\t\t\t\tabsolutePageName := path.Join(absoluteFolder, pageName)\n\t\t\t\t\t\terr = ioutil.WriteFile(absolutePageName, buff, constant.CreateFileMode)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tactive.SaveDownloadPageStatus(downloadingComic.ID, pagePoint.PageIndex, 1, relativePageName)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tactive.SaveDownloadPageStatus(downloadingComic.ID, pagePoint.PageIndex, 2, \"\")\n\t\t\t\t\t}\n\t\t\t\t\t// 下载漫画\n\t\t\t\t\t<-channel\n\t\t\t\t\twg.Done()\n\t\t\t\t}()\n\t\t\t}\n\t\t\twg.Wait()\n\t\t\treturn false\n\t\t}()\n\t\tif hasStop {\n\t\t\tgo downloadBegin()\n\t\t\treturn\n\t\t}\n\t}\n\t// 下载漫画\n\t// WARNING 无限循环\n\tfor {\n\t\tdownloadingPictureThumbs, err := active.TheDownloadNeedDownloadPageThumbs(downloadingComic.ID, downloadThreadFetch)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tif len(downloadingPictureThumbs) == 0 {\n\t\t\tbreak\n\t\t}\n\t\t// 多线程下载漫画\n\t\thasStop := func() bool {\n\t\t\tchannel := make(chan int, downloadThreadCount)\n\t\t\tdefer close(channel)\n\t\t\twg := sync.WaitGroup{}\n\t\t\tfor i := 0; i < len(downloadingPictureThumbs); i++ {\n\t\t\t\t// 暂停检测\n\t\t\t\tif downloadHasStop() {\n\t\t\t\t\twg.Wait()\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tchannel <- 0\n\t\t\t\twg.Add(1)\n\t\t\t\t// 不放入携程, 防止i已经变化\n\t\t\t\tpagePoint := &(downloadingPictureThumbs[i])\n\t\t\t\tgo func() {\n\t\t\t\t\t//\n\t\t\t\t\tbuff, err := downloadDecodeUrl(pagePoint.Url)\n\t\t\t\t\tif buff != nil {\n\t\t\t\t\t\tpageThumbName := fmt.Sprintf(\"p_%d_t\", pagePoint.PageIndex)\n\t\t\t\t\t\trelativePageThumbName := path.Join(relativeFolder, pageThumbName)\n\t\t\t\t\t\tabsolutePageThumbName := path.Join(absoluteFolder, pageThumbName)\n\t\t\t\t\t\terr = ioutil.WriteFile(absolutePageThumbName, buff, constant.CreateFileMode)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\terr = active.SaveDownloadPageThumbStatus(downloadingComic.ID, pagePoint.PageIndex, 1, relativePageThumbName)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\terr = active.SaveDownloadPageThumbStatus(downloadingComic.ID, pagePoint.PageIndex, 2, \"\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tpanic(err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t//\n\t\t\t\t\t<-channel\n\t\t\t\t\twg.Done()\n\t\t\t\t}()\n\t\t\t}\n\t\t\twg.Wait()\n\t\t\treturn false\n\t\t}()\n\t\tif hasStop {\n\t\t\tgo downloadBegin()\n\t\t\treturn\n\t\t}\n\t}\n\t// 总结下载进度\n\tif active.DownloadCoverOk(downloadingComic.ID) && active.DownloadCoverThumbOk(downloadingComic.ID) &&\n\t\tactive.DownloadPageOk(downloadingComic.ID) && active.DownloadPageThumbOk(downloadingComic.ID) {\n\t\terr := active.SaveDownloadStatus(downloadingComic.ID, 1)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t} else {\n\t\terr := active.SaveDownloadStatus(downloadingComic.ID, 2)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tgo downloadBegin()\n}\n\nfunc downloadDecodeUrl(url string) ([]byte, error) {\n\tbuff, err := decodeFromRemote(url)\n\tif buff != nil {\n\t\treturn buff, nil\n\t}\n\tfor i := 0; i < 5; i++ {\n\t\tbuff, err = decodeFromUrl(url)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif buff != nil {\n\t\t\treturn buff, nil\n\t\t}\n\t}\n\treturn nil, err\n}\n"
  },
  {
    "path": "go/nhentai/locks.go",
    "content": "package nhentai\n\nimport (\n\t\"hash/fnv\"\n\t\"sync\"\n)\n\nvar hashMutex []*sync.Mutex\n\nfunc init() {\n\tfor i := 0; i < 32; i++ {\n\t\thashMutex = append(hashMutex, &sync.Mutex{})\n\t}\n}\n\n// HashLock Hash一样的图片不同时处理\nfunc HashLock(key string) *sync.Mutex {\n\thash := fnv.New32()\n\thash.Write([]byte(key))\n\treturn hashMutex[int(hash.Sum32()%uint32(len(hashMutex)))]\n}\n\n\n"
  },
  {
    "path": "go/nhentai/nhentai.go",
    "content": "package nhentai\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/niuhuan/nhentai-go\"\n\t\"github.com/pkg/errors\"\n\t\"io/ioutil\"\n\t\"nhentai/nhentai/constant\"\n\t\"nhentai/nhentai/database/active\"\n\t\"nhentai/nhentai/database/cache\"\n\t\"nhentai/nhentai/database/properties\"\n\t\"path\"\n\t\"strconv\"\n)\n\nvar initFlag bool\nvar cachePath string\nvar downloadPath string\n\nfunc InitNHentai(documentDir string) {\n\tif initFlag {\n\t\treturn\n\t}\n\tinitFlag = true\n\tdatabaseDir := path.Join(documentDir, \"database\")\n\tconstant.ObtainDir(databaseDir)\n\tproperties.Init(databaseDir)\n\tcache.Init(databaseDir)\n\tactive.Init(databaseDir)\n\tcachePath = path.Join(documentDir, \"cache\")\n\tconstant.ObtainDir(cachePath)\n\tdownloadPath = path.Join(documentDir, \"download\")\n\tconstant.ObtainDir(downloadPath)\n\tinitClient()\n\tgo initDownload()\n}\n\nfunc cacheImagePath(aliasPath string) string {\n\treturn path.Join(cachePath, aliasPath)\n}\n\nvar methods = map[string]func(string) (string, error){\n\t\"availableWebAddresses\":      availableWebAddresses,\n\t\"availableImgAddresses\":      availableImgAddresses,\n\t\"setProxy\":                   setProxy,\n\t\"getProxy\":                   getProxy,\n\t\"comics\":                     comics,\n\t\"comicsByTagName\":            comicsByTagName,\n\t\"comicsBySearchRaw\":          comicsBySearchRaw,\n\t\"comicInfo\":                  comicInfo,\n\t\"cacheImageByUrlPath\":        cacheImageByUrlPath,\n\t\"loadProperty\":               loadProperty,\n\t\"saveProperty\":               saveProperty,\n\t\"saveViewInfo\":               saveViewInfo,\n\t\"saveViewIndex\":              saveViewIndex,\n\t\"loadLastViewIndexByComicId\": loadLastViewIndexByComicId,\n\t\"downloadComic\":              downloadComic,\n\t\"hasDownload\":                hasDownload,\n\t\"listDownloadComicInfo\":      listDownloadComicInfo,\n\t\"downloadSetDelete\":          downloadSetDelete,\n\t\"httpGet\":                    httpGet,\n\t\"convertImageToJPEG100\":      convertImageToJPEG100,\n\t\"setCookie\":                  setCookie,\n\t\"setUserAgent\":               setUserAgent,\n}\n\nfunc setUserAgent(s string) (string, error) {\n\tclient.UserAgent = s\n\treturn \"\", nil\n}\n\nfunc setCookie(s string) (string, error) {\n\tclient.Cookie = s\n\treturn \"\", nil\n}\n\nfunc FlatInvoke(method string, params string) (string, error) {\n\tif method, ok := methods[method]; ok {\n\t\treturn method(params)\n\t}\n\treturn \"\", errors.New(\"method not found (nhentai main)\")\n}\n\nfunc saveProperty(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tName  string `json:\"name\"`\n\t\tValue string `json:\"value\"`\n\t}\n\tjson.Unmarshal([]byte(params), &paramsStruct)\n\treturn \"\", properties.SaveProperty(paramsStruct.Name, paramsStruct.Value)\n}\n\nfunc loadProperty(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tName         string `json:\"name\"`\n\t\tDefaultValue string `json:\"defaultValue\"`\n\t}\n\tjson.Unmarshal([]byte(params), &paramsStruct)\n\treturn properties.LoadProperty(paramsStruct.Name, paramsStruct.DefaultValue)\n}\n\nfunc saveViewInfo(params string) (string, error) {\n\tvar comic nhentai.ComicInfo\n\tjson.Unmarshal([]byte(params), &comic)\n\treturn \"\", active.SaveViewInfo(comic)\n}\n\nfunc saveViewIndex(params string) (string, error) {\n\tvar paramsStruct struct {\n\t\tInfo  nhentai.ComicInfo `json:\"info\"`\n\t\tIndex int               `json:\"index\"`\n\t}\n\tjson.Unmarshal([]byte(params), &paramsStruct)\n\treturn \"\", active.SaveViewIndex(paramsStruct.Info, paramsStruct.Index)\n}\n\nfunc loadLastViewIndexByComicId(params string) (string, error) {\n\tcomicId, err := strconv.Atoi(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn serialize(active.LoadLastViewIndexByComicId(comicId))\n}\n\nfunc downloadComic(params string) (string, error) {\n\tvar comic nhentai.ComicInfo\n\tjson.Unmarshal([]byte(params), &comic)\n\treturn \"\", active.CreateDownload(comic)\n}\n\nfunc hasDownload(params string) (string, error) {\n\tcomicId, err := strconv.Atoi(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strconv.FormatBool(active.HasDownload(comicId)), nil\n}\n\nfunc listDownloadComicInfo(s string) (string, error) {\n\treturn serialize(active.ListDownloadComicInfo(), nil)\n}\n\nfunc downloadSetDelete(params string) (string, error) {\n\tcomicId, err := strconv.Atoi(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tactive.MarkComicDeleting(comicId)\n\tdownloadRestart = true\n\treturn \"\", nil\n}\n\nfunc httpGet(url string) (string, error) {\n\trsp, err := client.Get(url)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer rsp.Body.Close()\n\tbuff, err := ioutil.ReadAll(rsp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(buff), nil\n}\n"
  },
  {
    "path": "go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//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>English</string>\n        <key>CFBundleExecutable</key>\n        <string>{{.executableName}}</string>\n        <key>CFBundleGetInfoString</key>\n        <string>{{.description}}</string>\n        <key>CFBundleIconFile</key>\n        <string>icon.icns</string>\n        <key>NSHighResolutionCapable</key>\n        <true/>\n        <key>CFBundleIdentifier</key>\n        <string>{{.organizationName}}.{{.packageName}}</string>\n        <key>CFBundleInfoDictionaryVersion</key>\n        <string>6.0</string>\n        <key>CFBundleLongVersionString</key>\n        <string>{{.version}}</string>\n        <key>CFBundleName</key>\n        <string>{{.applicationName}}</string>\n        <key>CFBundlePackageType</key>\n        <string>APPL</string>\n        <key>CFBundleShortVersionString</key>\n        <string>{{.version}}</string>\n        <key>CFBundleSignature</key>\n        <string>{{.organizationName}}.{{.packageName}}</string>\n        <key>CFBundleVersion</key>\n        <string>{{.version}}</string>\n        <key>CSResourcesFileMapped</key>\n        <true/>\n        <key>NSHumanReadableCopyright</key>\n        <string></string>\n        <key>NSPrincipalClass</key>\n        <string>NSApplication</string>\n    </dict>\n</plist>\n"
  },
  {
    "path": "go/packaging/linux-appimage/AppRun.tmpl",
    "content": "#!/bin/sh\ncd \"$(dirname \"$0\")\"\nexec ./build/{{.executableName}}\n"
  },
  {
    "path": "go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl",
    "content": "[Desktop Entry]\nVersion=1.0\nType=Application\nTerminal=false\nCategories=\nComment={{.description}}\nName={{.applicationName}}\nIcon={{.iconPath}}\nExec={{.executablePath}}\n"
  },
  {
    "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>11.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, '11.0'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_ios_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_ios_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "content": "import UIKit\nimport Flutter\nimport Mobile\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n      \n      let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]\n      let applicationSupportsPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]\n\n      MobileMigration(documentsPath, applicationSupportsPath)\n      MobileInitApplication(applicationSupportsPath)\n    \n    let controller = self.window.rootViewController as! FlutterViewController\n    let channel = FlutterMethodChannel.init(name: \"nhentai\", binaryMessenger: controller as! FlutterBinaryMessenger)\n    \n    channel.setMethodCallHandler { (call, result) in\n        Thread {\n            if call.method == \"flatInvoke\" {\n                if let args = call.arguments as? Dictionary<String, Any>,\n                   let method = args[\"method\"] as? String,\n                   let params = args[\"params\"] as? String{\n                    var error: NSError?\n                    let data = MobileFlatInvoke(method, params, &error)\n                    if error != nil {\n                        result(FlutterError(code: \"\", message: error?.localizedDescription, details: \"\"))\n                    }else{\n                        result(data)\n                    }\n                }else{\n                    result(FlutterError(code: \"\", message: \"params error\", details: \"\"))\n                }\n            }\n            else if call.method == \"saveFileToImage\"{\n                if let args = call.arguments as? Dictionary<String, Any>,\n                   let path = args[\"path\"] as? String{\n                    \n                    do {\n                        let fileURL: URL = URL(fileURLWithPath: path)\n                            let imageData = try Data(contentsOf: fileURL)\n                        \n                        if let uiImage = UIImage(data: imageData) {\n                            UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)\n                            result(\"OK\")\n                        }else{\n                            result(FlutterError(code: \"\", message: \"Error loading image \", details: \"\"))\n                        }\n                        \n                    } catch {\n                            result(FlutterError(code: \"\", message: \"Error loading image : \\(error)\", details: \"\"))\n                    }\n                    \n                }else{\n                    result(FlutterError(code: \"\", message: \"params error\", details: \"\"))\n                }\n            }\n            else{\n                result(FlutterMethodNotImplemented)\n            }\n        }.start()\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      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-29x29@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-40x40@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"60x60\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-60x60@3x.png\",\n      \"scale\" : \"3x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"29x29\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-29x29@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"40x40\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-40x40@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@1x.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"76x76\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-76x76@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"83.5x83.5\",\n      \"idiom\" : \"ipad\",\n      \"filename\" : \"Icon-App-83.5x83.5@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"1024x1024\",\n      \"idiom\" : \"ios-marketing\",\n      \"filename\" : \"Icon-App-1024x1024@1x.png\",\n      \"scale\" : \"1x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@2x.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage@3x.png\",\n      \"scale\" : \"3x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "content": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in this directory.\n\nYou can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images."
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"12121\" systemVersion=\"16G29\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"12089\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <layoutGuides>\n                        <viewControllerLayoutGuide type=\"top\" id=\"Ydg-fD-yQy\"/>\n                        <viewControllerLayoutGuide type=\"bottom\" id=\"xbc-2k-c8Z\"/>\n                    </layoutGuides>\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <imageView opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" image=\"LaunchImage\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"YRO-k0-Ey4\">\n                            </imageView>\n                        </subviews>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                        <constraints>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerX\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerX\" id=\"1a2-6s-vTC\"/>\n                            <constraint firstItem=\"YRO-k0-Ey4\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"centerY\" id=\"4X2-HB-R7a\"/>\n                        </constraints>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\"/>\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"LaunchImage\" width=\"168\" height=\"185\"/>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"21225\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" useTraitCollections=\"YES\" colorMatched=\"YES\" initialViewController=\"BYZ-38-t0r\">\n    <device id=\"retina6_0\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"21207\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\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=\"390\" height=\"844\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"custom\" customColorSpace=\"sRGB\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"dkx-z0-nzr\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-26\" y=\"-76\"/>\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>UIFileSharingEnabled</key>\n\t<true/>\n\t<key>LSSupportsOpeningDocumentsInPlace</key>\n\t<true/>\n\t<key>LSApplicationCategoryType</key>\n\t<string>public.app-category.entertainment</string>\n\t<key>NSPhotoLibraryAddUsageDescription</key>\n\t<string>Save images</string>\n\t<key>NSPhotoLibraryUsageDescription</key>\n\t<string>Usage images</string>\n\t<key>CFBundleLocalizations</key>\n\t<array>\n\t\t<string>zh</string>\n\t\t<string>en</string>\n\t</array>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>nhentai</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<false/>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n</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 = 52;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };\n\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };\n\t\t62952ADFC370BEF926A4F77B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */; };\n\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };\n\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };\n\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };\n\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };\n\t\tDDC69A7A275E58C100118CCB /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDC69A79275E58C100118CCB /* Mobile.xcframework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t9705A1C41CF9048500538489 /* Embed Frameworks */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Embed Frameworks\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = \"<group>\"; };\n\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = \"<group>\"; };\n\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = \"<group>\"; };\n\t\t4B2752C74691B678911F7767 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-Runner.debug.xcconfig\"; path = \"Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t5B0F53137A8E31481FC50D89 /* 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\t68115AA88E6B5269DEDB93D6 /* 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\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Runner-Bridging-Header.h\"; sourceTree = \"<group>\"; };\n\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = \"<group>\"; };\n\t\t97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tCB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tDDC69A79275E58C100118CCB /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = ../go/mobile/lib/Mobile.xcframework; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t97C146EB1CF9000F007C117D /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t62952ADFC370BEF926A4F77B /* Pods_Runner.framework in Frameworks */,\n\t\t\t\tDDC69A7A275E58C100118CCB /* Mobile.xcframework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t74F492A9073C29BACAC4C529 /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t4B2752C74691B678911F7767 /* Pods-Runner.debug.xcconfig */,\n\t\t\t\t5B0F53137A8E31481FC50D89 /* Pods-Runner.release.xcconfig */,\n\t\t\t\t68115AA88E6B5269DEDB93D6 /* Pods-Runner.profile.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t9740EEB11CF90186004384FC /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t9740EEB31CF90195004384FC /* Generated.xcconfig */,\n\t\t\t);\n\t\t\tname = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146E51CF9000F007C117D = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tDDC69A79275E58C100118CCB /* Mobile.xcframework */,\n\t\t\t\t9740EEB11CF90186004384FC /* Flutter */,\n\t\t\t\t97C146F01CF9000F007C117D /* Runner */,\n\t\t\t\t97C146EF1CF9000F007C117D /* Products */,\n\t\t\t\t74F492A9073C29BACAC4C529 /* Pods */,\n\t\t\t\tDBE6F67E3D483577BA88D014 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146EF1CF9000F007C117D /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146EE1CF9000F007C117D /* Runner.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146F01CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FA1CF9000F007C117D /* Main.storyboard */,\n\t\t\t\t97C146FD1CF9000F007C117D /* Assets.xcassets */,\n\t\t\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,\n\t\t\t\t97C147021CF9000F007C117D /* Info.plist */,\n\t\t\t\t1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,\n\t\t\t\t1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,\n\t\t\t\t74858FAE1ED2DC5600515810 /* AppDelegate.swift */,\n\t\t\t\t74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tDBE6F67E3D483577BA88D014 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tCB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t97C146ED1CF9000F007C117D /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tF3AD040E0E519A573BB3125F /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t9740EEB61CF901F6004384FC /* Run Script */,\n\t\t\t\t97C146EA1CF9000F007C117D /* Sources */,\n\t\t\t\t97C146EB1CF9000F007C117D /* Frameworks */,\n\t\t\t\t97C146EC1CF9000F007C117D /* Resources */,\n\t\t\t\t9705A1C41CF9048500538489 /* Embed Frameworks */,\n\t\t\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */,\n\t\t\t\t45B4C6EDA02A2684AB90F0C8 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 97C146EE1CF9000F007C117D /* Runner.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t97C146E61CF9000F007C117D /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1300;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t97C146ED1CF9000F007C117D = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 7.3.1;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 97C146E51CF9000F007C117D;\n\t\t\tproductRefGroup = 97C146EF1CF9000F007C117D /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t97C146ED1CF9000F007C117D /* Runner */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t97C146EC1CF9000F007C117D /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,\n\t\t\t\t97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,\n\t\t\t\t97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Thin Binary\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" embed_and_thin\";\n\t\t};\n\t\t45B4C6EDA02A2684AB90F0C8 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9740EEB61CF901F6004384FC /* Run Script */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Run Script\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"/bin/sh \\\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\\" build\";\n\t\t};\n\t\tF3AD040E0E519A573BB3125F /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t97C146EA1CF9000F007C117D /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,\n\t\t\t\t1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t97C146FA1CF9000F007C117D /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C146FB1CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t97C147001CF9000F007C117D /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t249021D3217E4FDB00AE95B9 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 11.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\tIPHONEOS_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t97C147031CF9000F007C117D /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t97C147041CF9000F007C117D /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++0x\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 11.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\tIPHONEOS_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;\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\tIPHONEOS_DEPLOYMENT_TARGET = 11.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_OBJC_BRIDGING_HEADER = \"Runner/Runner-Bridging-Header.h\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t97C146E91CF9000F007C117D /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147031CF9000F007C117D /* Debug */,\n\t\t\t\t97C147041CF9000F007C117D /* Release */,\n\t\t\t\t249021D3217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t97C147061CF9000F007C117D /* Debug */,\n\t\t\t\t97C147071CF9000F007C117D /* Release */,\n\t\t\t\t249021D4217E4FDB00AE95B9 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 97C146E61CF9000F007C117D /* Project object */;\n}\n"
  },
  {
    "path": "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 = \"1300\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n               BuildableName = \"Runner.app\"\n               BlueprintName = \"Runner\"\n               ReferencedContainer = \"container:Runner.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n      </Testables>\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"97C146ED1CF9000F007C117D\"\n            BuildableName = \"Runner.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>PreviewsEnabled</key>\n\t<false/>\n</dict>\n</plist>\n"
  },
  {
    "path": "l10n.yaml",
    "content": "arb-dir: lib/l10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: app_localizations.dart\nuntranslated-messages-file: desiredFileName.txt\n"
  },
  {
    "path": "lib/basic/channels/nhentai.dart",
    "content": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:nhentai/basic/entities/entities.dart';\n\nconst nHentai = NHentai._();\n\nclass NHentai {\n  const NHentai._();\n\n  static const _channel = MethodChannel(\"nhentai\");\n\n  /// 平铺调用, 为了直接与golang进行通信\n  Future<String> _flatInvoke(String method, dynamic params) async {\n    return await _channel.invokeMethod(\"flatInvoke\", {\n      \"method\": method,\n      \"params\": params is String ? params : jsonEncode(params),\n    });\n  }\n\n  /// 设置代理\n  Future setProxy(String proxyUrl) {\n    return _flatInvoke(\"setProxy\", proxyUrl);\n  }\n\n  /// 获取代理\n  Future<String> getProxy() {\n    return _flatInvoke(\"getProxy\", \"\");\n  }\n\n  /// 获取漫画\n  Future<ComicPageData> comics(int page) async {\n    return ComicPageData.fromJson(\n      jsonDecode(await _flatInvoke(\"comics\", \"$page\")),\n    );\n  }\n\n  /// 获取漫画\n  Future<ComicPageData> comicsByTagName(String tagName, int page) async {\n    return ComicPageData.fromJson(jsonDecode(\n      await _flatInvoke(\"comicsByTagName\", {\n        \"tag_name\": tagName,\n        \"page\": page,\n      }),\n    ));\n  }\n\n  /// 获取漫画\n  Future<ComicPageData> comicsBySearchRaw(String raw, int page) async {\n    return ComicPageData.fromJson(jsonDecode(\n      await _flatInvoke(\"comicsBySearchRaw\", {\n        \"raw\": raw,\n        \"page\": page,\n      }),\n    ));\n  }\n\n  /// 漫画详情\n  Future<ComicInfo> comicInfo(int comicId) async {\n    return ComicInfo.formJson(jsonDecode(\n      await _flatInvoke(\"comicInfo\", \"$comicId\"),\n    ));\n  }\n\n  /// 加载图片 (返回路径)\n  Future<String> cacheImageByUrlPath(String url) async {\n    return await _flatInvoke(\"cacheImageByUrlPath\", url);\n  }\n\n  /// 手机端保存图片\n  Future saveFileToImage(String path) async {\n    return _channel.invokeMethod(\"saveFileToImage\", {\n      \"path\": path,\n    });\n  }\n\n  /// 桌面端保存图片\n  Future convertImageToJPEG100(String path, String dir) async {\n    return _flatInvoke(\"convertImageToJPEG100\", {\n      \"path\": path,\n      \"dir\": dir,\n    });\n  }\n\n  /// 安卓版本号\n  Future<int> androidVersion() async {\n    if (Platform.isAndroid) {\n      return await _channel.invokeMethod(\"androidVersion\", {});\n    }\n    return 0;\n  }\n\n  /// 读取配置文件\n  Future<String> loadProperty(String propertyName, String defaultValue) async {\n    return await _flatInvoke(\"loadProperty\", {\n      \"name\": propertyName,\n      \"defaultValue\": defaultValue,\n    });\n  }\n\n  /// 保存配置文件\n  Future<dynamic> saveProperty(String propertyName, String value) {\n    return _flatInvoke(\"saveProperty\", {\n      \"name\": propertyName,\n      \"value\": value,\n    });\n  }\n\n  /// 更新浏览记录 (打开详情页面时)\n  Future<dynamic> saveViewInfo(ComicInfo info) {\n    return _flatInvoke(\"saveViewInfo\", info);\n  }\n\n  /// 更新浏览记录 (打开页面滚动时)\n  Future<dynamic> saveViewIndex(ComicInfo info, int index) {\n    return _flatInvoke(\"saveViewIndex\", {\n      \"info\": info,\n      \"index\": index,\n    });\n  }\n\n  /// 获取最后浏览的页数\n  Future<int> loadLastViewIndexByComicId(int comicId) async {\n    return int.parse(await _flatInvoke(\"loadLastViewIndexByComicId\", comicId));\n  }\n\n  /// 创建一个下载\n  Future<dynamic> downloadComic(ComicInfo comicInfo) {\n    return _flatInvoke(\"downloadComic\", comicInfo);\n  }\n\n  /// 检查有没有下载过\n  Future<bool> hasDownload(int comicId) async {\n    return \"true\" == await _flatInvoke(\"hasDownload\", \"$comicId\");\n  }\n\n  /// 获取所有下载列表\n  Future<List<DownloadComicInfo>> listDownloadComicInfo() async {\n    return List.of(jsonDecode(await _flatInvoke(\"listDownloadComicInfo\", \"\")))\n        .map((e) => DownloadComicInfo.formJson(e))\n        .toList()\n        .cast<DownloadComicInfo>();\n  }\n\n  /// 删除一个下载\n  Future<dynamic> downloadSetDelete(int comicId) async {\n    return _flatInvoke(\"downloadSetDelete\", \"$comicId\");\n  }\n\n  /// HTTP-GET-STRING\n  Future<String> httpGet(String url) async {\n    return await _flatInvoke(\"httpGet\", url);\n  }\n\n  Future setCookie(String cookies) async {\n    return await _flatInvoke(\"setCookie\", cookies);\n  }\n\n  Future setUserAgent(String s) async {\n    return await _flatInvoke(\"setUserAgent\", s);\n  }\n}\n"
  },
  {
    "path": "lib/basic/common/common.dart",
    "content": "import 'package:app_links/app_links.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_styled_toast/flutter_styled_toast.dart';\n\n/// 显示一个toast\nvoid defaultToast(BuildContext context, String title) {\n  showToast(\n    title,\n    context: context,\n    position: StyledToastPosition.center,\n    animation: StyledToastAnimation.scale,\n    reverseAnimation: StyledToastAnimation.fade,\n    duration: const Duration(seconds: 4),\n    animDuration: const Duration(seconds: 1),\n    curve: Curves.elasticOut,\n    reverseCurve: Curves.linear,\n  );\n}\n\n/// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true\nFuture<bool> confirmDialog(BuildContext context, String title,\n    {String content = \"\"}) async {\n  return await showDialog(\n          context: context,\n          builder: (context) => AlertDialog(\n                title: Text(title),\n                content: SingleChildScrollView(\n                  child: ListBody(\n                    children: content == \"\"\n                        ? []\n                        : <Widget>[\n                            Text(content),\n                          ],\n                  ),\n                ),\n                actions: <Widget>[\n                  MaterialButton(\n                    child: Text(AppLocalizations.of(context)!.cancel),\n                    onPressed: () {\n                      Navigator.of(context).pop(false);\n                    },\n                  ),\n                  MaterialButton(\n                    child: Text(AppLocalizations.of(context)!.ok),\n                    onPressed: () {\n                      Navigator.of(context).pop(true);\n                    },\n                  ),\n                ],\n              )) ??\n      false;\n}\n\n/// 显示一个消息提示框\nFuture alertDialog(BuildContext context, String title, String content) {\n  return showDialog(\n      context: context,\n      builder: (context) => AlertDialog(\n            title: Text(title),\n            content: SingleChildScrollView(\n              child: ListBody(\n                children: <Widget>[\n                  Text(content),\n                ],\n              ),\n            ),\n            actions: <Widget>[\n              MaterialButton(\n                child: Text('确定'),\n                onPressed: () {\n                  Navigator.of(context).pop();\n                },\n              ),\n            ],\n          ));\n}\n\n/// stream-filter的替代方法\nList<T> filteredList<T>(List<T> list, bool Function(T) filter) {\n  List<T> result = [];\n  list.forEach((element) {\n    if (filter(element)) {\n      result.add(element);\n    }\n  });\n  return result;\n}\n\n/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容\nFuture<T?> chooseListDialog<T>(\n    BuildContext context, String title, List<T> items,\n    {String? tips}) async {\n  List<Widget> widgets = [];\n  if (tips != null) {\n    widgets.add(Container(\n      padding: const EdgeInsets.fromLTRB(15, 5, 15, 15),\n      child: Text(tips),\n    ));\n  }\n  widgets.addAll(items.map((e) => SimpleDialogOption(\n        onPressed: () {\n          Navigator.of(context).pop(e);\n        },\n        child: Text('$e'),\n      )));\n\n  return showDialog<T>(\n    context: context,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(title),\n        children: widgets,\n      );\n    },\n  );\n}\n\n/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value)\nFuture<T?> chooseMapDialog<T>(\n    BuildContext buildContext, Map<String, T> values, String title) async {\n  return await showDialog<T>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return SimpleDialog(\n        title: Text(title),\n        children: values.entries\n            .map((e) => SimpleDialogOption(\n                  child: Text(e.key),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.value);\n                  },\n                ))\n            .toList(),\n      );\n    },\n  );\n}\n\n/// 输入对话框1\n\nvar _controller = TextEditingController.fromValue(TextEditingValue(text: ''));\n\nFuture<String?> displayTextInputDialog(\n  BuildContext context,\n  String title,\n  String hint,\n  String src,\n  String desc,\n) {\n  _controller.text = src;\n  return showDialog(\n    context: context,\n    builder: (context) {\n      return AlertDialog(\n        title: Text(title),\n        content: SingleChildScrollView(\n          child: ListBody(\n            children: [\n              TextField(\n                controller: _controller,\n                decoration: InputDecoration(hintText: hint),\n              ),\n              desc.isEmpty\n                  ? Container()\n                  : Container(\n                      padding: const EdgeInsets.only(top: 20, bottom: 10),\n                      child: Text(\n                        desc,\n                        style: TextStyle(\n                            fontSize: 12,\n                            color: Theme.of(context)\n                                .textTheme\n                                .bodyText1\n                                ?.color\n                                ?.withOpacity(.5)),\n                      ),\n                    ),\n            ],\n          ),\n        ),\n        actions: <Widget>[\n          MaterialButton(\n            child: const Text('取消'),\n            onPressed: () {\n              Navigator.of(context).pop();\n            },\n          ),\n          MaterialButton(\n            child: const Text('确认'),\n            onPressed: () {\n              Navigator.of(context).pop(_controller.text);\n            },\n          ),\n        ],\n      );\n    },\n  );\n}\n\n/// 将字符串前面加0直至满足len位\nString add0(int num, int len) {\n  var rsp = \"$num\";\n  while (rsp.length < len) {\n    rsp = \"0$rsp\";\n  }\n  return rsp;\n}\n\n/// 格式化时间 2012-34-56\nString formatTimeToDate(String str) {\n  try {\n    var c = DateTime.parse(str);\n    return \"${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)}\";\n  } catch (e) {\n    return \"-\";\n  }\n}\n//\n// /// 格式化时间 2012-34-56 12:34:56\n// String formatTimeToDateTime(String str) {\n//   try {\n//     var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour()));\n//     return \"${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)} ${add0(c.hour, 2)}:${add0(c.minute, 2)}\";\n//   } catch (e) {\n//     return \"-\";\n//   }\n// }\n\n/// 输入对话框2\n\nfinal TextEditingController _textEditController =\n    TextEditingController(text: '');\n\nFuture<String?> inputString(BuildContext context, String title,\n    {String hint = \"\"}) async {\n  _textEditController.clear();\n  return showDialog(\n    context: context,\n    builder: (context) {\n      return AlertDialog(\n        content: Card(\n          child: SingleChildScrollView(\n            child: ListBody(\n              children: [\n                Text(title),\n                Container(\n                  child: TextField(\n                    controller: _textEditController,\n                    decoration: new InputDecoration(\n                      labelText: \"$hint\",\n                    ),\n                  ),\n                ),\n              ],\n            ),\n          ),\n        ),\n        actions: <Widget>[\n          MaterialButton(\n            onPressed: () {\n              Navigator.pop(context);\n            },\n            child: Text('取消'),\n          ),\n          MaterialButton(\n            onPressed: () {\n              Navigator.pop(context, _textEditController.text);\n            },\n            child: Text('确定'),\n          ),\n        ],\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/common/cross.dart",
    "content": "/// 与平台交互的操作\n\nimport 'dart:io';\nimport 'package:clipboard/clipboard.dart';\nimport 'package:filesystem_picker/filesystem_picker.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:permission_handler/permission_handler.dart';\nimport 'package:url_launcher/url_launcher.dart';\nimport 'common.dart';\n\n/// 复制内容到剪切板\nvoid copyToClipBoard(BuildContext context, String string) {\n  if (Platform.isWindows || Platform.isMacOS) {\n    FlutterClipboard.copy(string);\n    defaultToast(context, \"已复制到剪切板\");\n  } else if (Platform.isAndroid) {\n    FlutterClipboard.copy(string);\n    defaultToast(context, \"已复制到剪切板\");\n  }\n}\n\n/// 打开web页面\nFuture<dynamic> openUrl(String url) async {\n  if (await canLaunch(url)) {\n    await launch(\n      url,\n      forceSafariVC: false,\n    );\n  }\n}\n\n/// 保存图片\nFuture<dynamic> saveImage(String path, BuildContext context) async {\n  Future? future;\n  if (Platform.isIOS) {\n    future = nHentai.saveFileToImage(path);\n  } else if (Platform.isAndroid) {\n    future = _saveImageAndroid(path, context);\n  } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {\n    String? folder = await chooseFolder(context);\n    if (folder != null) {\n      future = nHentai.convertImageToJPEG100(path, folder);\n    }\n  } else {\n    defaultToast(context, '暂不支持该平台');\n    return;\n  }\n  if (future == null) {\n    defaultToast(context, '保存取消');\n    return;\n  }\n  try {\n    await future;\n    defaultToast(context, '保存成功');\n  } catch (e, s) {\n    print(\"$e\\n$s\");\n    defaultToast(context, '保存失败');\n  }\n}\n\n/// 保存图片且保持静默, 用于批量导出到相册\nFuture<dynamic> saveImageQuiet(String path, BuildContext context) async {\n  if (Platform.isIOS) {\n    return nHentai.saveFileToImage(path);\n  } else if (Platform.isAndroid) {\n    return _saveImageAndroid(path, context);\n  } else {\n    throw Exception(\"only mobile\");\n  }\n}\n\nFuture<dynamic> _saveImageAndroid(String path, BuildContext context) async {\n  var p = await Permission.storage.request();\n  if (!p.isGranted) {\n    return;\n  }\n  return nHentai.saveFileToImage(path);\n}\n\n/// 选择一个文件夹用于保存文件\nFuture<String?> chooseFolder(BuildContext context) async {\n  return FilesystemPicker.open(\n    title: '选择一个文件夹',\n    pickText: '将文件保存到这里',\n    context: context,\n    fsType: FilesystemType.folder,\n    rootDirectory: Directory(await currentChooserRoot()),\n  );\n}\n\n/// 复制对话框\nvoid confirmCopy(BuildContext context, String content) async {\n  if (await confirmDialog(context, \"复制\", content: content)) {\n    copyToClipBoard(context, content);\n  }\n}\n\nFuture<String> currentChooserRoot() async {\n  if (Platform.isAndroid) {\n    if (await nHentai.androidVersion() >= 30) {\n      if (!(await Permission.manageExternalStorage.request()).isGranted) {\n        throw Exception(\"申请权限被拒绝\");\n      }\n    } else {\n      if (!(await Permission.storage.request()).isGranted) {\n        throw Exception(\"申请权限被拒绝\");\n      }\n    }\n  }\n  if (Platform.isWindows) {\n    return '/';\n  } else if (Platform.isMacOS) {\n    return '/Users';\n  } else if (Platform.isLinux) {\n    return '/';\n  } else if (Platform.isAndroid) {\n    return '/storage/emulated/0';\n  } else {\n    throw 'error';\n  }\n}\n"
  },
  {
    "path": "lib/basic/common/error_types.dart",
    "content": "const ERROR_TYPE_NETWORK = \"NETWORK_ERROR\";\nconst ERROR_TYPE_PERMISSION = \"PERMISSION_ERROR\";\nconst ERROR_TYPE_TIME = \"TIME_ERROR\";\nconst ERROR_TYPE_UNDER_REVIEW = \"UNDER_VIEW_ERROR\";\n\n// 错误的类型, 方便照展示和谐的提示\nString errorType(String error) {\n  // EXCEPTION\n  // Get \"https://****\": net/http: TLS handshake timeout\n  // Get \"https://****\": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused\n  // Get \"https://****\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)\n  if (error.contains(\"timeout\") ||\n      error.contains(\"connection refused\") ||\n      error.contains(\"deadline\") ||\n      error.contains(\"connection abort\")) {\n    return ERROR_TYPE_NETWORK;\n  }\n  if (error.contains(\"permission denied\")) {\n    return ERROR_TYPE_PERMISSION;\n  }\n  if (error.contains(\"time is not synchronize\")) {\n    return ERROR_TYPE_TIME;\n  }\n  if (error.contains(\"under review\")) {\n    return ERROR_TYPE_UNDER_REVIEW;\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "lib/basic/configs/proxy.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\nconst _propertyName = \"proxy\";\nlate String _proxy;\n\nFuture initProxy() async {\n  _proxy = await nHentai.getProxy();\n}\n\nString currentProxy() {\n  return _proxy;\n}\n\nFuture chooseProxy(BuildContext context) async {\n  final newProxy = await displayTextInputDialog(\n      context,\n      AppLocalizations.of(context)!.proxy,\n      \"socks5://host:port/\",\n      _proxy,\n      AppLocalizations.of(context)!.inputProxyDesc);\n  if (newProxy != null) {\n    await nHentai.saveProperty(_propertyName, newProxy);\n    _proxy = newProxy;\n  }\n}\n\nWidget proxySetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\n          AppLocalizations.of(context)!.proxy,\n        ),\n        subtitle: Text(_proxy),\n        onTap: () async {\n          await chooseProxy(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n"
  },
  {
    "path": "lib/basic/configs/reader_direction.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\nenum ReaderDirection {\n  topToBottom,\n  leftToRight,\n  rightToLeft,\n}\n\nconst _propertyName = \"readerDirection\";\nlate ReaderDirection _readerDirection;\n\nFuture initReaderDirection() async {\n  _readerDirection = _fromString(await nHentai.loadProperty(_propertyName, \"\"));\n}\n\nReaderDirection _fromString(String valueForm) {\n  for (var value in ReaderDirection.values) {\n    if (value.toString() == valueForm) {\n      return value;\n    }\n  }\n  return ReaderDirection.values.first;\n}\n\nReaderDirection currentReaderDirection() {\n  return _readerDirection;\n}\n\nString readerDirectionName(ReaderDirection direction, BuildContext context) {\n  switch (direction) {\n    case ReaderDirection.topToBottom:\n      return AppLocalizations.of(context)!.topToBottom;\n    case ReaderDirection.leftToRight:\n      return AppLocalizations.of(context)!.leftToRight;\n    case ReaderDirection.rightToLeft:\n      return AppLocalizations.of(context)!.rightToLeft;\n  }\n}\n\nFuture chooseReaderDirection(BuildContext context) async {\n  final Map<String, ReaderDirection> map = {};\n  for (var element in ReaderDirection.values) {\n    map[readerDirectionName(element, context)] = element;\n  }\n  final newReaderDirection = await chooseMapDialog(\n    context,\n    map,\n    AppLocalizations.of(context)!.chooseReaderDirection,\n  );\n  if (newReaderDirection != null) {\n    await nHentai.saveProperty(_propertyName, \"$newReaderDirection\");\n    _readerDirection = newReaderDirection;\n  }\n}\n"
  },
  {
    "path": "lib/basic/configs/reader_type.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\nenum ReaderType {\n  webtoon,\n  gallery,\n}\n\nconst _propertyName = \"readerType\";\nlate ReaderType _readerType;\n\nFuture initReaderType() async {\n  _readerType = _fromString(await nHentai.loadProperty(_propertyName, \"\"));\n}\n\nReaderType _fromString(String valueForm) {\n  for (var value in ReaderType.values) {\n    if (value.toString() == valueForm) {\n      return value;\n    }\n  }\n  return ReaderType.values.first;\n}\n\nReaderType currentReaderType() {\n  return _readerType;\n}\n\nString readerTypeName(ReaderType type, BuildContext context) {\n  switch (type) {\n    case ReaderType.webtoon:\n      return AppLocalizations.of(context)!.webtoon;\n    case ReaderType.gallery:\n      return AppLocalizations.of(context)!.gallery;\n  }\n}\n\nFuture chooseReaderType(BuildContext context) async {\n  final Map<String, ReaderType> map = {};\n  for (var element in ReaderType.values) {\n    map[readerTypeName(element, context)] = element;\n  }\n  final newReaderType = await chooseMapDialog(\n    context,\n    map,\n    AppLocalizations.of(context)!.chooseReaderType,\n  );\n  if (newReaderType != null) {\n    await nHentai.saveProperty(_propertyName, \"$newReaderType\");\n    _readerType = newReaderType;\n  }\n}\n"
  },
  {
    "path": "lib/basic/configs/themes.dart",
    "content": "/// 主题\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\n\n// 字体相关\n\nconst _fontFamilyProperty = \"fontFamily\";\n\nString? _fontFamily;\n\nFuture initFont() async {\n  var defaultFont = \"\";\n  _fontFamily = await nHentai.loadProperty(_fontFamilyProperty, defaultFont);\n}\n\nThemeData _fontThemeData(bool dark) {\n  return ThemeData(\n    brightness: dark ? Brightness.dark : Brightness.light,\n    fontFamily: _fontFamily == \"\" ? null : _fontFamily,\n  );\n}\n\nFuture<void> inputFont(BuildContext context) async {\n  var font = await displayTextInputDialog(\n    context, \"字体\", \"请输入字体\", \"$_fontFamily\",\n    \"请输入字体的名称, 例如宋体/黑体, 如果您保存后没有发生变化, 说明字体无法使用或名称错误, 可以去参考C:\\\\Windows\\\\Fonts寻找您的字体。\",\n  );\n  if (font != null) {\n    await nHentai.saveProperty(_fontFamilyProperty, font);\n    _fontFamily = font;\n    _changeThemeByCode(_themeCode);\n  }\n}\n\nWidget fontSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(\"字体\"),\n        subtitle: Text(\"$_fontFamily\"),\n        onTap: () async {\n          await inputFont(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\n// 主题相关\n\n// 主题包\nabstract class _ThemePackage {\n  String code();\n\n  String name();\n\n  ThemeData themeData(ThemeData rawData);\n}\n\nclass _OriginTheme extends _ThemePackage {\n  @override\n  String code() => \"origin\";\n\n  @override\n  String name() => \"ORIGIN\";\n\n  @override\n  ThemeData themeData(ThemeData rawData) => rawData;\n}\n\nclass _PinkTheme extends _ThemePackage {\n  @override\n  String code() => \"pink\";\n\n  @override\n  String name() => \"PINK\";\n\n  @override\n  ThemeData themeData(ThemeData rawData) =>\n      rawData.copyWith(\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light,\n          color: Colors.pink.shade200,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.pink[300],\n          unselectedItemColor: Colors.grey[500],\n        ),\n        dividerColor: Colors.grey.shade200,\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n      );\n}\n\nclass _BlackTheme extends _ThemePackage {\n  @override\n  String code() => \"black\";\n\n  @override\n  String name() => \"BLACK\";\n\n  @override\n  ThemeData themeData(ThemeData rawData) =>\n      rawData.copyWith(\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light,\n          color: Colors.grey.shade800,\n          iconTheme: const IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.white,\n          unselectedItemColor: Colors.grey[400],\n          backgroundColor: Colors.grey.shade800,\n        ),\n        dividerColor: Colors.grey.shade200,\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n      );\n}\n\nclass _DarkTheme extends _ThemePackage {\n  @override\n  String code() => \"dark\";\n\n  @override\n  String name() => \"DARK\";\n\n  @override\n  ThemeData themeData(ThemeData rawData) =>\n      rawData.copyWith(\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          secondary: Colors.pink.shade200,\n        ),\n        appBarTheme: const AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light,\n          color: Color(0xFF1E1E1E),\n          iconTheme: IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          selectedItemColor: Colors.white,\n          unselectedItemColor: Colors.grey.shade300,\n          backgroundColor: Colors.grey.shade900,\n        ),\n        primaryColor: Colors.pink.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.pink.shade200,\n          selectionColor: Colors.pink.shade300.withAlpha(150),\n          selectionHandleColor: Colors.pink.shade300.withAlpha(200),\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.pink.shade200),\n          ),\n        ),\n      );\n}\n\nclass _DustyBlueTheme extends _ThemePackage {\n  @override\n  String code() => \"dustyBlue\";\n\n  @override\n  String name() => \"DUSTY BLUE\";\n\n  @override\n  ThemeData themeData(ThemeData rawData) =>\n      rawData.copyWith(\n        scaffoldBackgroundColor: Color.alphaBlend(\n            Color(0x11999999), Color(0xff20253b)),\n        cardColor: Color.alphaBlend(Color(0x11AAAAAA), Color(0xff20253b)),\n        brightness: Brightness.light,\n        colorScheme: ColorScheme.light(\n          secondary: Colors.blue.shade200,\n        ),\n        appBarTheme: AppBarTheme(\n          systemOverlayStyle: SystemUiOverlayStyle.light,\n          color: Color(0xff20253b),\n          iconTheme: IconThemeData(\n            color: Colors.white,\n          ),\n        ),\n        bottomNavigationBarTheme: BottomNavigationBarThemeData(\n          backgroundColor: Color(0xff191b26),\n          selectedItemColor: Colors.blue.shade200,\n          unselectedItemColor: Colors.grey.shade500,\n        ),\n        dividerColor: Colors.grey.shade800,\n        primaryColor: Colors.blue.shade200,\n        textSelectionTheme: TextSelectionThemeData(\n          cursorColor: Colors.blue.shade200,\n          selectionColor: Colors.blue.shade900,\n          selectionHandleColor: Colors.blue.shade800,\n        ),\n        inputDecorationTheme: InputDecorationTheme(\n          focusedBorder: UnderlineInputBorder(\n            borderSide: BorderSide(color: Colors.blue.shade500),\n          ),\n        ),\n      );\n}\n\nvar _darkTheme = _DarkTheme();\nvar _dustyBlueTheme = _DustyBlueTheme();\n\nfinal _themePackages = <_ThemePackage>[\n  _OriginTheme(),\n  _PinkTheme(),\n  _BlackTheme(),\n  _darkTheme,\n  _dustyBlueTheme,\n];\n\n// 主题更换事件\nvar themeEvent = Event<EventArgs>();\n\nconst _themePropertyName = \"theme\";\nconst _defaultThemeCode = \"dark\";\n\nString? _themeCode;\nThemeData? _themeData;\nThemeData? _currentDarkTheme;\nbool _androidNightMode = false;\n\nString currentThemeName() {\n  for (var package in _themePackages) {\n    if (_themeCode == package.code()) {\n      return package.name();\n    }\n  }\n  return \"\";\n}\n\nThemeData? currentThemeData() {\n  return _themeData;\n}\n\nThemeData? currentDarkTheme() {\n  return _currentDarkTheme;\n}\n\n// 根据Code选择主题, 并发送主题更换事件\nvoid _changeThemeByCode(String? themeCode) {\n  _ThemePackage? _themePackage;\n  for (var package in _themePackages) {\n    if (themeCode == package.code()) {\n      _themeCode = themeCode;\n      _themePackage = package;\n      break;\n    }\n  }\n  if (_themePackage != null) {\n    _themeData = _themePackage.themeData(\n      _fontThemeData(\n          _themePackage == _darkTheme || _themePackage == _dustyBlueTheme),\n    );\n  }\n  _currentDarkTheme = _androidNightMode\n      ? _darkTheme.themeData(_fontThemeData(true))\n      : _themeData;\n  themeEvent.broadcast();\n}\n\n// 为了匹配安卓夜间模式增加的配置文件\nconst _nightModePropertyName = \"androidNightMode\";\n\nFuture<dynamic> initTheme() async {\n  _androidNightMode =\n      await nHentai.loadProperty(_nightModePropertyName, \"true\") == \"true\";\n  _changeThemeByCode(\n    await nHentai.loadProperty(_themePropertyName, _defaultThemeCode),\n  );\n}\n\n// 选择主题的对话框\nFuture<dynamic> chooseTheme(BuildContext buildContext) async {\n  var androidVersion = 0; // todo\n  String? theme = await showDialog<String>(\n    context: buildContext,\n    builder: (BuildContext context) {\n      return StatefulBuilder(\n          builder: (BuildContext context, StateSetter setState) {\n            var list = <SimpleDialogOption>[];\n            if (androidVersion >= 29) {\n              Future onChange(bool? v) async {\n                if (v != null) {\n                  await nHentai.saveProperty(\n                      _nightModePropertyName, \"$v\");\n                  _androidNightMode = v;\n                }\n                _changeThemeByCode(_themeCode);\n              }\n              list.add(\n                SimpleDialogOption(\n                  child: GestureDetector(\n                    onTap: () {\n                      onChange(!_androidNightMode);\n                    },\n                    child: Container(\n                      margin: const EdgeInsets.only(top: 3, bottom: 3),\n                      decoration: BoxDecoration(\n                        border: Border(\n                          top: BorderSide(\n                            color: Theme\n                                .of(context)\n                                .dividerColor,\n                            width: 0.5,\n                          ),\n                          bottom: BorderSide(\n                              color: Theme\n                                  .of(context)\n                                  .dividerColor,\n                              width: 0.5\n                          ),\n                        ),\n                      ),\n                      child: Row(\n                        children: [\n                          Checkbox(\n                            value: _androidNightMode,\n                            onChanged: onChange,\n                          ),\n                          Text(AppLocalizations.of(context)!.themeIntoDarkFlowSystem),\n                        ],\n                      ),\n                    ),\n                  ),\n                ),\n              );\n            }\n            list.addAll(_themePackages\n                .map((e) =>\n                SimpleDialogOption(\n                  child: Text(e.name()),\n                  onPressed: () {\n                    Navigator.of(context).pop(e.code());\n                  },\n                )\n            ));\n            return SimpleDialog(\n              title: Text(AppLocalizations.of(context)!.chooseTheme),\n              children: list,\n            );\n          });\n    },\n  );\n  if (theme != null) {\n    nHentai.saveProperty(_themePropertyName, theme);\n    _changeThemeByCode(theme);\n  }\n}\n\nWidget themeSetting(BuildContext context) {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return\n        ListTile(\n          onTap: () async {\n            await chooseTheme(context);\n            setState(() {});\n          },\n          title: Text(AppLocalizations.of(context)!.theme),\n          subtitle: Text(currentThemeName()),\n        );\n    },);\n}"
  },
  {
    "path": "lib/basic/configs/version.dart",
    "content": "import 'package:flutter/gestures.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:async' show Future;\nimport 'dart:convert';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart' show rootBundle;\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'package:nhentai/basic/common/cross.dart';\nimport 'package:nhentai/screens/components/Badged.dart';\n\nconst _releasesUrl = \"https://github.com/niuhuan/nhentai-cross/releases\";\nconst _versionUrl =\n    \"https://api.github.com/repos/niuhuan/nhentai-cross/releases/latest\";\nconst _versionAssets = 'lib/assets/version.txt';\nRegExp _versionExp = RegExp(r\"^v\\d+\\.\\d+.\\d+$\");\n\nlate String _version;\nString? _latestVersion;\nString? _latestVersionInfo;\n\nconst _propertyName = \"checkVersionPeriod\";\nlate int _period = -1;\n\nFuture initVersion() async {\n  // 当前版本\n  try {\n    _version = (await rootBundle.loadString(_versionAssets)).trim();\n  } catch (e) {\n    _version = \"dirty\";\n  }\n  // 检查周期\n  _period = int.parse(await nHentai.loadProperty(_propertyName, \"0\"));\n  if (_period > 0) {\n    if (DateTime.now().millisecondsSinceEpoch > _period) {\n      await nHentai.saveProperty(_propertyName, \"0\");\n      _period = 0;\n    }\n  }\n}\n\nvar versionEvent = Event<EventArgs>();\n\nString currentVersion() {\n  return _version;\n}\n\nString? latestVersion() {\n  return _latestVersion;\n}\n\nString? latestVersionInfo() {\n  return _latestVersionInfo;\n}\n\nFuture autoCheckNewVersion() {\n  if (_period != 0) {\n    // -1 不检查, >0 未到检查时间\n    return Future.value();\n  }\n  return _versionCheck();\n}\n\nFuture manualCheckNewVersion(BuildContext context) async {\n  try {\n    defaultToast(context, AppLocalizations.of(context)!.checkingNewVersion);\n    await _versionCheck();\n    defaultToast(context, AppLocalizations.of(context)!.success);\n  } catch (e) {\n    defaultToast(context, AppLocalizations.of(context)!.failed + \" : $e\");\n  }\n}\n\nbool dirtyVersion() {\n  return !_versionExp.hasMatch(_version);\n}\n\n// maybe exception\nFuture _versionCheck() async {\n  if (_versionExp.hasMatch(_version)) {\n    var json = jsonDecode(await nHentai.httpGet(_versionUrl));\n    if (json[\"name\"] != null) {\n      String latestVersion = (json[\"name\"]);\n      if (latestVersion != _version) {\n        _latestVersion = latestVersion;\n        _latestVersionInfo = json[\"body\"] ?? \"\";\n      }\n    }\n  } // else dirtyVersion\n  versionEvent.broadcast();\n}\n\nString _periodText(BuildContext context) {\n  if (_period < 0) {\n    return AppLocalizations.of(context)!.disabled;\n  }\n  if (_period == 0) {\n    return AppLocalizations.of(context)!.enabled;\n  }\n  return AppLocalizations.of(context)!.nextTime +\n      \" : \" +\n      formatDateTimeToDateTime(\n        DateTime.fromMillisecondsSinceEpoch(_period),\n      );\n}\n\nFuture _choosePeriod(BuildContext context) async {\n  var result = await chooseMapDialog(\n    context,\n    {\n      AppLocalizations.of(context)!.enable: 0,\n      AppLocalizations.of(context)!.aWeek: 1,\n      AppLocalizations.of(context)!.aMonth: 2,\n      AppLocalizations.of(context)!.aYear: 3,\n      AppLocalizations.of(context)!.disable: 4,\n    },\n    AppLocalizations.of(context)!.autoCheckNewVersion,\n    // todo tips: \"重启后红点会消失\",\n  );\n  switch (result) {\n    case 0:\n      await nHentai.saveProperty(_propertyName, \"0\");\n      _period = 0;\n      break;\n    case 1:\n      var time = DateTime.now().millisecondsSinceEpoch + (1000 * 3600 * 24 * 7);\n      await nHentai.saveProperty(_propertyName, \"$time\");\n      _period = time;\n      break;\n    case 2:\n      var time =\n          DateTime.now().millisecondsSinceEpoch + (1000 * 3600 * 24 * 30);\n      await nHentai.saveProperty(_propertyName, \"$time\");\n      _period = time;\n      break;\n    case 3:\n      var time =\n          DateTime.now().millisecondsSinceEpoch + (1000 * 3600 * 24 * 365);\n      await nHentai.saveProperty(_propertyName, \"$time\");\n      _period = time;\n      break;\n    case 4:\n      await nHentai.saveProperty(_propertyName, \"-1\");\n      _period = -1;\n      break;\n  }\n}\n\nWidget autoUpdateCheckSetting() {\n  return StatefulBuilder(\n    builder: (BuildContext context, void Function(void Function()) setState) {\n      return ListTile(\n        title: Text(AppLocalizations.of(context)!.autoCheckNewVersion),\n        subtitle: Text(_periodText(context)),\n        onTap: () async {\n          await _choosePeriod(context);\n          setState(() {});\n        },\n      );\n    },\n  );\n}\n\nString formatDateTimeToDateTime(DateTime c) {\n  try {\n    return \"${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)} ${add0(c.hour, 2)}:${add0(c.minute, 2)}\";\n  } catch (e) {\n    return \"-\";\n  }\n}\n\nclass VersionInfo extends StatefulWidget {\n  const VersionInfo({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _VersionInfoState();\n}\n\nclass _VersionInfoState extends State<VersionInfo> {\n  @override\n  Widget build(BuildContext context) {\n    return Container(\n      padding: const EdgeInsets.only(left: 20, right: 20),\n      child: Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          Text(\n            '软件版本 : $_version',\n            style: const TextStyle(\n              height: 1.3,\n            ),\n          ),\n          Row(\n            children: [\n              const Text(\n                \"检查更新 : \",\n                style: TextStyle(\n                  height: 1.3,\n                ),\n              ),\n              \"dirty\" == _version\n                  ? _buildDirty()\n                  : _buildNewVersion(_latestVersion),\n              Expanded(child: Container()),\n            ],\n          ),\n          _buildNewVersionInfo(_latestVersionInfo),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildNewVersion(String? latestVersion) {\n    if (latestVersion != null) {\n      return Text.rich(\n        TextSpan(\n          children: [\n            WidgetSpan(\n              child: Badged(\n                child: Container(\n                  padding: const EdgeInsets.only(right: 12),\n                  child: Text(\n                    latestVersion,\n                    style: const TextStyle(height: 1.3),\n                  ),\n                ),\n                badge: \"1\",\n              ),\n            ),\n            const TextSpan(text: \"  \"),\n            TextSpan(\n              text: \"去下载\",\n              style: TextStyle(\n                height: 1.3,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => openUrl(_releasesUrl),\n            ),\n          ],\n        ),\n      );\n    }\n    return Text.rich(\n      TextSpan(\n        children: [\n          const TextSpan(text: \"未检测到新版本\", style: TextStyle(height: 1.3)),\n          WidgetSpan(\n            alignment: PlaceholderAlignment.middle,\n            child: Container(\n              padding: const EdgeInsets.all(4),\n              margin: const EdgeInsets.only(left: 3, right: 3),\n              decoration: const BoxDecoration(\n                borderRadius: BorderRadius.all(Radius.circular(20)),\n              ),\n            ),\n          ),\n          TextSpan(\n            text: \"检查更新\",\n            style: TextStyle(\n              height: 1.3,\n              color: Theme.of(context).colorScheme.primary,\n            ),\n            recognizer: TapGestureRecognizer()\n              ..onTap = () => manualCheckNewVersion(context),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildDirty() {\n    return Text.rich(\n      TextSpan(\n        text: \"下载RELEASE版\",\n        style: TextStyle(\n          height: 1.3,\n          color: Theme.of(context).colorScheme.primary,\n        ),\n        recognizer: TapGestureRecognizer()..onTap = () => openUrl(_releasesUrl),\n      ),\n    );\n  }\n\n  Widget _buildNewVersionInfo(String? latestVersionInfo) {\n    if (latestVersionInfo != null) {\n      return Column(\n        crossAxisAlignment: CrossAxisAlignment.start,\n        children: [\n          const Divider(),\n          const Text(\"更新内容:\"),\n          Container(\n            padding: EdgeInsets.all(15),\n            child: Text(\n              latestVersionInfo,\n              style: TextStyle(),\n            ),\n          ),\n        ],\n      );\n    }\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Divider(),\n        Container(\n          padding: EdgeInsets.all(15),\n          child: Text.rich(\n            TextSpan(\n              text: \"去RELEASE仓库\",\n              style: TextStyle(\n                height: 1.3,\n                color: Theme.of(context).colorScheme.primary,\n              ),\n              recognizer: TapGestureRecognizer()\n                ..onTap = () => openUrl(_releasesUrl),\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/basic/entities/entities.dart",
    "content": "import 'package:flutter/cupertino.dart';\n\nclass PageData {\n  late int pageCount;\n\n  PageData.fromJson(Map<String, dynamic> json) {\n    pageCount = json[\"page_count\"] ?? 0;\n  }\n\n  PageData(this.pageCount);\n}\n\nclass ComicPageData extends PageData {\n  late List<ComicSimple> records;\n\n  ComicPageData.fromJson(Map<String, dynamic> json) : super.fromJson(json) {\n    records = (json[\"records\"] ?? [])\n        .map((e) => ComicSimple.fromJson(e))\n        .toList()\n        .cast<ComicSimple>();\n  }\n\n  ComicPageData(int pageCount, this.records) : super(pageCount);\n}\n\nclass ComicSimple {\n  late int id;\n  late int mediaId;\n  late String title;\n  late List<int> tagIds;\n  late String lang;\n  late String thumb;\n  late int thumbWidth;\n  late int thumbHeight;\n\n  ComicSimple.fromJson(Map<String, dynamic> json) {\n    id = json[\"id\"] ?? 0;\n    title = json[\"title\"] ?? \"\";\n    mediaId = json[\"media_id\"] ?? 0;\n    tagIds = (json[\"records\"] ?? []).cast<int>();\n    lang = json[\"lang\"] ?? \"\";\n    thumb = json[\"thumb\"] ?? \"\";\n    thumbWidth = json[\"thumb_width\"] ?? 1;\n    thumbHeight = json[\"thumb_height\"] ?? 1;\n  }\n}\n\nclass ComicInfo {\n  late int id;\n  late int mediaId;\n  late ComicInfoTitle title;\n  late ComicImages images;\n  late String scanlator;\n  late int uploadDate;\n  late List<ComicInfoTag> tags;\n  late int numPages;\n  late int numFavorites;\n\n  ComicInfo.formJson(Map<String, dynamic> json) {\n    id = json[\"id\"] ?? 0;\n    mediaId = json[\"media_id\"] ?? 0;\n    title = ComicInfoTitle.fromJson(json[\"title\"]);\n    images = ComicImages.formJson(json[\"images\"]);\n    scanlator = json[\"scanlator\"] ?? \"\";\n    uploadDate = json[\"upload_date\"] ?? 0;\n    tags = (json[\"tags\"] ?? [])\n        .map((e) => ComicInfoTag.formJson(e))\n        .toList()\n        .cast<ComicInfoTag>();\n    numPages = json[\"num_pages\"] ?? 0;\n    numFavorites = json[\"num_favorites\"] ?? 0;\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      \"id\": id,\n      \"media_id\": mediaId,\n      \"title\": title,\n      \"images\": images,\n      \"scanlator\": scanlator,\n      \"upload_date\": uploadDate,\n      \"tags\": tags,\n      \"num_pages\": numPages,\n      \"num_favorites\": numFavorites,\n    };\n  }\n}\n\nclass ComicInfoTitle {\n  late String english;\n  late String japanese;\n  late String pretty;\n\n  ComicInfoTitle.fromJson(Map<String, dynamic> json) {\n    english = json[\"english\"] ?? \"\";\n    japanese = json[\"japanese\"] ?? \"\";\n    pretty = json[\"pretty\"] ?? \"\";\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      \"english\": english,\n      \"japanese\": japanese,\n      \"pretty\": pretty,\n    };\n  }\n}\n\nclass ComicImages {\n  late List<ImageInfo> pages;\n  late ImageInfo cover;\n  late ImageInfo thumbnail;\n\n  ComicImages.formJson(Map<String, dynamic> json) {\n    pages = List.of(json[\"pages\"])\n        .map((e) => ImageInfo.formJson(e))\n        .toList()\n        .cast<ImageInfo>();\n    cover = ImageInfo.formJson(json[\"cover\"]);\n    thumbnail = ImageInfo.formJson(json[\"thumbnail\"]);\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      \"pages\": pages,\n      \"cover\": cover,\n      \"thumbnail\": thumbnail,\n    };\n  }\n}\n\nclass ImageInfo {\n  late String t;\n  late int w;\n  late int h;\n\n  ImageInfo.formJson(Map<String, dynamic> json) {\n    t = json[\"t\"];\n    w = json[\"w\"];\n    h = json[\"h\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      \"t\": t,\n      \"w\": w,\n      \"h\": h,\n    };\n  }\n}\n\nclass ComicInfoTag {\n  late int id;\n  late String name;\n  late int count;\n  late String type;\n  late String url;\n\n  ComicInfoTag.formJson(Map<String, dynamic> json) {\n    id = json[\"id\"];\n    name = json[\"name\"];\n    count = json[\"count\"];\n    type = json[\"type\"];\n    url = json[\"url\"];\n  }\n\n  Map<String, dynamic> toJson() {\n    return {\n      \"id\": id,\n      \"name\": name,\n      \"count\": count,\n      \"type\": type,\n      \"url\": url,\n    };\n  }\n}\n\nclass DownloadComicInfo extends ComicInfo {\n  late int downloadStatus;\n\n  DownloadComicInfo.formJson(Map<String, dynamic> json) : super.formJson(json) {\n    downloadStatus = json[\"download_status\"] ?? 0;\n  }\n}\n"
  },
  {
    "path": "lib/l10n/app_en.arb",
    "content": "{\n  \"initializing\": \"Initializing\",\n  \"settings\": \"Settings\",\n  \"none\": \"None\",\n  \"webAddress\": \"Web IP redirect\",\n  \"chooseWebAddress\": \"Choose web domain IP\",\n  \"imgAddress\": \"Image domain IP redirect\",\n  \"chooseImgAddress\": \"Choose Image domain IP\",\n  \"theme\": \"Theme\",\n  \"chooseTheme\": \"Choose theme\",\n  \"themeIntoDarkFlowSystem\": \"Flow system to dark mode\",\n  \"loading\": \"Loading\",\n  \"errorAndTapRetry\": \"Error, tap to retry\",\n  \"allComics\": \"All comics\",\n  \"search\": \"Search\",\n  \"searchContent\": \"Search content\",\n  \"yes\": \"Yes\",\n  \"no\": \"No\",\n  \"ok\": \"Ok\",\n  \"cancel\": \"Cancel\",\n  \"addFilter\": \"Add filter\",\n  \"exclusion\": \"Exclusion\",\n  \"type\": \"Type\",\n  \"content\": \"Content\",\n  \"tag\": \"Tag\",\n  \"chooseAction\": \"Choose action\",\n  \"saveImage\": \"Save image\",\n  \"previewImage\": \"Preview image\",\n  \"questionDownloadComic\": \"Download this comic?\",\n  \"download\": \"Download\",\n  \"checkingNewVersion\": \"Checking new version\",\n  \"success\": \"Success\",\n  \"failed\": \"Failed\",\n  \"autoCheckNewVersion\": \"Auto check new version\",\n  \"disabled\": \"Disabled\",\n  \"disable\": \"Disable\",\n  \"enabled\": \"Enabled\",\n  \"enable\": \"Enable\",\n  \"nextTime\": \"Next time\",\n  \"aWeek\": \"A week\",\n  \"aMonth\": \"A month\",\n  \"aYear\": \"A year\",\n  \"delete\": \"Delete\",\n  \"deleting\": \"Deleting\",\n  \"chooseReaderDirection\": \"Choose reader direction\",\n  \"topToBottom\": \"Top to bottom\",\n  \"leftToRight\": \"Left to right\",\n  \"rightToLeft\": \"Right to left\",\n  \"chooseReaderType\": \"Choose reader type\",\n  \"webtoon\": \"WebToon\",\n  \"gallery\": \"Gallery\",\n  \"proxy\": \"Proxy\",\n  \"inputProxyDesc\": \"socks5://host:port/\"\n}\n"
  },
  {
    "path": "lib/l10n/app_zh.arb",
    "content": "{\n  \"initializing\": \"启动中\",\n  \"settings\": \"设置\",\n  \"none\": \"无\",\n  \"webAddress\": \"网站IP重定向\",\n  \"chooseWebAddress\": \"选择网站IP\",\n  \"imgAddress\": \"图片IP重定向\",\n  \"chooseImgAddress\": \"选择图片IP\",\n  \"theme\": \"主题\",\n  \"chooseTheme\": \"选择主题\",\n  \"themeIntoDarkFlowSystem\": \"随手机进入黑暗模式\",\n  \"loading\": \"加载中\",\n  \"errorAndTapRetry\": \"出错了, 点击重试\",\n  \"allComics\": \"所有漫画\",\n  \"search\": \"搜索\",\n  \"searchContent\": \"搜索内容\",\n  \"yes\": \"是\",\n  \"no\": \"否\",\n  \"ok\": \"确认\",\n  \"cancel\": \"取消\",\n  \"addFilter\": \"增加过滤条件\",\n  \"exclusion\": \"排除\",\n  \"type\": \"类型\",\n  \"content\": \"内容\",\n  \"tag\": \"标签\",\n  \"chooseAction\": \"请选择\",\n  \"saveImage\": \"保存图片\",\n  \"previewImage\": \"预览图片\",\n  \"questionDownloadComic\": \"下载这个漫画吗？\",\n  \"download\": \"下载\",\n  \"checkingNewVersion\": \"正在检查更新\",\n  \"success\": \"成功\",\n  \"failed\": \"失败\",\n  \"autoCheckNewVersion\": \"自动检查更新\",\n  \"disabled\": \"已禁用\",\n  \"disable\": \"禁用\",\n  \"enabled\": \"已开启\",\n  \"enable\": \"开启\",\n  \"nextTime\": \"下一次\",\n  \"aWeek\": \"一周后\",\n  \"aMonth\": \"一个月后\",\n  \"aYear\": \"一年后\",\n  \"delete\": \"删除\",\n  \"deleting\": \"删除中\",\n  \"chooseReaderDirection\": \"选贼阅读器方向\",\n  \"topToBottom\": \"从上到下\",\n  \"leftToRight\": \"从左到右\",\n  \"rightToLeft\": \"从右到左\",\n  \"chooseReaderType\": \"选择阅读器类型\",\n  \"webtoon\": \"WebToon\",\n  \"gallery\": \"相册\",\n  \"proxy\": \"代理\",\n  \"inputProxyDesc\": \"socks5://host:port/\"\n}\n"
  },
  {
    "path": "lib/main.dart",
    "content": "import 'dart:io';\n\nimport 'package:event/event.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_localizations/flutter_localizations.dart';\nimport 'package:nhentai/screens/comic_info_screen.dart';\nimport 'package:nhentai/screens/components/mouse_and_touch_scroll_behavior.dart';\nimport 'package:nhentai/screens/init_screen.dart';\n\nimport 'basic/common/common.dart';\nimport 'basic/configs/themes.dart';\n\nvoid main() async {\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatefulWidget {\n\n  const MyApp({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => MyAppState();\n}\n\nclass MyAppState extends State<MyApp> {\n\n  @override\n  void initState() {\n    themeEvent.subscribe(_onChangeTheme);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    themeEvent.unsubscribe(_onChangeTheme);\n    super.dispose();\n  }\n\n  void _onChangeTheme(EventArgs? args) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      scrollBehavior: mouseAndTouchScrollBehavior,\n      debugShowCheckedModeBanner: false,\n      theme: currentThemeData(),\n      darkTheme: currentDarkTheme(),\n      home: const InitScreen(),\n      localizationsDelegates: const [\n        AppLocalizations.delegate,\n        GlobalMaterialLocalizations.delegate,\n        GlobalWidgetsLocalizations.delegate,\n        GlobalCupertinoLocalizations.delegate,\n      ],\n      supportedLocales: AppLocalizations.supportedLocales,\n    );\n  }\n}\n"
  },
  {
    "path": "lib/main_desktop.dart",
    "content": "import 'main.dart' as original_main;\n\n// This file is the default main entry-point for go-flutter application.\nvoid main() {\n  original_main.main();\n}\n"
  },
  {
    "path": "lib/screens/comic_downloads_screen.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'package:nhentai/basic/entities/entities.dart';\nimport 'package:nhentai/screens/components/actions.dart';\nimport 'package:nhentai/screens/components/content_builder.dart';\nimport 'package:waterfall_flow/waterfall_flow.dart';\n\nimport 'comic_info_screen.dart';\nimport 'components/images.dart';\n\nclass ComicDownloadsScreen extends StatefulWidget {\n  const ComicDownloadsScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicDownloadsScreenState();\n}\n\nclass _ComicDownloadsScreenState extends State<ComicDownloadsScreen> {\n  late Future<List<DownloadComicInfo>> _future;\n\n  @override\n  void initState() {\n    _future = nHentai.listDownloadComicInfo();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(AppLocalizations.of(context)!.download),\n        actions: [\n          ...alwaysInActions(),\n        ],\n      ),\n      body: ContentBuilder(\n        future: _future,\n        successBuilder: (BuildContext context,\n            AsyncSnapshot<List<DownloadComicInfo>> snapshot) {\n          var crossCount = 2;\n          var _data = snapshot.requireData;\n          return WaterfallFlow.builder(\n            physics: const AlwaysScrollableScrollPhysics(),\n            padding: const EdgeInsets.only(top: 10, bottom: 10),\n            itemCount: _data.length,\n            gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(\n              crossAxisCount: crossCount,\n              crossAxisSpacing: 0,\n              mainAxisSpacing: 0,\n            ),\n            itemBuilder: (BuildContext context, int index) {\n              return _buildImageCard(_data[index]);\n            },\n          );\n        },\n        onRefresh: () async {},\n      ),\n    );\n  }\n\n  Widget _buildImageCard(DownloadComicInfo item) {\n    return GestureDetector(\n      onTap: () {\n        if (item.downloadStatus == 4) return;\n        Navigator.of(context).push(MaterialPageRoute(\n          builder: (context) {\n            return ComicInfoScreen(item.id, item.title.pretty);\n          },\n        ));\n      },\n      onLongPress: () async {\n        if (item.downloadStatus == 4) return;\n        var action = await chooseMapDialog(\n          context,\n          {\n            AppLocalizations.of(context)!.delete: 1,\n          },\n          AppLocalizations.of(context)!.chooseAction,\n        );\n        if (action != null) {\n          switch (action) {\n            case 1:\n              setState(() {\n                item.downloadStatus = 3;\n              });\n              await nHentai.downloadSetDelete(item.id);\n              break;\n          }\n        }\n      },\n      child: Card(\n        child: LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints constraints) {\n            var width = constraints.maxWidth;\n            var height = constraints.maxWidth *\n                item.images.thumbnail.h /\n                item.images.thumbnail.w;\n            return SizedBox(\n              width: width,\n              height: height,\n              child: Stack(\n                children: [\n                  NHentaiImage(\n                    url:\n                        \"https://t2.nhentai.net/galleries/${item.mediaId}/thumb.jpg\",\n                    size: Size(width, height),\n                    disablePreview: true,\n                  ),\n                  _buildDownloadStatus(item.downloadStatus),\n                ],\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n\n  Widget _buildDownloadStatus(int downloadStatus) {\n    late IconData iconData;\n    late Color color;\n    switch (downloadStatus) {\n      case 1:\n        iconData = Icons.download_done_sharp;\n        color = Colors.green;\n        break;\n      case 2:\n        iconData = Icons.error_outline;\n        color = Colors.yellow;\n        break;\n      case 3:\n        iconData = Icons.auto_delete_outlined;\n        color = Colors.red;\n        break;\n      default:\n        iconData = Icons.query_builder;\n        color = Colors.grey;\n        break;\n    }\n    return Align(\n      alignment: Alignment.topRight,\n      child: Container(\n        margin: EdgeInsets.only(top: 3, right: 3),\n        padding: EdgeInsets.all(1),\n        decoration: BoxDecoration(\n          color: color,\n          borderRadius: BorderRadius.circular(3),\n        ),\n        child: Icon(iconData, color: Colors.white, size: 14),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/comic_info_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:date_format/date_format.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/rendering.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'package:nhentai/basic/entities/entities.dart';\nimport 'package:nhentai/screens/comic_reader_screen.dart';\nimport 'package:nhentai/screens/comic_search_screen.dart';\nimport 'package:nhentai/screens/comics_screen.dart';\nimport 'package:nhentai/screens/components/actions.dart';\nimport 'package:nhentai/screens/components/content_builder.dart';\nimport 'package:nhentai/screens/components/images.dart';\nimport 'package:nhentai/screens/webview_screen.dart';\n\nclass ComicInfoScreen extends StatefulWidget {\n  final int comicId;\n  final String comicTitle;\n\n  const ComicInfoScreen(this.comicId, this.comicTitle, {Key? key})\n      : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicInfoScreenState();\n}\n\nclass _ComicInfoScreenState extends State<ComicInfoScreen> {\n  late Future<ComicInfo> _future;\n  late Future<bool> _hasDownloadFuture;\n\n  Future<ComicInfo> _loadComic() async {\n    var info = await nHentai.comicInfo(widget.comicId);\n    var _ = nHentai.saveViewInfo(info); // 在后台线程保存浏览记录\n    return info;\n  }\n\n  @override\n  void initState() {\n    _future = _loadComic();\n    _hasDownloadFuture = nHentai.hasDownload(widget.comicId);\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: FutureBuilder(\n          future: _future,\n          builder: (BuildContext context, AsyncSnapshot<ComicInfo> snapshot) {\n            if (snapshot.hasData) {\n              return Text(snapshot.requireData.title.pretty);\n            }\n            return Text(widget.comicTitle);\n          },\n        ),\n        actions: [\n          ...(Platform.isAndroid || Platform.isIOS) && widget.comicTitle.isEmpty\n              ? [\n                  IconButton(\n                      onPressed: () {\n                        Navigator.of(context).push(\n                            MaterialPageRoute(builder: (BuildContext context) {\n                          return const WebViewScreen();\n                        }));\n                      },\n                      icon: const Icon(Icons.wb_auto)),\n                ]\n              : [],\n          FutureBuilder(\n            future: _future,\n            builder:\n                (BuildContext context, AsyncSnapshot<ComicInfo> snapshot1) {\n              if (snapshot1.hasError ||\n                  snapshot1.connectionState != ConnectionState.done) {\n                return Container();\n              }\n              return FutureBuilder(\n                future: _hasDownloadFuture,\n                builder: (BuildContext context, AsyncSnapshot<bool> snapshot2) {\n                  if (snapshot2.hasError ||\n                      snapshot2.connectionState != ConnectionState.done) {\n                    return Container();\n                  }\n                  if (snapshot2.requireData) {\n                    // todo downloading\n                    return IconButton(\n                      onPressed: () {},\n                      icon: const Icon(Icons.download_done),\n                    );\n                  } else {\n                    // todo reload\n                    return IconButton(\n                      onPressed: () async {\n                        var confirm = await confirmDialog(\n                          context,\n                          AppLocalizations.of(context)!.questionDownloadComic,\n                        );\n                        if (confirm) {\n                          await nHentai.downloadComic(snapshot1.requireData);\n                          setState(() {\n                            _hasDownloadFuture =\n                                nHentai.hasDownload(widget.comicId);\n                          });\n                        }\n                      },\n                      icon: const Icon(Icons.download),\n                    );\n                  }\n                },\n              );\n            },\n          ),\n          ...alwaysInActions(),\n        ],\n      ),\n      floatingActionButton: FutureBuilder(\n        future: _future,\n        builder: (BuildContext context, AsyncSnapshot<ComicInfo> snapshot) {\n          if (snapshot.connectionState == ConnectionState.done &&\n              !snapshot.hasError) {\n            return Padding(\n              padding: const EdgeInsets.only(right: 30, bottom: 30),\n              child: FloatingActionButton(\n                onPressed: () {\n                  Navigator.of(context)\n                      .push(MaterialPageRoute(builder: (BuildContext context) {\n                    return ComicReaderScreen(snapshot.requireData);\n                  }));\n                },\n                child: const Icon(Icons.menu_book),\n              ),\n            );\n          }\n          return Container();\n        },\n      ),\n      body: ContentBuilder(\n        future: _future,\n        onRefresh: () async {\n          setState(() {\n            _future = _loadComic();\n          });\n        },\n        successBuilder:\n            (BuildContext context, AsyncSnapshot<ComicInfo> snapshot) {\n          var item = snapshot.data!;\n          var mq = MediaQuery.of(context);\n          var imageWidth =\n              (mq.size.width < mq.size.height) ? mq.size.width : mq.size.height;\n          imageWidth = imageWidth / 2;\n          var subColor = Color.alphaBlend(\n            Colors.grey.shade500.withAlpha(80),\n            (Theme.of(context).textTheme.bodyText1?.color ?? Colors.black),\n          );\n          return ListView(\n            children: [\n              Container(height: 20),\n              Align(\n                alignment: Alignment.center,\n                child: SizedBox(\n                  width: imageWidth,\n                  child: ClipRRect(\n                    borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                    child: HorizontalStretchNHentaiImage(\n                      url: coverImageUrl(item.mediaId),\n                      originSize: Size(\n                        item.images.cover.w.toDouble(),\n                        item.images.cover.h.toDouble(),\n                      ),\n                    ),\n                  ),\n                ),\n              ),\n              Container(height: 20),\n              Container(\n                margin: const EdgeInsets.only(left: 20, right: 20),\n                child: Text(\n                  item.title.english,\n                  textAlign: TextAlign.center,\n                  style: const TextStyle(\n                    fontSize: 18,\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n              ),\n              Container(height: 10),\n              Container(\n                margin: const EdgeInsets.only(left: 20, right: 20),\n                child: Text(\n                  item.title.japanese,\n                  textAlign: TextAlign.center,\n                  style: const TextStyle(\n                    fontSize: 14,\n                    fontWeight: FontWeight.bold,\n                  ),\n                ),\n              ),\n              Container(height: 10),\n              Align(\n                alignment: Alignment.center,\n                child: Text.rich(TextSpan(\n                  style: TextStyle(\n                    fontSize: 12,\n                    color: subColor,\n                  ),\n                  children: [\n                    WidgetSpan(\n                      child: Icon(\n                        Icons.calendar_today_outlined,\n                        size: 12,\n                        color: subColor,\n                      ),\n                    ),\n                    const TextSpan(text: \" \"),\n                    TextSpan(\n                      text: formatDate(\n                        DateTime.fromMillisecondsSinceEpoch(\n                          item.uploadDate * 1000,\n                        ),\n                        [yyyy, \"-\", mm, \"-\", dd, \" \", HH, \":\", nn, \":\", ss],\n                      ),\n                    ),\n                    const TextSpan(text: \"  \"),\n                  ],\n                )),\n              ),\n              Container(height: 10),\n              Align(\n                alignment: Alignment.center,\n                child: Text.rich(\n                  TextSpan(\n                    style: TextStyle(\n                      fontSize: 15,\n                      color: subColor,\n                    ),\n                    children: [\n                      TextSpan(\n                        children: [\n                          WidgetSpan(\n                            alignment: PlaceholderAlignment.middle,\n                            child: Icon(\n                              Icons.image,\n                              size: 14,\n                              color: subColor,\n                            ),\n                          ),\n                          const TextSpan(text: \" \"),\n                          TextSpan(text: \"${item.images.pages.length}\"),\n                        ],\n                      ),\n                      const TextSpan(text: \"    \"),\n                      TextSpan(\n                        children: [\n                          WidgetSpan(\n                            alignment: PlaceholderAlignment.middle,\n                            child: Icon(\n                              Icons.favorite_outline,\n                              size: 15,\n                              color: subColor,\n                            ),\n                          ),\n                          const TextSpan(text: \" \"),\n                          TextSpan(text: \"${item.numFavorites}\"),\n                        ],\n                      ),\n                    ],\n                  ),\n                ),\n              ),\n              Container(height: 10),\n              Container(\n                margin: const EdgeInsets.only(left: 20, right: 20),\n                child: Wrap(\n                  children: (item.tags.map(_buildTag)).toList(),\n                ),\n              ),\n            ],\n          );\n        },\n      ),\n    );\n  }\n\n  Widget _buildTag(ComicInfoTag e) {\n    return GestureDetector(\n      onTap: () {\n        Navigator.of(context)\n            .push(MaterialPageRoute(builder: (BuildContext context) {\n          return ComicsScreen(\n            searchStruct: ComicSearchStruct(\n              searchContext: \"\",\n              conditions: [\n                ComicSearchCondition('tag', e.name, false),\n              ],\n            ),\n          );\n        }));\n      },\n      child: Card(\n        child: Text.rich(TextSpan(\n          style: const TextStyle(fontSize: 10),\n          children: [\n            WidgetSpan(\n              child: ClipRRect(\n                borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                child: Container(\n                  color: Colors.grey.withAlpha(20),\n                  padding: const EdgeInsets.only(\n                      top: 2, bottom: 2, left: 4, right: 4),\n                  child: Text(e.name),\n                ),\n              ),\n            ),\n            WidgetSpan(\n              child: ClipRRect(\n                borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                child: Container(\n                  padding: const EdgeInsets.only(\n                      top: 2, bottom: 2, left: 4, right: 4),\n                  child: Text(\"${e.count}\"),\n                ),\n              ),\n            ),\n          ],\n        )),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/comic_reader_screen.dart",
    "content": "import 'dart:async';\nimport 'dart:io';\nimport 'package:another_xlider/another_xlider.dart';\nimport 'package:event/event.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter/services.dart';\nimport 'package:modal_bottom_sheet/modal_bottom_sheet.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/configs/proxy.dart';\nimport 'package:nhentai/basic/configs/reader_direction.dart';\nimport 'package:nhentai/basic/configs/reader_type.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:nhentai/basic/entities/entities.dart';\nimport 'package:nhentai/screens/components/images.dart';\nimport 'package:photo_view/photo_view_gallery.dart';\n\nclass ComicReaderScreen extends StatefulWidget {\n  final ComicInfo comicInfo;\n\n  const ComicReaderScreen(this.comicInfo, {Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _ComicReaderScreenState();\n}\n\nclass _ComicReaderScreenState extends State<ComicReaderScreen> {\n  late ReaderType _readerType;\n  late ReaderDirection _readerDirection;\n  late int _startIndex;\n  late Future _future;\n\n  Future _init() async {\n    var last = await nHentai.loadLastViewIndexByComicId(widget.comicInfo.id);\n    _readerType = currentReaderType();\n    _readerDirection = currentReaderDirection();\n    _startIndex = last;\n  }\n\n  @override\n  void initState() {\n    _future = _init();\n    super.initState();\n  }\n\n  Future _reload() async {\n    // Navigator.of(context)\n    //     .pushReplacement(MaterialPageRoute(builder: (BuildContext context) {\n    //   return ComicReaderScreen(widget.comicInfo);\n    // }));\n    setState(() {\n      _future = _init();\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: _future,\n      builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Scaffold(\n            backgroundColor: Colors.black,\n            appBar: AppBar(),\n          );\n        }\n        final screen = Scaffold(\n          backgroundColor: Colors.black,\n          body: _ComicReader(\n            widget.comicInfo,\n            readerType: _readerType,\n            readerDirection: _readerDirection,\n            reload: _reload,\n            startIndex: _startIndex,\n          ),\n        );\n        return readerKeyboardHolder(screen);\n      },\n    );\n  }\n}\n\nclass _ComicReader extends StatefulWidget {\n  final ComicInfo comicInfo;\n  final ReaderType readerType;\n  final ReaderDirection readerDirection;\n  final FutureOr Function() reload;\n  final int startIndex;\n\n  const _ComicReader(\n    this.comicInfo, {\n    required this.readerType,\n    required this.readerDirection,\n    required this.reload,\n    required this.startIndex,\n    Key? key,\n  }) : super(key: key);\n\n  @override\n  // ignore: no_logic_in_create_state\n  State<StatefulWidget> createState() {\n    switch (readerType) {\n      case ReaderType.webtoon:\n        return _ComicReaderWebToonState();\n      case ReaderType.gallery:\n        return _ComicReaderGalleryState();\n    }\n  }\n}\n\nabstract class _ComicReaderState extends State<_ComicReader> {\n  late final ReaderDirection _direction = currentReaderDirection();\n\n  Widget _buildViewer();\n\n  _needJumpTo(int pageIndex, bool animation);\n\n  late bool _fullScreen;\n  late int _current;\n  late int _slider;\n\n  Future _onFullScreenChange(bool fullScreen) async {\n    setState(() {\n      SystemChrome.setEnabledSystemUIOverlays(\n          fullScreen ? [] : SystemUiOverlay.values);\n      _fullScreen = fullScreen;\n    });\n  }\n\n  void _onCurrentChange(int index) {\n    if (index != _current) {\n      setState(() {\n        _current = index;\n        _slider = index;\n        var _ = nHentai.saveViewIndex(widget.comicInfo, index); // 在后台线程入库\n      });\n    }\n  }\n\n  @override\n  void initState() {\n    _fullScreen = false;\n    _current = widget.startIndex;\n    _slider = widget.startIndex;\n    _readerControllerEvent.subscribe(_onPageControl);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _readerControllerEvent.unsubscribe(_onPageControl);\n    if (Platform.isAndroid || Platform.isIOS) {\n      SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);\n    }\n    super.dispose();\n  }\n\n  void _onPageControl(_ReaderControllerEventArgs? args) {\n    if (args != null) {\n      var event = args.key;\n      switch (event) {\n        case \"UP\":\n          if (_current > 0) {\n            _needJumpTo(_current - 1, true);\n          }\n          break;\n        case \"DOWN\":\n          if (_current < widget.comicInfo.images.pages.length - 1) {\n            _needJumpTo(_current + 1, true);\n          }\n          break;\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        _buildViewer(),\n        _buildFrame(),\n      ],\n    );\n  }\n\n  Widget _buildFrame() {\n    return Column(\n      children: [\n        _fullScreen ? Container() : _buildAppBar(),\n        Expanded(\n          child: GestureDetector(\n            behavior: HitTestBehavior.translucent,\n            onTap: () {\n              _onFullScreenChange(!_fullScreen);\n            },\n            child: Container(),\n          ),\n        ),\n        _fullScreen ? Container() : _buildBottomBar(),\n        _fullScreen ? Container() : _buildEdgePadding(),\n      ],\n    );\n  }\n\n  Widget _buildAppBar() {\n    return AppBar(\n      title: Text(widget.comicInfo.title.pretty),\n      actions: [\n        IconButton(\n          onPressed: _onMoreSetting,\n          icon: const Icon(Icons.more_horiz),\n        ),\n      ],\n    );\n  }\n\n  Widget _buildBottomBar() {\n    return Container(\n      height: 45,\n      color: const Color(0x88000000),\n      child: Row(\n        crossAxisAlignment: CrossAxisAlignment.center,\n        children: [\n          Expanded(child: _buildSlider()),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildEdgePadding() {\n    return Container(\n      color: const Color(0x88000000),\n      child: SafeArea(\n        top: false,\n        child: Container(),\n      ),\n    );\n  }\n\n  Widget _buildSlider() {\n    return Column(\n      children: [\n        Expanded(child: Container()),\n        SizedBox(\n          height: 25,\n          child: FlutterSlider(\n            axis: Axis.horizontal,\n            values: [_slider.toDouble()],\n            min: 0,\n            max: (widget.comicInfo.images.pages.length - 1).toDouble(),\n            onDragging: (handlerIndex, lowerValue, upperValue) {\n              _slider = (lowerValue.toInt());\n            },\n            onDragCompleted: (handlerIndex, lowerValue, upperValue) {\n              _slider = (lowerValue.toInt());\n              if (_slider != _current) {\n                _needJumpTo(_slider, false);\n              }\n            },\n            trackBar: FlutterSliderTrackBar(\n              inactiveTrackBar: BoxDecoration(\n                borderRadius: BorderRadius.circular(20),\n                color: Colors.grey.shade300,\n              ),\n              activeTrackBar: BoxDecoration(\n                borderRadius: BorderRadius.circular(4),\n                color: Theme.of(context).colorScheme.secondary,\n              ),\n            ),\n            step: const FlutterSliderStep(\n              step: 1,\n              isPercentRange: false,\n            ),\n            tooltip: FlutterSliderTooltip(custom: (value) {\n              double a = value + 1;\n              return Container(\n                padding: const EdgeInsets.all(8),\n                decoration: ShapeDecoration(\n                  color: Colors.black.withAlpha(0xCC),\n                  shape: RoundedRectangleBorder(\n                      borderRadius: BorderRadiusDirectional.circular(3)),\n                ),\n                child: Text(\n                  '${a.toInt()}',\n                  style: const TextStyle(\n                    color: Colors.white,\n                    fontSize: 18,\n                  ),\n                ),\n              );\n            }),\n          ),\n        ),\n        Expanded(child: Container()),\n      ],\n    );\n  }\n\n  //\n  _onMoreSetting() async {\n    await showMaterialModalBottomSheet(\n      context: context,\n      backgroundColor: const Color(0xAA000000),\n      builder: (context) {\n        return SizedBox(\n          height: MediaQuery.of(context).size.height / 2,\n          child: _SettingPanel(),\n        );\n      },\n    );\n    if (_direction != currentReaderDirection() ||\n        widget.readerType != currentReaderType()) {\n      widget.reload();\n    }\n  }\n\n  //\n  double _appBarHeight() {\n    return Scaffold.of(context).appBarMaxHeight ?? 0;\n  }\n\n  double _bottomBarHeight() {\n    return 45;\n  }\n}\n\nclass _SettingPanel extends StatefulWidget {\n  @override\n  State<StatefulWidget> createState() => _SettingPanelState();\n}\n\nclass _SettingPanelState extends State<_SettingPanel> {\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        Row(\n          children: [\n            _bottomIcon(\n              icon: Icons.crop_sharp,\n              title: readerDirectionName(currentReaderDirection(), context),\n              onPressed: () async {\n                await chooseReaderDirection(context);\n                setState(() {});\n              },\n            ),\n            _bottomIcon(\n              icon: Icons.view_day_outlined,\n              title: readerTypeName(currentReaderType(), context),\n              onPressed: () async {\n                await chooseReaderType(context);\n                setState(() {});\n              },\n            ),\n            _bottomIcon(\n              icon: Icons.shuffle,\n              title: AppLocalizations.of(context)!.proxy,\n              onPressed: () async {\n                await chooseProxy(context);\n                setState(() {});\n              },\n            ),\n          ],\n        ),\n      ],\n    );\n  }\n\n  Widget _bottomIcon({\n    required IconData icon,\n    required String title,\n    required void Function() onPressed,\n  }) {\n    return Expanded(\n      child: Center(\n        child: Column(\n          children: [\n            IconButton(\n              iconSize: 55,\n              icon: Column(\n                children: [\n                  Container(height: 3),\n                  Icon(\n                    icon,\n                    size: 25,\n                    color: Colors.white,\n                  ),\n                  Container(height: 3),\n                  Text(\n                    title,\n                    style: const TextStyle(color: Colors.white, fontSize: 10),\n                    maxLines: 1,\n                    textAlign: TextAlign.center,\n                  ),\n                  Container(height: 3),\n                ],\n              ),\n              onPressed: onPressed,\n            )\n          ],\n        ),\n      ),\n    );\n  }\n}\n\nclass _ComicReaderWebToonState extends _ComicReaderState {\n  ScrollController? _controller;\n  List<double> _offsets = [];\n  List<Size> _sizes = [];\n\n  @override\n  void dispose() {\n    _controller?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget _buildViewer() {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        if (_direction == ReaderDirection.topToBottom) {\n          _offsets = widget.comicInfo.images.pages\n              .map((e) => constraints.maxWidth * e.h / e.w)\n              .toList()\n              .cast<double>();\n        } else {\n          var height = constraints.maxHeight -\n              super._appBarHeight() -\n              super._bottomBarHeight() -\n              MediaQuery.of(context).padding.bottom;\n          _offsets = widget.comicInfo.images.pages\n              .map((e) => height * e.w / e.h)\n              .toList()\n              .cast<double>();\n        }\n        if (_direction == ReaderDirection.topToBottom) {\n          _sizes = widget.comicInfo.images.pages\n              .map((e) =>\n                  Size(constraints.maxWidth, constraints.maxWidth * e.h / e.w))\n              .toList()\n              .cast<Size>();\n        } else {\n          var height = constraints.maxHeight -\n              super._appBarHeight() -\n              super._bottomBarHeight() -\n              MediaQuery.of(context).padding.bottom;\n          _sizes = widget.comicInfo.images.pages\n              .map((e) => Size(height * e.w / e.h, height))\n              .toList()\n              .cast<Size>();\n        }\n        if (_controller == null) {\n          _controller = ScrollController(initialScrollOffset: _initOffset());\n          _controller!.addListener(_onScroll);\n        }\n        return ListView.builder(\n          scrollDirection: _direction == ReaderDirection.topToBottom\n              ? Axis.vertical\n              : Axis.horizontal,\n          reverse: _direction == ReaderDirection.rightToLeft,\n          controller: _controller,\n          padding: EdgeInsets.only(\n            top: super._appBarHeight(),\n            bottom: _direction == ReaderDirection.topToBottom\n                ? 130\n                : (super._bottomBarHeight() +\n                MediaQuery.of(context).padding.bottom)\n          ),\n          itemCount: widget.comicInfo.images.pages.length,\n          itemBuilder: (BuildContext context, int index) {\n            return NHentaiImage(\n              url: pageImageUrl(widget.comicInfo.mediaId, index + 1),\n              size: _sizes[index],\n              fit: BoxFit.contain,\n            );\n          },\n        );\n      },\n    );\n  }\n\n  _onScroll() {\n    double cOff = _controller!.offset;\n    double off = 0;\n    int i = 0;\n    for (; i < _offsets.length; i++) {\n      if (cOff == off) {\n        // 最顶端, 以及每个图片的开始\n        // 0 == 0\n        super._onCurrentChange(i);\n        return;\n      }\n      // 第二轮, 假设第一张的300px, 现在是299px\n      if (cOff < off) {\n        super._onCurrentChange(i - 1);\n        return;\n      }\n      off += _offsets[i];\n    }\n    // 特殊情况1: i = 0, 如果没图片, i = 0\n    // 特殊情况2: i = _offset.length 如果下方padding超过一屏 i 最后还++, 但是已经达到了length, 这个index是不存在的\n    if (i == 0) {\n      return;\n    }\n    super._onCurrentChange(i - 1);\n  }\n\n  double _initOffset() {\n    double off = 0;\n    for (var i = 1; i <= widget.startIndex && i < _offsets.length; i++) {\n      off += _offsets[i - 1];\n    }\n    return off;\n  }\n\n  @override\n  _needJumpTo(int pageIndex, bool animation) {\n    if (_offsets.length > pageIndex) {\n      double off = 0;\n      for (int i = 0; i < pageIndex; i++) {\n        off += _offsets[i];\n      }\n      if (animation) {\n        _controller?.animateTo(\n          off,\n          duration: const Duration(milliseconds: 250),\n          curve: Curves.ease,\n        );\n      } else {\n        _controller?.jumpTo(off);\n      }\n    }\n  }\n}\n\nclass _ComicReaderGalleryState extends _ComicReaderState {\n  late PageController _pageController;\n  late PhotoViewGallery _gallery;\n\n  @override\n  void initState() {\n    _pageController = PageController(initialPage: widget.startIndex);\n    _gallery = PhotoViewGallery.builder(\n      scrollDirection: _direction == ReaderDirection.topToBottom\n          ? Axis.vertical\n          : Axis.horizontal,\n      reverse: _direction == ReaderDirection.rightToLeft,\n      backgroundDecoration: const BoxDecoration(color: Colors.black),\n      loadingBuilder: (context, event) => LayoutBuilder(\n        builder: (BuildContext context, BoxConstraints constraints) {\n          return buildLoading(constraints.maxWidth, constraints.maxHeight);\n        },\n      ),\n      pageController: _pageController,\n      onPageChanged: _onGalleryPageChange,\n      itemCount: widget.comicInfo.images.pages.length,\n      allowImplicitScrolling: true,\n      builder: (BuildContext context, int index) {\n        return PhotoViewGalleryPageOptions(\n          filterQuality: FilterQuality.high,\n          imageProvider: NHentaiImageProvider(\n              pageImageUrl(widget.comicInfo.mediaId, index + 1)),\n          errorBuilder: (b, e, s) {\n            print(\"$e,$s\");\n            return LayoutBuilder(\n              builder: (BuildContext context, BoxConstraints constraints) {\n                return buildError(constraints.maxWidth, constraints.maxHeight);\n              },\n            );\n          },\n        );\n      },\n    );\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _pageController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget _buildViewer() {\n    return Column(\n      children: [\n        Container(height: _fullScreen ? 0 : super._appBarHeight()),\n        Expanded(\n          child: Stack(\n            children: [\n              _gallery,\n            ],\n          ),\n        ),\n        Container(height: _fullScreen ? 0 : super._bottomBarHeight()),\n      ],\n    );\n  }\n\n  @override\n  _needJumpTo(int pageIndex, bool animation) {\n    if (animation) {\n      _pageController.animateToPage(\n        pageIndex,\n        duration: const Duration(milliseconds: 400),\n        curve: Curves.ease,\n      );\n    } else {\n      _pageController.jumpToPage(pageIndex);\n    }\n  }\n\n  void _onGalleryPageChange(int to) {\n    super._onCurrentChange(to);\n  }\n}\n\n////////////////////////////////\n\n// 仅支持安卓\n// 监听后会拦截安卓手机音量键\n// 仅最后一次监听生效\n// event可能为DOWN/UP\nEvent<_ReaderControllerEventArgs> _readerControllerEvent =\n    Event<_ReaderControllerEventArgs>();\n\nclass _ReaderControllerEventArgs extends EventArgs {\n  final String key;\n\n  _ReaderControllerEventArgs(this.key);\n}\n\nconst _listVolume = false;\n\nvar _volumeListenCount = 0;\n\nvoid _onVolumeEvent(dynamic args) {\n  _readerControllerEvent.broadcast(_ReaderControllerEventArgs(\"$args\"));\n}\n\nEventChannel volumeButtonChannel = const EventChannel(\"volume_button\");\nStreamSubscription? volumeS;\n\nvoid addVolumeListen() {\n  _volumeListenCount++;\n  if (_volumeListenCount == 1) {\n    volumeS =\n        volumeButtonChannel.receiveBroadcastStream().listen(_onVolumeEvent);\n  }\n}\n\nvoid delVolumeListen() {\n  _volumeListenCount--;\n  if (_volumeListenCount == 0) {\n    volumeS?.cancel();\n  }\n}\n\nWidget readerKeyboardHolder(Widget widget) {\n  if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {\n    widget = RawKeyboardListener(\n      focusNode: FocusNode(),\n      child: widget,\n      autofocus: true,\n      onKey: (event) {\n        if (event is RawKeyDownEvent) {\n          if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {\n            _readerControllerEvent.broadcast(_ReaderControllerEventArgs(\"UP\"));\n          }\n          if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {\n            _readerControllerEvent\n                .broadcast(_ReaderControllerEventArgs(\"DOWN\"));\n          }\n        }\n      },\n    );\n  }\n  return widget;\n}\n\n////////////////////////////////\n"
  },
  {
    "path": "lib/screens/comic_search_screen.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\nclass ComicSearchStruct {\n  String searchContext;\n  late List<ComicSearchCondition> conditions;\n\n  ComicSearchStruct({\n    this.searchContext = \"\",\n    List<ComicSearchCondition>? conditions,\n  }) {\n    if (conditions != null) {\n      this.conditions = conditions;\n    } else {\n      this.conditions = [];\n    }\n  }\n\n  String dumpSearchString() {\n    var ss = searchContext.replaceAll(\"\\\"\", \" \").trim();\n    var content =\n        ss == \"\" ? \"\" : ss.split(\"\\\\s+\").map((e) => \"\\\"$e\\\"\").join(\" \");\n    for (var element in conditions) {\n      if (content != \"\") {\n        content += \" \";\n      }\n      content += element.exclude ? \"-\" : \"\";\n      content += element.type;\n      content += \":\";\n      content += \"\\\"${element.content.replaceAll(\"\\\"\", \" \")}\\\"\";\n    }\n    return content;\n  }\n}\n\nclass ComicSearchCondition {\n  String type;\n  String content;\n  bool exclude;\n\n  ComicSearchCondition(this.type, this.content, this.exclude);\n}\n\nclass ComicSearchScreen extends StatefulWidget {\n  late final ComicSearchStruct defaultSearchStruct;\n\n  ComicSearchScreen(ComicSearchStruct? defaultSearchStruct, {Key? key})\n      : super(key: key) {\n    if (defaultSearchStruct != null) {\n      this.defaultSearchStruct = defaultSearchStruct;\n    } else {\n      this.defaultSearchStruct = ComicSearchStruct();\n    }\n  }\n\n  @override\n  State<StatefulWidget> createState() => _ComicSearchScreenState();\n}\n\nclass _ComicSearchScreenState extends State<ComicSearchScreen> {\n  late ComicSearchStruct _searchStruct = widget.defaultSearchStruct;\n  late final _searchContentFocusNode = FocusNode();\n  late final _textEditingController = TextEditingController();\n\n  @override\n  void initState() {\n    _textEditingController.text = _searchStruct.searchContext;\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _searchContentFocusNode.dispose();\n    _textEditingController.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(AppLocalizations.of(context)!.search),\n        actions: [\n          IconButton(\n              onPressed: () {\n                setState(() {\n                  _searchStruct = ComicSearchStruct();\n                  _textEditingController.text = _searchStruct.searchContext;\n                });\n              },\n              icon: const Icon(Icons.search_off)),\n          IconButton(\n            onPressed: () {\n              Navigator.of(context).pop(_searchStruct);\n            },\n            icon: const Icon(Icons.done),\n          ),\n        ],\n      ),\n      body: ListView(\n        children: [\n          Container(height: 15),\n          Container(\n            padding: const EdgeInsets.all(15),\n            child: TextField(\n              controller: _textEditingController,\n              onChanged: (value) => _searchStruct.searchContext = value,\n              focusNode: _searchContentFocusNode,\n              cursorColor: Colors.red,\n              decoration: InputDecoration(\n                labelText: AppLocalizations.of(context)!.searchContent,\n                border: OutlineInputBorder(\n                  borderRadius: BorderRadius.circular(15.0),\n                ),\n              ),\n              style: const TextStyle(),\n            ),\n          ),\n          Container(\n            margin: const EdgeInsets.all(15),\n            child: Wrap(\n              children: _searchStruct.conditions\n                  .map(_buildCondition)\n                  .toList()\n                  .cast<Widget>(),\n            ),\n          ),\n          Container(\n            margin: const EdgeInsets.all(15),\n            color: Colors.grey.shade500.withAlpha(50),\n            child: MaterialButton(\n              onPressed: () async {\n                var dia = _AddTagConditionDialog();\n                ComicSearchCondition? con = await showDialog(\n                    context: context,\n                    builder: (BuildContext context) {\n                      return AlertDialog(\n                        title: Text(AppLocalizations.of(context)!.addFilter),\n                        content: SizedBox(\n                          width: double.minPositive,\n                          height: MediaQuery.of(context).size.height / 2,\n                          child: dia,\n                        ),\n                        actions: [\n                          MaterialButton(\n                            onPressed: () {\n                              Navigator.of(context).pop();\n                            },\n                            child: Text(AppLocalizations.of(context)!.cancel),\n                          ),\n                          MaterialButton(\n                            onPressed: () {\n                              var con = dia.condition;\n                              con.content = con.content.trim();\n                              if (con.content == \"\") {\n                                defaultToast(context, \"内容不能为空\");\n                              } else {\n                                Navigator.of(context).pop(con);\n                              }\n                            },\n                            child: Text(AppLocalizations.of(context)!.ok),\n                          ),\n                        ],\n                      );\n                    });\n                if (con != null) {\n                  setState(() {\n                    _searchStruct.conditions.add(con);\n                  });\n                }\n              },\n              child: Text(AppLocalizations.of(context)!.addFilter),\n            ),\n          ),\n        ],\n      ),\n    );\n  }\n\n  Widget _buildCondition(ComicSearchCondition condition) {\n    return GestureDetector(\n      onTap: () {\n        setState(() {\n          _searchStruct.conditions.remove(condition);\n        });\n      },\n      child: Card(\n        child: Text.rich(TextSpan(\n          style: const TextStyle(fontSize: 10),\n          children: [\n            WidgetSpan(\n              child: ClipRRect(\n                borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                child: Container(\n                  padding: const EdgeInsets.only(\n                      top: 2, bottom: 2, left: 4, right: 4),\n                  child: Text(condition.exclude ? \"-\" : \"+\"),\n                ),\n              ),\n            ),\n            WidgetSpan(\n              child: ClipRRect(\n                borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                child: Container(\n                  color: Colors.grey.withAlpha(20),\n                  padding: const EdgeInsets.only(\n                      top: 2, bottom: 2, left: 4, right: 4),\n                  child: Text(condition.type),\n                ),\n              ),\n            ),\n            WidgetSpan(\n              child: ClipRRect(\n                borderRadius: const BorderRadius.all(Radius.circular(4.0)),\n                child: Container(\n                  padding: const EdgeInsets.only(\n                      top: 2, bottom: 2, left: 4, right: 4),\n                  child: Text(condition.content),\n                ),\n              ),\n            ),\n          ],\n        )),\n      ),\n    );\n  }\n}\n\nclass _AddTagConditionDialog extends StatefulWidget {\n  late final ComicSearchCondition condition;\n\n  _AddTagConditionDialog({Key? key}) : super(key: key) {\n    condition = ComicSearchCondition(\"tag\", \"\", false);\n  }\n\n  @override\n  State<StatefulWidget> createState() => _AddTagConditionDialogState();\n}\n\nclass _AddTagConditionDialogState extends State<_AddTagConditionDialog> {\n  @override\n  Widget build(BuildContext context) {\n    return ListView(\n      children: [\n        const Divider(),\n        SwitchListTile(\n          title: Text(AppLocalizations.of(context)!.exclusion),\n          value: widget.condition.exclude,\n          onChanged: (value) => setState(() {\n            widget.condition.exclude = value;\n          }),\n        ),\n        const Divider(),\n        Text(AppLocalizations.of(context)!.type),\n        DropdownButton<String>(\n          value: widget.condition.type,\n          items: [\n            DropdownMenuItem<String>(\n              value: 'tag',\n              child: Text(AppLocalizations.of(context)!.tag),\n            ),\n          ],\n          onChanged: (value) => setState(() {\n            widget.condition.type = value ?? \"\";\n          }),\n        ),\n        const Divider(),\n        Text(AppLocalizations.of(context)!.content),\n        TextFormField(\n          initialValue: widget.condition.content,\n          onChanged: (value) => widget.condition.content = value,\n        ),\n        const Divider(),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/comics_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\n\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/screens/comic_search_screen.dart';\nimport 'package:nhentai/screens/components/pager.dart';\nimport 'package:nhentai/screens/webview_screen.dart';\n\nimport 'comic_downloads_screen.dart';\nimport 'components/actions.dart';\n\nclass ComicsScreen extends StatefulWidget {\n  late final ComicSearchStruct searchStruct;\n\n  ComicsScreen({ComicSearchStruct? searchStruct, Key? key}) : super(key: key) {\n    if (searchStruct != null) {\n      this.searchStruct = searchStruct;\n    } else {\n      this.searchStruct = ComicSearchStruct();\n    }\n  }\n\n  @override\n  State<StatefulWidget> createState() => _ComicsScreenState();\n}\n\nclass _ComicsScreenState extends State<ComicsScreen> {\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(\n          widget.searchStruct.dumpSearchString() == \"\"\n              ? AppLocalizations.of(context)!.allComics\n              : widget.searchStruct.dumpSearchString(),\n        ),\n        actions: [\n          ...Platform.isAndroid || Platform.isIOS\n              ? [\n                  IconButton(\n                      onPressed: () {\n                        Navigator.of(context).push(\n                            MaterialPageRoute(builder: (BuildContext context) {\n                          return const WebViewScreen();\n                        }));\n                      },\n                      icon: const Icon(Icons.wb_auto)),\n                ]\n              : [],\n          IconButton(\n            onPressed: () async {\n              ComicSearchStruct? struct = await Navigator.of(context).push(\n                MaterialPageRoute(\n                  builder: (BuildContext context) =>\n                      ComicSearchScreen(widget.searchStruct),\n                ),\n              );\n              if (struct != null) {\n                Navigator.of(context).pushReplacement(MaterialPageRoute(\n                  builder: (BuildContext context) =>\n                      ComicsScreen(searchStruct: struct),\n                ));\n              }\n            },\n            icon: const Icon(Icons.search),\n          ),\n          IconButton(\n            onPressed: () {\n              Navigator.of(context).push(MaterialPageRoute(\n                builder: (BuildContext context) {\n                  return const ComicDownloadsScreen();\n                },\n              ));\n            },\n            icon: const Icon(Icons.archive),\n          ),\n          ...alwaysInActions(),\n        ],\n      ),\n      body: Pager(\n        onPage: (int page) {\n          return nHentai.comicsBySearchRaw(\n            widget.searchStruct.dumpSearchString(),\n            page,\n          );\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/Badged.dart",
    "content": "import 'package:flutter/material.dart';\n\nclass Badged extends StatelessWidget {\n  final String? badge;\n  final Widget child;\n\n  const Badged({Key? key, required this.child, this.badge}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    if (badge == null) {\n      return child;\n    }\n    return Stack(\n      children: [\n        child,\n        Positioned(\n          right: 0,\n          child: Container(\n            padding: const EdgeInsets.all(1),\n            decoration: BoxDecoration(\n              color: Colors.red,\n              borderRadius: BorderRadius.circular(6),\n            ),\n            constraints: const BoxConstraints(\n              minWidth: 12,\n              minHeight: 12,\n            ),\n            child: Text(\n              badge!,\n              style: const TextStyle(\n                color: Colors.white,\n                fontSize: 8,\n              ),\n              textAlign: TextAlign.center,\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/actions.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'package:nhentai/basic/configs/version.dart';\n\nimport '../settings_screen.dart';\nimport 'Badged.dart';\n\nList<Widget> alwaysInActions() {\n  return [\n    _SettingsAction(),\n  ];\n}\n\nclass _SettingsAction extends StatefulWidget {\n  @override\n  State<StatefulWidget> createState() => _SettingsActionState();\n}\n\nclass _SettingsActionState extends State<_SettingsAction> {\n  @override\n  void initState() {\n    versionEvent.subscribe(_setState);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    versionEvent.unsubscribe(_setState);\n    super.dispose();\n  }\n\n  void _setState(_) {\n    setState(() {});\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return IconButton(\n      onPressed: () {\n        Navigator.of(context).push(MaterialPageRoute(\n          builder: (BuildContext context) => const SettingsScreen(),\n        ));\n      },\n      icon: Badged(\n        child: const Icon(Icons.settings),\n        badge: latestVersion() == null ? null : \"1\",\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/content_builder.dart",
    "content": "import 'package:flutter/material.dart';\nimport 'content_error.dart';\nimport 'content_loading.dart';\n\nclass ContentBuilder<T> extends StatelessWidget {\n  final Future<T> future;\n  final Future<dynamic> Function() onRefresh;\n  final AsyncWidgetBuilder<T> successBuilder;\n  final String? loadingLabel;\n\n  const ContentBuilder({\n    Key? key,\n    required this.future,\n    required this.onRefresh,\n    required this.successBuilder,\n    this.loadingLabel,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return FutureBuilder(\n      future: future,\n      builder: (BuildContext context, AsyncSnapshot<T> snapshot) {\n        if (snapshot.hasError) {\n          return ContentError(\n            error: snapshot.error,\n            stackTrace: snapshot.stackTrace,\n            onRefresh: onRefresh,\n          );\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return ContentLoading(label: loadingLabel);\n        }\n        return successBuilder(context, snapshot);\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/content_error.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'dart:ui';\n\nimport 'package:nhentai/basic/common/error_types.dart';\n\nclass ContentError extends StatelessWidget {\n  final Object? error;\n  final StackTrace? stackTrace;\n  final Future<void> Function() onRefresh;\n\n  const ContentError({\n    Key? key,\n    required this.error,\n    required this.stackTrace,\n    required this.onRefresh,\n  }) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    var type = errorType(\"$error\");\n    late String message;\n    late IconData iconData;\n    switch (type) {\n      case ERROR_TYPE_NETWORK:\n        iconData = Icons.wifi_off_rounded;\n        message = \"连接不上啦, 请检查网络\";\n        break;\n      case ERROR_TYPE_PERMISSION:\n        iconData = Icons.highlight_off;\n        message = \"没有权限或路径不可用\";\n        break;\n      case ERROR_TYPE_TIME:\n        iconData = Icons.timer_off;\n        message = \"请检查设备时间\";\n        break;\n      case ERROR_TYPE_UNDER_REVIEW:\n        iconData = Icons.highlight_off;\n        message = \"资源未审核或不可用\";\n        break;\n      default:\n        iconData = Icons.highlight_off;\n        message = \"啊哦, 被玩坏了\";\n        break;\n    }\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        print(\"$error\");\n        print(\"$stackTrace\");\n        var width = constraints.maxWidth;\n        var height = constraints.maxHeight;\n        var min = width < height ? width : height;\n        var iconSize = min / 2.3;\n        var textSize = min / 16;\n        var tipSize = min / 20;\n        var infoSize = min / 30;\n        return GestureDetector(\n          onTap: onRefresh,\n          child: ListView(\n            children: [\n              SizedBox(\n                height: height,\n                child: Column(\n                  children: [\n                    Expanded(child: Container()),\n                    Icon(\n                      iconData,\n                      size: iconSize,\n                      color: Colors.grey.shade600,\n                    ),\n                    Container(height: min / 10),\n                    Container(\n                      padding: const EdgeInsets.only(\n                        left: 30,\n                        right: 30,\n                      ),\n                      child: Text(\n                        message,\n                        style: TextStyle(fontSize: textSize),\n                        textAlign: TextAlign.center,\n                      ),\n                    ),\n                    Text(\n                      AppLocalizations.of(context)!.errorAndTapRetry,\n                      style: TextStyle(fontSize: tipSize),\n                    ),\n                    Container(height: min / 15),\n                    Text('$error', style: TextStyle(fontSize: infoSize)),\n                    Expanded(child: Container()),\n                  ],\n                ),\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/content_loading.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\n\nclass ContentLoading extends StatelessWidget {\n  final String? label;\n\n  const ContentLoading({Key? key, this.label}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    String label = this.label ?? AppLocalizations.of(context)!.loading;\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        var height = constraints.maxHeight;\n        var min = width < height ? width : height;\n        var theme = Theme.of(context);\n        return Center(\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              SizedBox(\n                width: min / 2,\n                height: min / 2,\n                child: CircularProgressIndicator(\n                  color: theme.colorScheme.secondary,\n                  backgroundColor: Colors.grey[100],\n                ),\n              ),\n              Container(height: min / 10),\n              Text(label, style: TextStyle(fontSize: min / 15)),\n              Expanded(child: Container()),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/images.dart",
    "content": "import 'package:flutter/foundation.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:io';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'package:nhentai/basic/common/cross.dart';\n\nimport '../file_photo_view_screen.dart';\nimport 'dart:ui' as ui show Codec;\n\nString coverImageUrl(int mediaId) {\n  return \"https://t.nhentai.net/galleries/$mediaId/cover.${\"jpg\"}\";\n}\n\nString pageImageUrl(int mediaId, int num) {\n  return \"https://i.nhentai.net/galleries/$mediaId/$num.${\"jpg\"}\";\n}\n\nclass NHentaiImageProvider extends ImageProvider<NHentaiImageProvider> {\n  final String url;\n  final double scale;\n\n  NHentaiImageProvider(this.url, {this.scale = 1.0});\n\n  @override\n  ImageStreamCompleter load(key, DecoderCallback decode) {\n    return MultiFrameImageStreamCompleter(\n      codec: _loadAsync(key),\n      scale: key.scale,\n    );\n  }\n\n  @override\n  Future<NHentaiImageProvider> obtainKey(ImageConfiguration configuration) {\n    return SynchronousFuture<NHentaiImageProvider>(this);\n  }\n\n  Future<ui.Codec> _loadAsync(NHentaiImageProvider key) async {\n    assert(key == this);\n    var path = await nHentai.cacheImageByUrlPath(url);\n    var data = await File(path).readAsBytes();\n    return PaintingBinding.instance!.instantiateImageCodec(data);\n  }\n}\n\nclass NHentaiImage extends StatefulWidget {\n  final String url;\n  final Size size;\n  final BoxFit fit;\n  final bool disablePreview;\n\n  const NHentaiImage({\n    Key? key,\n    required this.url,\n    required this.size,\n    this.fit = BoxFit.cover,\n    this.disablePreview = false,\n  }) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _NHentaiImageState();\n}\n\nclass _NHentaiImageState extends State<NHentaiImage> {\n  late final Future<String> _future = nHentai.cacheImageByUrlPath(widget.url);\n\n  @override\n  Widget build(BuildContext context) {\n    return pathFutureImage(\n      _future,\n      widget.size.width,\n      widget.size.height,\n      fit: widget.fit,\n      context: context,\n      disablePreview: widget.disablePreview,\n    );\n  }\n}\n\nWidget pathFutureImage(Future<String> future, double? width, double? height,\n    {required BuildContext context,\n    BoxFit fit = BoxFit.cover,\n    bool disablePreview = false}) {\n  return FutureBuilder(\n      future: future,\n      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {\n        if (snapshot.hasError) {\n          print(\"${snapshot.error}\");\n          print(\"${snapshot.stackTrace}\");\n          return buildError(width, height);\n        }\n        if (snapshot.connectionState != ConnectionState.done) {\n          return buildLoading(width, height);\n        }\n        return buildFile(\n          snapshot.data!,\n          width,\n          height,\n          fit: fit,\n          context: context,\n          disablePreview: disablePreview,\n        );\n      });\n}\n\nWidget buildError(double? width, double? height) {\n  return Image(\n    image: const AssetImage('lib/assets/error.png'),\n    width: width,\n    height: height,\n  );\n}\n\nWidget buildLoading(double? width, double? height) {\n  double? size;\n  if (width != null && height != null) {\n    size = width < height ? width : height;\n  }\n  return SizedBox(\n    width: width,\n    height: height,\n    child: Center(\n      child: Icon(\n        Icons.downloading,\n        size: size,\n        color: Colors.grey.shade500.withAlpha(80),\n      ),\n    ),\n  );\n}\n\nWidget buildFile(String file, double? width, double? height,\n    {required BuildContext context,\n    BoxFit fit = BoxFit.cover,\n    bool disablePreview = false}) {\n  var image = Image(\n    image: FileImage(File(file)),\n    width: width,\n    height: height,\n    errorBuilder: (context, obj, stackTrace) {\n      print(\"$obj\");\n      print(\"$stackTrace\");\n      return buildError(width, height);\n    },\n    fit: fit,\n  );\n  if (disablePreview) return image;\n  return GestureDetector(\n    onLongPress: () async {\n      final previewImageText = AppLocalizations.of(context)!.previewImage;\n      final saveImageText = AppLocalizations.of(context)!.saveImage;\n      final chooseActionText = AppLocalizations.of(context)!.chooseAction;\n      int? choose = await chooseMapDialog(\n        context,\n        {\n          previewImageText: 1,\n          saveImageText: 2,\n        },\n        chooseActionText,\n      );\n      switch (choose) {\n        case 1:\n          Navigator.of(context).push(MaterialPageRoute(\n            builder: (context) => FilePhotoViewScreen(file),\n          ));\n          break;\n        case 2:\n          saveImage(file, context);\n          break;\n      }\n    },\n    child: image,\n  );\n}\n\nclass HorizontalStretchNHentaiImage extends StatelessWidget {\n  final String url;\n  final Size originSize;\n\n  const HorizontalStretchNHentaiImage(\n      {required this.url, required this.originSize, Key? key})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        var height =\n            constraints.maxWidth * originSize.height / originSize.width;\n        return NHentaiImage(url: url, size: Size(width, height));\n      },\n    );\n  }\n}\n\nclass ScaleImageTitle extends StatelessWidget {\n  final String title;\n  final Size originSize;\n\n  const ScaleImageTitle(\n      {required this.title, required this.originSize, Key? key})\n      : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return LayoutBuilder(\n      builder: (BuildContext context, BoxConstraints constraints) {\n        var width = constraints.maxWidth;\n        var height =\n            constraints.maxWidth * originSize.height / originSize.width;\n        return SizedBox(\n          width: width,\n          height: height,\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              Row(\n                children: [\n                  Expanded(\n                    child: Material(\n                      color: const Color(0xAA000000),\n                      child: Text(\n                        \"$title\\n\",\n                        maxLines: 2,\n                        textAlign: TextAlign.center,\n                        style: const TextStyle(\n                          color: Colors.white,\n                        ),\n                      ),\n                    ),\n                  ),\n                ],\n              ),\n            ],\n          ),\n        );\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/components/mouse_and_touch_scroll_behavior.dart",
    "content": "import 'dart:ui';\nimport 'package:flutter/material.dart';\n\nfinal mouseAndTouchScrollBehavior = MouseAndTouchScrollBehavior();\n\nclass MouseAndTouchScrollBehavior extends MaterialScrollBehavior {\n  @override\n  Set<PointerDeviceKind> get dragDevices => {\n    PointerDeviceKind.touch,\n    PointerDeviceKind.mouse,\n  };\n}\n"
  },
  {
    "path": "lib/screens/components/pager.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:async';\n\nimport 'package:flutter/cupertino.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/entities/entities.dart';\nimport 'package:nhentai/screens/comic_info_screen.dart';\nimport 'package:waterfall_flow/waterfall_flow.dart';\n\nimport 'images.dart';\n\nclass Pager extends StatefulWidget {\n  final FutureOr<ComicPageData> Function(int page) onPage;\n\n  const Pager({required this.onPage, Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _PageState();\n}\n\nclass _PageState extends State<Pager> {\n  late ScrollController _controller;\n  int _currentPage = 1;\n  bool _lastPage = false;\n  final List<ComicSimple> _data = [];\n  var _joining = false;\n  late Future _joinFuture;\n\n  Future _join() async {\n    try {\n      setState(() {\n        _joining = true;\n      });\n      var response = await widget.onPage(_currentPage);\n      _data.addAll(response.records);\n      _currentPage++;\n      if (response.pageCount <= _currentPage) {\n        _lastPage = true;\n      }\n    } finally {\n      setState(() {\n        _joining = false;\n      });\n    }\n  }\n\n  void _next() {\n    setState(() {\n      _joinFuture = _join();\n    });\n  }\n\n  void _onScroll() {\n    if (_joining || _lastPage) {\n      return;\n    }\n    if (_controller.position.pixels < _controller.position.maxScrollExtent) {\n      return;\n    }\n    _next();\n  }\n\n  @override\n  void initState() {\n    _joinFuture = _join();\n    _controller = ScrollController();\n    _controller.addListener(_onScroll);\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    _controller.removeListener(_onScroll);\n    _controller.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    const int crossCount = 2;\n    return WaterfallFlow.builder(\n      controller: _controller,\n      physics: const AlwaysScrollableScrollPhysics(),\n      padding: const EdgeInsets.only(top: 10, bottom: 10),\n      itemCount: _data.length + 1,\n      gridDelegate: const SliverWaterfallFlowDelegateWithFixedCrossAxisCount(\n        crossAxisCount: crossCount,\n        crossAxisSpacing: 0,\n        mainAxisSpacing: 0,\n      ),\n      itemBuilder: (BuildContext context, int index) {\n        if (index >= _data.length) {\n          return _buildLoadingCard();\n        }\n        return _buildImageCard(_data[index]);\n      },\n    );\n  }\n\n  Widget _buildLoadingCard() {\n    return FutureBuilder(\n      future: _joinFuture,\n      builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {\n        if (snapshot.connectionState != ConnectionState.done) {\n          return Card(\n            child: Column(\n              children: [\n                Container(\n                  padding: const EdgeInsets.only(top: 10, bottom: 10),\n                  child: const CupertinoActivityIndicator(\n                    radius: 14,\n                  ),\n                ),\n                Text(AppLocalizations.of(context)!.loading),\n              ],\n            ),\n          );\n        }\n        if (snapshot.hasError) {\n          print(\"${snapshot.error}\");\n          print(\"${snapshot.stackTrace}\");\n          return Card(\n            child: InkWell(\n              onTap: _next,\n              child: Column(\n                children: [\n                  Container(\n                    padding: const EdgeInsets.only(top: 10, bottom: 10),\n                    child: const Icon(Icons.sync_problem_rounded),\n                  ),\n                  Text(AppLocalizations.of(context)!.errorAndTapRetry),\n                ],\n              ),\n            ),\n          );\n        }\n        return Container();\n      },\n    );\n  }\n\n  Widget _buildImageCard(ComicSimple item) {\n    return GestureDetector(\n      onTap: () {\n        Navigator.of(context).push(MaterialPageRoute(\n          builder: (context) {\n            return ComicInfoScreen(item.id, item.title);\n          },\n        ));\n      },\n      child: Card(\n        child: LayoutBuilder(\n          builder: (BuildContext context, BoxConstraints constraints) {\n            return HorizontalStretchNHentaiImage(\n              url: item.thumb,\n              originSize: Size(\n                item.thumbWidth.toDouble(),\n                item.thumbHeight.toDouble(),\n              ),\n            );\n          },\n        ),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/file_photo_view_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'package:nhentai/basic/common/cross.dart';\nimport 'package:photo_view/photo_view.dart';\n\n// 预览图片\nclass FilePhotoViewScreen extends StatelessWidget {\n  final String filePath;\n\n  const FilePhotoViewScreen(this.filePath);\n\n  @override\n  Widget build(BuildContext context) => Scaffold(\n        body: Stack(\n          children: [\n            GestureDetector(\n              onLongPress: () async {\n                String? choose =\n                    await chooseListDialog(context, '请选择', ['保存图片']);\n                switch (choose) {\n                  case '保存图片':\n                    saveImage(filePath, context);\n                    break;\n                }\n              },\n              child: PhotoView(\n                imageProvider: FileImage(File(filePath)),\n              ),\n            ),\n            InkWell(\n              onTap: () => Navigator.of(context).pop(),\n              child: Container(\n                margin: const EdgeInsets.only(top: 30),\n                padding: const EdgeInsets.only(left: 4, right: 4),\n                decoration: BoxDecoration(\n                  color: Colors.black.withOpacity(.75),\n                  borderRadius: const BorderRadius.only(\n                    topRight: Radius.circular(8),\n                    bottomRight: Radius.circular(8),\n                  ),\n                ),\n                child:\n                    const Icon(Icons.keyboard_backspace, color: Colors.white),\n              ),\n            ),\n          ],\n        ),\n      );\n}\n"
  },
  {
    "path": "lib/screens/init_screen.dart",
    "content": "import 'dart:io';\n\nimport 'package:app_links/app_links.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/configs/proxy.dart';\nimport 'package:nhentai/basic/configs/reader_direction.dart';\nimport 'package:nhentai/basic/configs/reader_type.dart';\nimport 'package:nhentai/basic/configs/themes.dart';\nimport 'package:nhentai/basic/configs/version.dart';\nimport 'comic_info_screen.dart';\nimport 'comics_screen.dart';\n\nclass InitScreen extends StatefulWidget {\n  const InitScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _InitScreenState();\n}\n\nclass _InitScreenState extends State<InitScreen> {\n  @override\n  void initState() {\n    _init();\n    super.initState();\n  }\n\n  Future<void> _init() async {\n    await initVersion();\n    autoCheckNewVersion();\n    await initTheme();\n    await initProxy();\n    await initReaderType();\n    await initReaderDirection();\n\n    late Widget gotoScreen;\n    String? initUrl;\n    if (Platform.isAndroid || Platform.isIOS) {\n      try {\n        final appLinks = AppLinks();\n        initUrl = (await appLinks.getInitialAppLink())?.toString();\n        // Use the uri and warn the user, if it is not correct,\n        // but keep in mind it could be `null`.\n      } on FormatException {\n        // Handle exception by warning the user their action did not succeed\n        // return?\n      }\n    }\n    if (initUrl != null) {\n      RegExp regExp = RegExp(r\"^https://nhentai\\.net/g/(\\d+)/$\");\n      final matches = regExp.allMatches(initUrl!);\n      if (matches.isNotEmpty) {\n        final id = int.parse(matches.first.group(1)!);\n        gotoScreen = ComicInfoScreen(id, \"\");\n      } else {\n        gotoScreen = ComicsScreen();\n      }\n    } else {\n      gotoScreen = ComicsScreen();\n    }\n\n    Navigator.of(context).pushReplacement(MaterialPageRoute(\n      builder: (BuildContext context) => gotoScreen,\n    ));\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Stack(\n      children: [\n        Container(\n          color: const Color(0xff313131),\n        ),\n        SafeArea(\n          child: Column(\n            children: [\n              Expanded(child: Container()),\n              Material(\n                color: const Color(0x00000000),\n                child: Text(\n                  AppLocalizations.of(context)!.initializing,\n                  style: const TextStyle(color: Colors.white),\n                ),\n              ),\n            ],\n          ),\n        ),\n        SafeArea(\n          child: Center(\n            child: LayoutBuilder(\n              builder: (BuildContext context, BoxConstraints constraints) {\n                var size = constraints.maxWidth < constraints.maxHeight\n                    ? constraints.maxWidth\n                    : constraints.maxHeight;\n                size /= 2;\n                return SizedBox(\n                  width: size,\n                  height: size,\n                  child: Image.asset(\n                    \"lib/assets/startup.png\",\n                    width: size,\n                    height: size,\n                  ),\n                );\n              },\n            ),\n          ),\n        ),\n      ],\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/settings_screen.dart",
    "content": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/configs/proxy.dart';\nimport 'package:nhentai/basic/configs/themes.dart';\nimport 'package:nhentai/basic/configs/version.dart';\n\nclass SettingsScreen extends StatelessWidget {\n  const SettingsScreen({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text(AppLocalizations.of(context)!.settings),\n      ),\n      body: ListView(\n        children: [\n          const Divider(),\n          themeSetting(context),\n          const Divider(),\n          proxySetting(),\n          const Divider(),\n          autoUpdateCheckSetting(),\n          const Divider(),\n          const VersionInfo(),\n          const Divider(),\n        ],\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "lib/screens/webview_screen.dart",
    "content": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_inappwebview/flutter_inappwebview.dart';\nimport 'package:nhentai/basic/channels/nhentai.dart';\nimport 'package:nhentai/basic/common/common.dart';\n\nclass WebViewScreen extends StatefulWidget {\n  const WebViewScreen({Key? key}) : super(key: key);\n\n  @override\n  State<StatefulWidget> createState() => _WebViewScreenState();\n}\n\nclass _WebViewScreenState extends State<WebViewScreen> {\n  late InAppWebViewController _webViewController;\n  late CookieManager _cookieManager;\n\n  @override\n  void initState() {\n    _cookieManager = CookieManager.instance();\n    super.initState();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: const Text(\"Load web cookies\"),\n        actions: [\n          IconButton(\n            onPressed: () {\n              _webViewController.loadUrl(\n                  urlRequest:\n                      URLRequest(url: Uri.parse('https://nhentai.net/')));\n            },\n            icon: const Icon(Icons.refresh),\n          ),\n          IconButton(\n            onPressed: () async {\n              final body = await _webViewController.evaluateJavascript(\n                  source: \"navigator.userAgent\");\n              await nHentai.setUserAgent(body);\n\n              // var cookies =\n              // await _webViewController.evaluateJavascript(source: \"document.cookie\");\n              //\n              // if (cookies.startsWith(\"\\\"\")) {\n              //   cookies = cookies.replaceAll(\"\\\"\", \"\");\n              // }\n\n              final cookies = await _cookieManager.getCookies(\n                  url: Uri.parse('https://nhentai.net/'));\n              print(cookies.map((e) => \"${e.name}=${e.value}\").join(\"; \"));\n              await nHentai.setCookie(\n                  cookies.map((e) => \"${e.name}=${e.value}\").join(\"; \"));\n              Navigator.of(context).pop();\n            },\n            icon: const Icon(Icons.check),\n          ),\n        ],\n      ),\n      body: InAppWebView(\n        initialUrlRequest: URLRequest(\n          url: Uri.parse('https://nhentai.net/'),\n        ),\n        onLoadStart: (c, url) {\n          print(\"onLoadStart\");\n          _webViewController = c;\n        },\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "linux/.gitignore",
    "content": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\nset(BINARY_NAME \"nhentai\")\nset(APPLICATION_ID \"com.example.nhentai\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Root filesystem for cross-building.\nif(FLUTTER_TARGET_PLATFORM_SYSROOT)\n  set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})\n  set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\n  set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\n  set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nendif()\n\n# Configure build options.\nif(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n  set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n    STRING \"Flutter build mode\" FORCE)\n  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n    \"Debug\" \"Profile\" \"Release\")\nendif()\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_14)\n  target_compile_options(${TARGET} PRIVATE -Wall -Werror)\n  target_compile_options(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:-O3>\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<NOT:$<CONFIG:Debug>>:NDEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\n\nadd_definitions(-DAPPLICATION_ID=\"${APPLICATION_ID}\")\n\n# Application build\nadd_executable(${BINARY_NAME}\n  \"main.cc\"\n  \"my_application.cc\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter)\ntarget_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n# Only the install-generated bundle's copy of the executable will launch\n# correctly, since the resources must in the right relative locations. To avoid\n# people trying to run the unbundled copy, put it in a subdirectory instead of\n# the default top-level location.\nset_target_properties(${BINARY_NAME}\n  PROPERTIES\n  RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/intermediates_do_not_run\"\n)\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# By default, \"installing\" just makes a relocatable bundle in the build\n# directory.\nset(BUILD_BUNDLE_DIR \"${PROJECT_BINARY_DIR}/bundle\")\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\n# Start with a clean build bundle directory every time.\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${BUILD_BUNDLE_DIR}/\\\")\n  \" COMPONENT Runtime)\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}/lib\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\nif(NOT CMAKE_BUILD_TYPE MATCHES \"Debug\")\n  install(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\n\n# Serves the same purpose as list(TRANSFORM ... PREPEND ...),\n# which isn't available in 3.10.\nfunction(list_prepend LIST_NAME PREFIX)\n    set(NEW_LIST \"\")\n    foreach(element ${${LIST_NAME}})\n        list(APPEND NEW_LIST \"${PREFIX}${element}\")\n    endforeach(element)\n    set(${LIST_NAME} \"${NEW_LIST}\" PARENT_SCOPE)\nendfunction()\n\n# === Flutter Library ===\n# System-level dependencies.\nfind_package(PkgConfig REQUIRED)\npkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)\npkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)\npkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)\n\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/libflutter_linux_gtk.so\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/lib/libapp.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"fl_basic_message_channel.h\"\n  \"fl_binary_codec.h\"\n  \"fl_binary_messenger.h\"\n  \"fl_dart_project.h\"\n  \"fl_engine.h\"\n  \"fl_json_message_codec.h\"\n  \"fl_json_method_codec.h\"\n  \"fl_message_codec.h\"\n  \"fl_method_call.h\"\n  \"fl_method_channel.h\"\n  \"fl_method_codec.h\"\n  \"fl_method_response.h\"\n  \"fl_plugin_registrar.h\"\n  \"fl_plugin_registry.h\"\n  \"fl_standard_message_codec.h\"\n  \"fl_standard_method_codec.h\"\n  \"fl_string_codec.h\"\n  \"fl_value.h\"\n  \"fl_view.h\"\n  \"flutter_linux.h\"\n)\nlist_prepend(FLUTTER_LIBRARY_HEADERS \"${EPHEMERAL_DIR}/flutter_linux/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}\")\ntarget_link_libraries(flutter INTERFACE\n  PkgConfig::GTK\n  PkgConfig::GLIB\n  PkgConfig::GIO\n)\nadd_dependencies(flutter flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CMAKE_CURRENT_BINARY_DIR}/_phony_\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh\"\n      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n)\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <url_launcher_linux/url_launcher_plugin.h>\n\nvoid fl_register_plugins(FlPluginRegistry* registry) {\n  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =\n      fl_plugin_registry_get_registrar_for_plugin(registry, \"UrlLauncherPlugin\");\n  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);\n}\n"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter_linux/flutter_linux.h>\n\n// Registers Flutter plugins.\nvoid fl_register_plugins(FlPluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  url_launcher_linux\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "linux/main.cc",
    "content": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  return g_application_run(G_APPLICATION(app), argc, argv);\n}\n"
  },
  {
    "path": "linux/my_application.cc",
    "content": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#endif\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nstruct _MyApplication {\n  GtkApplication parent_instance;\n  char** dart_entrypoint_arguments;\n};\n\nG_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)\n\n// Implements GApplication::activate.\nstatic void my_application_activate(GApplication* application) {\n  MyApplication* self = MY_APPLICATION(application);\n  GtkWindow* window =\n      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\n\n  // Use a header bar when running in GNOME as this is the common style used\n  // by applications and is the setup most users will be using (e.g. Ubuntu\n  // desktop).\n  // If running on X and not using GNOME then just use a traditional title bar\n  // in case the window manager does more exotic layout, e.g. tiling.\n  // If running on Wayland assume the header bar will work (may need changing\n  // if future cases occur).\n  gboolean use_header_bar = TRUE;\n#ifdef GDK_WINDOWING_X11\n  GdkScreen* screen = gtk_window_get_screen(window);\n  if (GDK_IS_X11_SCREEN(screen)) {\n    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\n    if (g_strcmp0(wm_name, \"GNOME Shell\") != 0) {\n      use_header_bar = FALSE;\n    }\n  }\n#endif\n  if (use_header_bar) {\n    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\n    gtk_widget_show(GTK_WIDGET(header_bar));\n    gtk_header_bar_set_title(header_bar, \"nhentai\");\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, \"nhentai\");\n  }\n\n  gtk_window_set_default_size(window, 1280, 720);\n  gtk_widget_show(GTK_WIDGET(window));\n\n  g_autoptr(FlDartProject) project = fl_dart_project_new();\n  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\n\n  FlView* view = fl_view_new(project);\n  gtk_widget_show(GTK_WIDGET(view));\n  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\n\n  fl_register_plugins(FL_PLUGIN_REGISTRY(view));\n\n  gtk_widget_grab_focus(GTK_WIDGET(view));\n}\n\n// Implements GApplication::local_command_line.\nstatic gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {\n  MyApplication* self = MY_APPLICATION(application);\n  // Strip out the first argument as it is the binary name.\n  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);\n\n  g_autoptr(GError) error = nullptr;\n  if (!g_application_register(application, nullptr, &error)) {\n     g_warning(\"Failed to register: %s\", error->message);\n     *exit_status = 1;\n     return TRUE;\n  }\n\n  g_application_activate(application);\n  *exit_status = 0;\n\n  return TRUE;\n}\n\n// Implements GObject::dispose.\nstatic void my_application_dispose(GObject* object) {\n  MyApplication* self = MY_APPLICATION(object);\n  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);\n  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);\n}\n\nstatic void my_application_class_init(MyApplicationClass* klass) {\n  G_APPLICATION_CLASS(klass)->activate = my_application_activate;\n  G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;\n  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;\n}\n\nstatic void my_application_init(MyApplication* self) {}\n\nMyApplication* my_application_new() {\n  return MY_APPLICATION(g_object_new(my_application_get_type(),\n                                     \"application-id\", APPLICATION_ID,\n                                     \"flags\", G_APPLICATION_NON_UNIQUE,\n                                     nullptr));\n}\n"
  },
  {
    "path": "linux/my_application.h",
    "content": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,\n                     GtkApplication)\n\n/**\n * my_application_new:\n *\n * Creates a new Flutter-based application.\n *\n * Returns: a new #MyApplication.\n */\nMyApplication* my_application_new();\n\n#endif  // FLUTTER_MY_APPLICATION_H_\n"
  },
  {
    "path": "macos/.gitignore",
    "content": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/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_macos\nimport url_launcher_macos\n\nfunc RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {\n  AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: \"AppLinksMacosPlugin\"))\n  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: \"UrlLauncherPlugin\"))\n}\n"
  },
  {
    "path": "macos/Podfile",
    "content": "platform :osx, '10.11'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['COCOAPODS_DISABLE_STATS'] = 'true'\n\nproject 'Runner', {\n  'Debug' => :debug,\n  'Profile' => :release,\n  'Release' => :release,\n}\n\ndef flutter_root\n  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)\n  unless File.exist?(generated_xcode_build_settings_path)\n    raise \"#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \\\"flutter pub get\\\" is executed first\"\n  end\n\n  File.foreach(generated_xcode_build_settings_path) do |line|\n    matches = line.match(/FLUTTER_ROOT\\=(.*)/)\n    return matches[1].strip if matches\n  end\n  raise \"FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \\\"flutter pub get\\\"\"\nend\n\nrequire File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)\n\nflutter_macos_podfile_setup\n\ntarget 'Runner' do\n  use_frameworks!\n  use_modular_headers!\n\n  flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))\nend\n\npost_install do |installer|\n  installer.pods_project.targets.each do |target|\n    flutter_additional_macos_build_settings(target)\n  end\nend\n"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\n@NSApplicationMain\nclass AppDelegate: FlutterAppDelegate {\n  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {\n    return true\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_32.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"32x32\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_64.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_128.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"128x128\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_256.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"256x256\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"2x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_512.png\",\n      \"scale\" : \"1x\"\n    },\n    {\n      \"size\" : \"512x512\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_1024.png\",\n      \"scale\" : \"2x\"\n    }\n  ],\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion=\"14490.70\" targetRuntime=\"MacOSX.Cocoa\" propertyAccessControl=\"none\" useAutolayout=\"YES\" customObjectInstantitationMethod=\"direct\">\n    <dependencies>\n        <deployment identifier=\"macosx\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.CocoaPlugin\" version=\"14490.70\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <objects>\n        <customObject id=\"-2\" userLabel=\"File's Owner\" customClass=\"NSApplication\">\n            <connections>\n                <outlet property=\"delegate\" destination=\"Voe-Tx-rLC\" id=\"GzC-gU-4Uq\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"-1\" userLabel=\"First Responder\" customClass=\"FirstResponder\"/>\n        <customObject id=\"-3\" userLabel=\"Application\" customClass=\"NSObject\"/>\n        <customObject id=\"Voe-Tx-rLC\" customClass=\"AppDelegate\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <connections>\n                <outlet property=\"applicationMenu\" destination=\"uQy-DD-JDr\" id=\"XBo-yE-nKs\"/>\n                <outlet property=\"mainFlutterWindow\" destination=\"QvC-M9-y7g\" id=\"gIp-Ho-8D9\"/>\n            </connections>\n        </customObject>\n        <customObject id=\"YLy-65-1bz\" customClass=\"NSFontManager\"/>\n        <menu title=\"Main Menu\" systemMenu=\"main\" id=\"AYu-sK-qS6\">\n            <items>\n                <menuItem title=\"APP_NAME\" id=\"1Xt-HY-uBw\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"APP_NAME\" systemMenu=\"apple\" id=\"uQy-DD-JDr\">\n                        <items>\n                            <menuItem title=\"About APP_NAME\" id=\"5kV-Vb-QxS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"orderFrontStandardAboutPanel:\" target=\"-1\" id=\"Exp-CZ-Vem\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"VOq-y0-SEH\"/>\n                            <menuItem title=\"Preferences…\" keyEquivalent=\",\" id=\"BOF-NM-1cW\"/>\n                            <menuItem isSeparatorItem=\"YES\" id=\"wFC-TO-SCJ\"/>\n                            <menuItem title=\"Services\" id=\"NMo-om-nkz\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Services\" systemMenu=\"services\" id=\"hz9-B4-Xy5\"/>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"4je-JR-u6R\"/>\n                            <menuItem title=\"Hide APP_NAME\" keyEquivalent=\"h\" id=\"Olw-nP-bQN\">\n                                <connections>\n                                    <action selector=\"hide:\" target=\"-1\" id=\"PnN-Uc-m68\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Hide Others\" keyEquivalent=\"h\" id=\"Vdr-fp-XzO\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"hideOtherApplications:\" target=\"-1\" id=\"VT4-aY-XCT\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Show All\" id=\"Kd2-mp-pUS\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"unhideAllApplications:\" target=\"-1\" id=\"Dhg-Le-xox\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"kCx-OE-vgT\"/>\n                            <menuItem title=\"Quit APP_NAME\" keyEquivalent=\"q\" id=\"4sb-4s-VLi\">\n                                <connections>\n                                    <action selector=\"terminate:\" target=\"-1\" id=\"Te7-pn-YzF\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Edit\" id=\"5QF-Oa-p0T\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Edit\" id=\"W48-6f-4Dl\">\n                        <items>\n                            <menuItem title=\"Undo\" keyEquivalent=\"z\" id=\"dRJ-4n-Yzg\">\n                                <connections>\n                                    <action selector=\"undo:\" target=\"-1\" id=\"M6e-cu-g7V\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Redo\" keyEquivalent=\"Z\" id=\"6dh-zS-Vam\">\n                                <connections>\n                                    <action selector=\"redo:\" target=\"-1\" id=\"oIA-Rs-6OD\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"WRV-NI-Exz\"/>\n                            <menuItem title=\"Cut\" keyEquivalent=\"x\" id=\"uRl-iY-unG\">\n                                <connections>\n                                    <action selector=\"cut:\" target=\"-1\" id=\"YJe-68-I9s\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Copy\" keyEquivalent=\"c\" id=\"x3v-GG-iWU\">\n                                <connections>\n                                    <action selector=\"copy:\" target=\"-1\" id=\"G1f-GL-Joy\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste\" keyEquivalent=\"v\" id=\"gVA-U4-sdL\">\n                                <connections>\n                                    <action selector=\"paste:\" target=\"-1\" id=\"UvS-8e-Qdg\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Paste and Match Style\" keyEquivalent=\"V\" id=\"WeT-3V-zwk\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"pasteAsPlainText:\" target=\"-1\" id=\"cEh-KX-wJQ\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Delete\" id=\"pa3-QI-u2k\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"delete:\" target=\"-1\" id=\"0Mk-Ml-PaM\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Select All\" keyEquivalent=\"a\" id=\"Ruw-6m-B2m\">\n                                <connections>\n                                    <action selector=\"selectAll:\" target=\"-1\" id=\"VNm-Mi-diN\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"uyl-h8-XO2\"/>\n                            <menuItem title=\"Find\" id=\"4EN-yA-p0u\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Find\" id=\"1b7-l0-nxx\">\n                                    <items>\n                                        <menuItem title=\"Find…\" tag=\"1\" keyEquivalent=\"f\" id=\"Xz5-n4-O0W\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"cD7-Qs-BN4\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find and Replace…\" tag=\"12\" keyEquivalent=\"f\" id=\"YEy-JH-Tfz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\" option=\"YES\" command=\"YES\"/>\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"WD3-Gg-5AJ\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Next\" tag=\"2\" keyEquivalent=\"g\" id=\"q09-fT-Sye\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"NDo-RZ-v9R\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Find Previous\" tag=\"3\" keyEquivalent=\"G\" id=\"OwM-mh-QMV\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"HOh-sY-3ay\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Use Selection for Find\" tag=\"7\" keyEquivalent=\"e\" id=\"buJ-ug-pKt\">\n                                            <connections>\n                                                <action selector=\"performFindPanelAction:\" target=\"-1\" id=\"U76-nv-p5D\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Jump to Selection\" keyEquivalent=\"j\" id=\"S0p-oC-mLd\">\n                                            <connections>\n                                                <action selector=\"centerSelectionInVisibleArea:\" target=\"-1\" id=\"IOG-6D-g5B\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Spelling and Grammar\" id=\"Dv1-io-Yv7\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Spelling\" id=\"3IN-sU-3Bg\">\n                                    <items>\n                                        <menuItem title=\"Show Spelling and Grammar\" keyEquivalent=\":\" id=\"HFo-cy-zxI\">\n                                            <connections>\n                                                <action selector=\"showGuessPanel:\" target=\"-1\" id=\"vFj-Ks-hy3\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Document Now\" keyEquivalent=\";\" id=\"hz2-CU-CR7\">\n                                            <connections>\n                                                <action selector=\"checkSpelling:\" target=\"-1\" id=\"fz7-VC-reM\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"bNw-od-mp5\"/>\n                                        <menuItem title=\"Check Spelling While Typing\" id=\"rbD-Rh-wIN\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleContinuousSpellChecking:\" target=\"-1\" id=\"7w6-Qz-0kB\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Check Grammar With Spelling\" id=\"mK6-2p-4JG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleGrammarChecking:\" target=\"-1\" id=\"muD-Qn-j4w\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Correct Spelling Automatically\" id=\"78Y-hA-62v\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticSpellingCorrection:\" target=\"-1\" id=\"2lM-Qi-WAP\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Substitutions\" id=\"9ic-FL-obx\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Substitutions\" id=\"FeM-D8-WVr\">\n                                    <items>\n                                        <menuItem title=\"Show Substitutions\" id=\"z6F-FW-3nz\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"orderFrontSubstitutionsPanel:\" target=\"-1\" id=\"oku-mr-iSq\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem isSeparatorItem=\"YES\" id=\"gPx-C9-uUO\"/>\n                                        <menuItem title=\"Smart Copy/Paste\" id=\"9yt-4B-nSM\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleSmartInsertDelete:\" target=\"-1\" id=\"3IJ-Se-DZD\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Quotes\" id=\"hQb-2v-fYv\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticQuoteSubstitution:\" target=\"-1\" id=\"ptq-xd-QOA\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Dashes\" id=\"rgM-f4-ycn\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDashSubstitution:\" target=\"-1\" id=\"oCt-pO-9gS\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Smart Links\" id=\"cwL-P1-jid\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticLinkDetection:\" target=\"-1\" id=\"Gip-E3-Fov\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Data Detectors\" id=\"tRr-pd-1PS\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticDataDetection:\" target=\"-1\" id=\"R1I-Nq-Kbl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Text Replacement\" id=\"HFQ-gK-NFA\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"toggleAutomaticTextReplacement:\" target=\"-1\" id=\"DvP-Fe-Py6\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Transformations\" id=\"2oI-Rn-ZJC\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Transformations\" id=\"c8a-y6-VQd\">\n                                    <items>\n                                        <menuItem title=\"Make Upper Case\" id=\"vmV-6d-7jI\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"uppercaseWord:\" target=\"-1\" id=\"sPh-Tk-edu\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Make Lower Case\" id=\"d9M-CD-aMd\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"lowercaseWord:\" target=\"-1\" id=\"iUZ-b5-hil\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Capitalize\" id=\"UEZ-Bs-lqG\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"capitalizeWord:\" target=\"-1\" id=\"26H-TL-nsh\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                            <menuItem title=\"Speech\" id=\"xrE-MZ-jX0\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <menu key=\"submenu\" title=\"Speech\" id=\"3rS-ZA-NoH\">\n                                    <items>\n                                        <menuItem title=\"Start Speaking\" id=\"Ynk-f8-cLZ\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"startSpeaking:\" target=\"-1\" id=\"654-Ng-kyl\"/>\n                                            </connections>\n                                        </menuItem>\n                                        <menuItem title=\"Stop Speaking\" id=\"Oyz-dy-DGm\">\n                                            <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                            <connections>\n                                                <action selector=\"stopSpeaking:\" target=\"-1\" id=\"dX8-6p-jy9\"/>\n                                            </connections>\n                                        </menuItem>\n                                    </items>\n                                </menu>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"View\" id=\"H8h-7b-M4v\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"View\" id=\"HyV-fh-RgO\">\n                        <items>\n                            <menuItem title=\"Enter Full Screen\" keyEquivalent=\"f\" id=\"4J7-dP-txa\">\n                                <modifierMask key=\"keyEquivalentModifierMask\" control=\"YES\" command=\"YES\"/>\n                                <connections>\n                                    <action selector=\"toggleFullScreen:\" target=\"-1\" id=\"dU3-MA-1Rq\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n                <menuItem title=\"Window\" id=\"aUF-d1-5bR\">\n                    <modifierMask key=\"keyEquivalentModifierMask\"/>\n                    <menu key=\"submenu\" title=\"Window\" systemMenu=\"window\" id=\"Td7-aD-5lo\">\n                        <items>\n                            <menuItem title=\"Minimize\" keyEquivalent=\"m\" id=\"OY7-WF-poV\">\n                                <connections>\n                                    <action selector=\"performMiniaturize:\" target=\"-1\" id=\"VwT-WD-YPe\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem title=\"Zoom\" id=\"R4o-n2-Eq4\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"performZoom:\" target=\"-1\" id=\"DIl-cC-cCs\"/>\n                                </connections>\n                            </menuItem>\n                            <menuItem isSeparatorItem=\"YES\" id=\"eu3-7i-yIM\"/>\n                            <menuItem title=\"Bring All to Front\" id=\"LE2-aR-0XJ\">\n                                <modifierMask key=\"keyEquivalentModifierMask\"/>\n                                <connections>\n                                    <action selector=\"arrangeInFront:\" target=\"-1\" id=\"DRN-fu-gQh\"/>\n                                </connections>\n                            </menuItem>\n                        </items>\n                    </menu>\n                </menuItem>\n            </items>\n            <point key=\"canvasLocation\" x=\"142\" y=\"-258\"/>\n        </menu>\n        <window title=\"APP_NAME\" allowsToolTipsWhenApplicationIsInactive=\"NO\" autorecalculatesKeyViewLoop=\"NO\" releasedWhenClosed=\"NO\" animationBehavior=\"default\" id=\"QvC-M9-y7g\" customClass=\"MainFlutterWindow\" customModule=\"Runner\" customModuleProvider=\"target\">\n            <windowStyleMask key=\"styleMask\" titled=\"YES\" closable=\"YES\" miniaturizable=\"YES\" resizable=\"YES\"/>\n            <rect key=\"contentRect\" x=\"335\" y=\"390\" width=\"800\" height=\"600\"/>\n            <rect key=\"screenRect\" x=\"0.0\" y=\"0.0\" width=\"2560\" height=\"1577\"/>\n            <view key=\"contentView\" wantsLayer=\"YES\" id=\"EiT-Mj-1SZ\">\n                <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"800\" height=\"600\"/>\n                <autoresizingMask key=\"autoresizingMask\"/>\n            </view>\n        </window>\n    </objects>\n</document>\n"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "content": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the\n// future. If not, the values below would default to using the project name when this becomes a\n// 'flutter create' template.\n\n// The application's name. By default this is also the title of the Flutter window.\nPRODUCT_NAME = nhentai\n\n// The application's bundle identifier\nPRODUCT_BUNDLE_IDENTIFIER = com.example.nhentai\n\n// The copyright displayed in application information\nPRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.\n"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "content": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "content": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings\nGCC_WARN_UNDECLARED_SELECTOR = YES\nCLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES\nCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE\nCLANG_WARN__DUPLICATE_METHOD_MATCH = YES\nCLANG_WARN_PRAGMA_PACK = YES\nCLANG_WARN_STRICT_PROTOTYPES = YES\nCLANG_WARN_COMMA = YES\nGCC_WARN_STRICT_SELECTOR_MATCH = YES\nCLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES\nCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES\nGCC_WARN_SHADOW = YES\nCLANG_WARN_UNREACHABLE_CODE = YES\n"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIconFile</key>\n\t<string></string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(FLUTTER_BUILD_NAME)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(FLUTTER_BUILD_NUMBER)</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>$(PRODUCT_COPYRIGHT)</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "content": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterViewController = FlutterViewController.init()\n    let windowFrame = self.frame\n    self.contentViewController = flutterViewController\n    self.setFrame(windowFrame, display: true)\n\n    RegisterGeneratedPlugins(registry: flutterViewController)\n\n    super.awakeFromNib()\n  }\n}\n"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.app-sandbox</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 51;\n\tobjects = {\n\n/* Begin PBXAggregateTarget section */\n\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {\n\t\t\tisa = PBXAggregateTarget;\n\t\t\tbuildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC111E2044C6BF0003C045 /* ShellScript */,\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = \"Flutter Assemble\";\n\t\t\tproductName = FLX;\n\t\t};\n/* End PBXAggregateTarget section */\n\n/* Begin PBXBuildFile section */\n\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };\n\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };\n\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };\n\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };\n\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 33CC10E52044A3C60003C045 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 33CC111A2044C6BA0003C045;\n\t\t\tremoteInfo = FLX;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXCopyFilesBuildPhase section */\n\t\t33CC110E2044A8840003C045 /* Bundle Framework */ = {\n\t\t\tisa = PBXCopyFilesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tdstPath = \"\";\n\t\t\tdstSubfolderSpec = 10;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tname = \"Bundle Framework\";\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXCopyFilesBuildPhase section */\n\n/* Begin PBXFileReference section */\n\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = \"<group>\"; };\n\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = \"<group>\"; };\n\t\t33CC10ED2044A3C60003C045 /* nhentai.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = \"nhentai.app\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = \"<group>\"; };\n\t\t33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = \"<group>\"; };\n\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = \"<group>\"; };\n\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = \"Flutter-Release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = \"Flutter-Generated.xcconfig\"; path = \"ephemeral/Flutter-Generated.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = \"<group>\"; };\n\t\t33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = \"<group>\"; };\n\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = \"<group>\"; };\n\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = \"<group>\"; };\n\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t33CC10EA2044A3C60003C045 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t33BA886A226E78AF003329D5 /* Configs */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33E5194F232828860026EE4D /* AppInfo.xcconfig */,\n\t\t\t\t9740EEB21CF90195004384FC /* Debug.xcconfig */,\n\t\t\t\t7AFA3C8E1D35360C0083082E /* Release.xcconfig */,\n\t\t\t\t333000ED22D3DE5D00554162 /* Warnings.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configs;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10E42044A3C60003C045 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33FAB671232836740065AC1E /* Runner */,\n\t\t\t\t33CEB47122A05771004F2AC0 /* Flutter */,\n\t\t\t\t33CC10EE2044A3C60003C045 /* Products */,\n\t\t\t\tD73912EC22F37F3D000D13A0 /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC10EE2044A3C60003C045 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10ED2044A3C60003C045 /* nhentai.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CC11242044D66E0003C045 /* Resources */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F22044A3C60003C045 /* Assets.xcassets */,\n\t\t\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */,\n\t\t\t\t33CC10F72044A3C60003C045 /* Info.plist */,\n\t\t\t);\n\t\t\tname = Resources;\n\t\t\tpath = ..;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33CEB47122A05771004F2AC0 /* Flutter */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,\n\t\t\t\t33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,\n\t\t\t\t33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,\n\t\t\t\t33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,\n\t\t\t);\n\t\t\tpath = Flutter;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t33FAB671232836740065AC1E /* Runner */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F02044A3C60003C045 /* AppDelegate.swift */,\n\t\t\t\t33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,\n\t\t\t\t33E51913231747F40026EE4D /* DebugProfile.entitlements */,\n\t\t\t\t33E51914231749380026EE4D /* Release.entitlements */,\n\t\t\t\t33CC11242044D66E0003C045 /* Resources */,\n\t\t\t\t33BA886A226E78AF003329D5 /* Configs */,\n\t\t\t);\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tD73912EC22F37F3D000D13A0 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t33CC10EC2044A3C60003C045 /* Runner */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t33CC10E92044A3C60003C045 /* Sources */,\n\t\t\t\t33CC10EA2044A3C60003C045 /* Frameworks */,\n\t\t\t\t33CC10EB2044A3C60003C045 /* Resources */,\n\t\t\t\t33CC110E2044A8840003C045 /* Bundle Framework */,\n\t\t\t\t3399D490228B24CF009A79C7 /* ShellScript */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = Runner;\n\t\t\tproductName = Runner;\n\t\t\tproductReference = 33CC10ED2044A3C60003C045 /* nhentai.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t33CC10E52044A3C60003C045 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 0920;\n\t\t\t\tLastUpgradeCheck = 0930;\n\t\t\t\tORGANIZATIONNAME = \"\";\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t33CC10EC2044A3C60003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t\tSystemCapabilities = {\n\t\t\t\t\t\t\tcom.apple.Sandbox = {\n\t\t\t\t\t\t\t\tenabled = 1;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t};\n\t\t\t\t\t};\n\t\t\t\t\t33CC111A2044C6BA0003C045 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tProvisioningStyle = Manual;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */;\n\t\t\tcompatibilityVersion = \"Xcode 9.3\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 33CC10E42044A3C60003C045;\n\t\t\tproductRefGroup = 33CC10EE2044A3C60003C045 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t33CC10EC2044A3C60003C045 /* Runner */,\n\t\t\t\t33CC111A2044C6BA0003C045 /* Flutter Assemble */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t33CC10EB2044A3C60003C045 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,\n\t\t\t\t33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t3399D490228B24CF009A79C7 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"echo \\\"$PRODUCT_NAME.app\\\" > \\\"$PROJECT_DIR\\\"/Flutter/ephemeral/.app_filename && \\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh embed\\n\";\n\t\t};\n\t\t33CC111E2044C6BF0003C045 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterInputs.xcfilelist,\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\tFlutter/ephemeral/tripwire,\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t\tFlutter/ephemeral/FlutterOutputs.xcfilelist,\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"$FLUTTER_ROOT\\\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t33CC10E92044A3C60003C045 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,\n\t\t\t\t33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,\n\t\t\t\t335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t33CC11202044C79F0003C045 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;\n\t\t\ttargetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin PBXVariantGroup section */\n\t\t33CC10F42044A3C60003C045 /* MainMenu.xib */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t33CC10F52044A3C60003C045 /* Base */,\n\t\t\t);\n\t\t\tname = MainMenu.xib;\n\t\t\tpath = Runner;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t338D0CE9231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEA231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t338D0CEB231458BD00FA5F75 /* Profile */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Profile;\n\t\t};\n\t\t33CC10F92044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FA2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"-\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tMACOSX_DEPLOYMENT_TARGET = 10.11;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = macosx;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC10FC2044A3C60003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC10FD2044A3C60003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCOMBINE_HIDPI_IMAGES = YES;\n\t\t\t\tINFOPLIST_FILE = Runner/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/../Frameworks\",\n\t\t\t\t);\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t33CC111C2044C6BA0003C045 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Manual;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t33CC111D2044C6BA0003C045 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t33CC10E82044A3C60003C045 /* Build configuration list for PBXProject \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10F92044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FA2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CE9231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget \"Runner\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC10FC2044A3C60003C045 /* Debug */,\n\t\t\t\t33CC10FD2044A3C60003C045 /* Release */,\n\t\t\t\t338D0CEA231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget \"Flutter Assemble\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t33CC111C2044C6BA0003C045 /* Debug */,\n\t\t\t\t33CC111D2044C6BA0003C045 /* Release */,\n\t\t\t\t338D0CEB231458BD00FA5F75 /* Profile */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 33CC10E52044A3C60003C045 /* Project object */;\n}\n"
  },
  {
    "path": "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 = \"1000\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n               BuildableName = \"nhentai.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 = \"nhentai.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"nhentai.app\"\n            BlueprintName = \"Runner\"\n            ReferencedContainer = \"container:Runner.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Profile\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"33CC10EC2044A3C60003C045\"\n            BuildableName = \"nhentai.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</Workspace>\n"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "pubspec.yaml",
    "content": "name: nhentai\ndescription: nhentai client\npublish_to: 'none'\nversion: 0.0.9+2\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n\ndependencies:\n  flutter:\n    sdk: flutter\n  flutter_localizations:\n    sdk: flutter\n  cupertino_icons: ^1.0.2\n  flutter_styled_toast: 2.0.0\n  intl: ^0.17.0\n  event: ^2.0.5\n  waterfall_flow: ^3.0.1\n  photo_view: ^0.13.0\n  clipboard: ^0.1.3\n  hidden_drawer_menu: ^3.0.1\n  url_launcher: ^6.0.17\n  filesystem_picker: 2.0.0\n  date_format: ^2.0.4\n  another_xlider: 1.0.1+2\n  modal_bottom_sheet: ^2.0.0\n  app_links: ^3.2.0\n  flutter_inappwebview: ^5.5.0+2\n  permission_handler: ^10.1.0\n\ndev_dependencies:\n  flutter_test:\n    sdk: flutter\n  flutter_lints: ^1.0.0\n\nflutter:\n  uses-material-design: true\n  generate: true\n  assets:\n    - lib/assets/\n"
  },
  {
    "path": "scripts/README.md",
    "content": "用于记录作者构建时使用的脚本\n"
  },
  {
    "path": "scripts/bind-android-debug.sh",
    "content": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -androidapi 19 -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./\n"
  },
  {
    "path": "scripts/bind-ios-arm64.sh",
    "content": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -androidapi 19 -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "scripts/bind-ios.sh",
    "content": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "scripts/build-apk-arm.sh",
    "content": "# 仅构建arm的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile bind -androidapi 19 -target=android/arm -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-arm\n"
  },
  {
    "path": "scripts/build-apk-arm64.sh",
    "content": "# 仅构建arm64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile bind -androidapi 19 -target=android/arm64 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-arm64\n"
  },
  {
    "path": "scripts/build-apk-x64.sh",
    "content": "# 仅构建x86_64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile bind -androidapi 19 -target=android/amd64 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-x64\n"
  },
  {
    "path": "scripts/build-apk-x86.sh",
    "content": "# 仅构建x86的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile bind -androidapi 19 -target=android/386 -o lib/Mobile.aar ./\ncd ../..\nflutter build apk --target-platform android-x86\n"
  },
  {
    "path": "scripts/build-ipa.sh",
    "content": "# 构建未签名的IPA\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile bind -iosversion 11.0 -target=ios -o lib/Mobile.xcframework ./\ncd ../..\nflutter build ios --release --no-codesign\n\ncd build\nmkdir -p Payload\nmv ios/iphoneos/Runner.app Payload\n\nsh ../scripts/thin-payload.sh\nzip -9 nosign.ipa -r Payload\n"
  },
  {
    "path": "scripts/build-macos-dmg.sh",
    "content": "# 构建macos\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\nhover build darwin-dmg\n"
  },
  {
    "path": "scripts/sign-apk-github-actions.sh",
    "content": "cd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\necho $KEY_FILE_BASE64 > key.jks.base64\nbase64 -d key.jks.base64 > key.jks\necho $KEY_PASSWORD | $ANDROID_HOME/build-tools/30.0.2/apksigner sign --ks key.jks build/app/outputs/flutter-apk/app-release.apk"
  },
  {
    "path": "scripts/thin-payload.sh",
    "content": "# 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa)\n\nforeachThin(){\n  for file in $1/*\n  do\n      if test -f $file\n      then\n           mime=$(file --mime-type -b $file)\n           if [ \"$mime\" == 'application/x-mach-binary' ]  || [ \"${file##*.}\"x = \"dylib\"x ]\n           then\n                echo thin $file\n                xcrun -sdk iphoneos lipo \"$file\" -thin arm64 -output \"$file\"\n                xcrun -sdk iphoneos bitcode_strip \"$file\" -r -o  \"$file\"\n                strip -S -x \"$file\" -o \"$file\"\n           fi\n      fi\n      if test -d $file\n      then\n          foreachThin $file\n      fi\n  done\n}\n\nforeachThin ./Payload\n"
  },
  {
    "path": "scripts/version.sh",
    "content": "# 设置版本号\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\nif [ \"$1\" == \"set\" ] ; then\n  if [ \"$2\" != \"\" ] ; then\n    echo $2 > lib/assets/version.txt\n  fi\n\nelif [ \"$1\" == \"unset\" ]; then\n    rm -f lib/assets/version.txt\nfi\n"
  },
  {
    "path": "test/widget_test.dart",
    "content": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester\n// utility that Flutter provides. For example, you can send tap and scroll\n// gestures. You can also use WidgetTester to find child widgets in the widget\n// tree, read text, and verify that the values of widget properties are correct.\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_test/flutter_test.dart';\n\nimport 'package:nhentai/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": "cmake_minimum_required(VERSION 3.15)\nproject(nhentai LANGUAGES CXX)\n\nset(BINARY_NAME \"nhentai\")\n\ncmake_policy(SET CMP0063 NEW)\n\nset(CMAKE_INSTALL_RPATH \"$ORIGIN/lib\")\n\n# Configure build options.\nget_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)\nif(IS_MULTICONFIG)\n  set(CMAKE_CONFIGURATION_TYPES \"Debug;Profile;Release\"\n    CACHE STRING \"\" FORCE)\nelse()\n  if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)\n    set(CMAKE_BUILD_TYPE \"Debug\" CACHE\n      STRING \"Flutter build mode\" FORCE)\n    set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS\n      \"Debug\" \"Profile\" \"Release\")\n  endif()\nendif()\n\nset(CMAKE_EXE_LINKER_FLAGS_PROFILE \"${CMAKE_EXE_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_SHARED_LINKER_FLAGS_PROFILE \"${CMAKE_SHARED_LINKER_FLAGS_RELEASE}\")\nset(CMAKE_C_FLAGS_PROFILE \"${CMAKE_C_FLAGS_RELEASE}\")\nset(CMAKE_CXX_FLAGS_PROFILE \"${CMAKE_CXX_FLAGS_RELEASE}\")\n\n# Use Unicode for all projects.\nadd_definitions(-DUNICODE -D_UNICODE)\n\n# Compilation settings that should be applied to most targets.\nfunction(APPLY_STANDARD_SETTINGS TARGET)\n  target_compile_features(${TARGET} PUBLIC cxx_std_17)\n  target_compile_options(${TARGET} PRIVATE /W4 /WX /wd\"4100\")\n  target_compile_options(${TARGET} PRIVATE /EHsc)\n  target_compile_definitions(${TARGET} PRIVATE \"_HAS_EXCEPTIONS=0\")\n  target_compile_definitions(${TARGET} PRIVATE \"$<$<CONFIG:Debug>:_DEBUG>\")\nendfunction()\n\nset(FLUTTER_MANAGED_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/flutter\")\n\n# Flutter library and tool build rules.\nadd_subdirectory(${FLUTTER_MANAGED_DIR})\n\n# Application build\nadd_subdirectory(\"runner\")\n\n# Generated plugin build rules, which manage building the plugins and adding\n# them to the application.\ninclude(flutter/generated_plugins.cmake)\n\n\n# === Installation ===\n# Support files are copied into place next to the executable, so that it can\n# run in place. This is done instead of making a separate bundle (as on Linux)\n# so that building and running from within Visual Studio will work.\nset(BUILD_BUNDLE_DIR \"$<TARGET_FILE_DIR:${BINARY_NAME}>\")\n# Make the \"install\" step default, as it's required to run.\nset(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)\nif(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)\n  set(CMAKE_INSTALL_PREFIX \"${BUILD_BUNDLE_DIR}\" CACHE PATH \"...\" FORCE)\nendif()\n\nset(INSTALL_BUNDLE_DATA_DIR \"${CMAKE_INSTALL_PREFIX}/data\")\nset(INSTALL_BUNDLE_LIB_DIR \"${CMAKE_INSTALL_PREFIX}\")\n\ninstall(TARGETS ${BINARY_NAME} RUNTIME DESTINATION \"${CMAKE_INSTALL_PREFIX}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_ICU_DATA_FILE}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  COMPONENT Runtime)\n\ninstall(FILES \"${FLUTTER_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n  COMPONENT Runtime)\n\nif(PLUGIN_BUNDLED_LIBRARIES)\n  install(FILES \"${PLUGIN_BUNDLED_LIBRARIES}\"\n    DESTINATION \"${INSTALL_BUNDLE_LIB_DIR}\"\n    COMPONENT Runtime)\nendif()\n\n# Fully re-copy the assets directory on each build to avoid having stale files\n# from a previous install.\nset(FLUTTER_ASSET_DIR_NAME \"flutter_assets\")\ninstall(CODE \"\n  file(REMOVE_RECURSE \\\"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\\\")\n  \" COMPONENT Runtime)\ninstall(DIRECTORY \"${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}\"\n  DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\" COMPONENT Runtime)\n\n# Install the AOT library on non-Debug builds only.\ninstall(FILES \"${AOT_LIBRARY}\" DESTINATION \"${INSTALL_BUNDLE_DATA_DIR}\"\n  CONFIGURATIONS Profile;Release\n  COMPONENT Runtime)\n"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provided via flutter tool.\ninclude(${EPHEMERAL_DIR}/generated_config.cmake)\n\n# TODO: Move the rest of this into files in ephemeral. See\n# https://github.com/flutter/flutter/issues/57146.\nset(WRAPPER_ROOT \"${EPHEMERAL_DIR}/cpp_client_wrapper\")\n\n# === Flutter Library ===\nset(FLUTTER_LIBRARY \"${EPHEMERAL_DIR}/flutter_windows.dll\")\n\n# Published to parent scope for install step.\nset(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)\nset(FLUTTER_ICU_DATA_FILE \"${EPHEMERAL_DIR}/icudtl.dat\" PARENT_SCOPE)\nset(PROJECT_BUILD_DIR \"${PROJECT_DIR}/build/\" PARENT_SCOPE)\nset(AOT_LIBRARY \"${PROJECT_DIR}/build/windows/app.so\" PARENT_SCOPE)\n\nlist(APPEND FLUTTER_LIBRARY_HEADERS\n  \"flutter_export.h\"\n  \"flutter_windows.h\"\n  \"flutter_messenger.h\"\n  \"flutter_plugin_registrar.h\"\n  \"flutter_texture_registrar.h\"\n)\nlist(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND \"${EPHEMERAL_DIR}/\")\nadd_library(flutter INTERFACE)\ntarget_include_directories(flutter INTERFACE\n  \"${EPHEMERAL_DIR}\"\n)\ntarget_link_libraries(flutter INTERFACE \"${FLUTTER_LIBRARY}.lib\")\nadd_dependencies(flutter flutter_assemble)\n\n# === Wrapper ===\nlist(APPEND CPP_WRAPPER_SOURCES_CORE\n  \"core_implementations.cc\"\n  \"standard_codec.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_PLUGIN\n  \"plugin_registrar.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND \"${WRAPPER_ROOT}/\")\nlist(APPEND CPP_WRAPPER_SOURCES_APP\n  \"flutter_engine.cc\"\n  \"flutter_view_controller.cc\"\n)\nlist(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND \"${WRAPPER_ROOT}/\")\n\n# Wrapper sources needed for a plugin.\nadd_library(flutter_wrapper_plugin STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n)\napply_standard_settings(flutter_wrapper_plugin)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  POSITION_INDEPENDENT_CODE ON)\nset_target_properties(flutter_wrapper_plugin PROPERTIES\n  CXX_VISIBILITY_PRESET hidden)\ntarget_link_libraries(flutter_wrapper_plugin PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_plugin PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_plugin flutter_assemble)\n\n# Wrapper sources needed for the runner.\nadd_library(flutter_wrapper_app STATIC\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\napply_standard_settings(flutter_wrapper_app)\ntarget_link_libraries(flutter_wrapper_app PUBLIC flutter)\ntarget_include_directories(flutter_wrapper_app PUBLIC\n  \"${WRAPPER_ROOT}/include\"\n)\nadd_dependencies(flutter_wrapper_app flutter_assemble)\n\n# === Flutter tool backend ===\n# _phony_ is a non-existent file to force this command to run every time,\n# since currently there's no way to get a full input/output list from the\n# flutter tool.\nset(PHONY_OUTPUT \"${CMAKE_CURRENT_BINARY_DIR}/_phony_\")\nset_source_files_properties(\"${PHONY_OUTPUT}\" PROPERTIES SYMBOLIC TRUE)\nadd_custom_command(\n  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}\n    ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}\n    ${CPP_WRAPPER_SOURCES_APP}\n    ${PHONY_OUTPUT}\n  COMMAND ${CMAKE_COMMAND} -E env\n    ${FLUTTER_TOOL_ENVIRONMENT}\n    \"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat\"\n      windows-x64 $<CONFIG>\n  VERBATIM\n)\nadd_custom_target(flutter_assemble DEPENDS\n  \"${FLUTTER_LIBRARY}\"\n  ${FLUTTER_LIBRARY_HEADERS}\n  ${CPP_WRAPPER_SOURCES_CORE}\n  ${CPP_WRAPPER_SOURCES_PLUGIN}\n  ${CPP_WRAPPER_SOURCES_APP}\n)\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.cc",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <app_links_windows/app_links_windows_plugin.h>\n#include <permission_handler_windows/permission_handler_windows_plugin.h>\n#include <url_launcher_windows/url_launcher_windows.h>\n\nvoid RegisterPlugins(flutter::PluginRegistry* registry) {\n  AppLinksWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"AppLinksWindowsPlugin\"));\n  PermissionHandlerWindowsPluginRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"PermissionHandlerWindowsPlugin\"));\n  UrlLauncherWindowsRegisterWithRegistrar(\n      registry->GetRegistrarForPlugin(\"UrlLauncherWindows\"));\n}\n"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "content": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUGIN_REGISTRANT_\n\n#include <flutter/plugin_registry.h>\n\n// Registers Flutter plugins.\nvoid RegisterPlugins(flutter::PluginRegistry* registry);\n\n#endif  // GENERATED_PLUGIN_REGISTRANT_\n"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "content": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  app_links_windows\n  permission_handler_windows\n  url_launcher_windows\n)\n\nlist(APPEND FLUTTER_FFI_PLUGIN_LIST\n)\n\nset(PLUGIN_BUNDLED_LIBRARIES)\n\nforeach(plugin ${FLUTTER_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})\n  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})\nendforeach(plugin)\n\nforeach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})\n  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})\n  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})\nendforeach(ffi_plugin)\n"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\nproject(runner LANGUAGES CXX)\n\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_window.cpp\"\n  \"main.cpp\"\n  \"utils.cpp\"\n  \"win32_window.cpp\"\n  \"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc\"\n  \"Runner.rc\"\n  \"runner.exe.manifest\"\n)\napply_standard_settings(${BINARY_NAME})\ntarget_compile_definitions(${BINARY_NAME} PRIVATE \"NOMINMAX\")\ntarget_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)\ntarget_include_directories(${BINARY_NAME} PRIVATE \"${CMAKE_SOURCE_DIR}\")\nadd_dependencies(${BINARY_NAME} flutter_assemble)\n"
  },
  {
    "path": "windows/runner/Runner.rc",
    "content": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_READONLY_SYMBOLS\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 2 resource.\n//\n#include \"winres.h\"\n\n/////////////////////////////////////////////////////////////////////////////\n#undef APSTUDIO_READONLY_SYMBOLS\n\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\nLANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"#include \"\"winres.h\"\"\\r\\n\"\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Icon\n//\n\n// Icon with lowest ID value placed first to ensure application icon\n// remains consistent on all systems.\nIDI_APP_ICON            ICON                    \"resources\\\\app_icon.ico\"\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\n#ifdef FLUTTER_BUILD_NUMBER\n#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER\n#else\n#define VERSION_AS_NUMBER 1,0,0\n#endif\n\n#ifdef FLUTTER_BUILD_NAME\n#define VERSION_AS_STRING #FLUTTER_BUILD_NAME\n#else\n#define VERSION_AS_STRING \"1.0.0\"\n#endif\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION VERSION_AS_NUMBER\n PRODUCTVERSION VERSION_AS_NUMBER\n FILEFLAGSMASK VS_FFI_FILEFLAGSMASK\n#ifdef _DEBUG\n FILEFLAGS VS_FF_DEBUG\n#else\n FILEFLAGS 0x0L\n#endif\n FILEOS VOS__WINDOWS32\n FILETYPE VFT_APP\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904e4\"\n        BEGIN\n            VALUE \"CompanyName\", \"com.example\" \"\\0\"\n            VALUE \"FileDescription\", \"A new Flutter project.\" \"\\0\"\n            VALUE \"FileVersion\", VERSION_AS_STRING \"\\0\"\n            VALUE \"InternalName\", \"nhentai\" \"\\0\"\n            VALUE \"LegalCopyright\", \"Copyright (C) 2021 com.example. All rights reserved.\" \"\\0\"\n            VALUE \"OriginalFilename\", \"nhentai.exe\" \"\\0\"\n            VALUE \"ProductName\", \"nhentai\" \"\\0\"\n            VALUE \"ProductVersion\", VERSION_AS_STRING \"\\0\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1252\n    END\nEND\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "content": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::FlutterWindow(const flutter::DartProject& project)\n    : project_(project) {}\n\nFlutterWindow::~FlutterWindow() {}\n\nbool FlutterWindow::OnCreate() {\n  if (!Win32Window::OnCreate()) {\n    return false;\n  }\n\n  RECT frame = GetClientArea();\n\n  // The size here must match the window dimensions to avoid unnecessary surface\n  // creation / destruction in the startup path.\n  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(\n      frame.right - frame.left, frame.bottom - frame.top, project_);\n  // Ensure that basic setup of the controller was successful.\n  if (!flutter_controller_->engine() || !flutter_controller_->view()) {\n    return false;\n  }\n  RegisterPlugins(flutter_controller_->engine());\n  SetChildContent(flutter_controller_->view()->GetNativeWindow());\n  return true;\n}\n\nvoid FlutterWindow::OnDestroy() {\n  if (flutter_controller_) {\n    flutter_controller_ = nullptr;\n  }\n\n  Win32Window::OnDestroy();\n}\n\nLRESULT\nFlutterWindow::MessageHandler(HWND hwnd, UINT const message,\n                              WPARAM const wparam,\n                              LPARAM const lparam) noexcept {\n  // Give Flutter, including plugins, an opportunity to handle window messages.\n  if (flutter_controller_) {\n    std::optional<LRESULT> result =\n        flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,\n                                                      lparam);\n    if (result) {\n      return *result;\n    }\n  }\n\n  switch (message) {\n    case WM_FONTCHANGE:\n      flutter_controller_->engine()->ReloadSystemFonts();\n      break;\n  }\n\n  return Win32Window::MessageHandler(hwnd, message, wparam, lparam);\n}\n"
  },
  {
    "path": "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\nint APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,\n                      _In_ wchar_t *command_line, _In_ int show_command) {\n  // Attach to console when present (e.g., 'flutter run') or create a\n  // new console when running with a debugger.\n  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {\n    CreateAndAttachConsole();\n  }\n\n  // Initialize COM, so that it is available for use in the library and/or\n  // plugins.\n  ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);\n\n  flutter::DartProject project(L\"data\");\n\n  std::vector<std::string> command_line_arguments =\n      GetCommandLineArguments();\n\n  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));\n\n  FlutterWindow window(project);\n  Win32Window::Point origin(10, 10);\n  Win32Window::Size size(1280, 720);\n  if (!window.CreateAndShow(L\"nhentai\", 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 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/>\n      <!-- Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/>\n      <!-- Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!-- Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n    </application>\n  </compatibility>\n</assembly>\n"
  },
  {
    "path": "windows/runner/utils.cpp",
    "content": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iostream>\n\nvoid CreateAndAttachConsole() {\n  if (::AllocConsole()) {\n    FILE *unused;\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stdout)) {\n      _dup2(_fileno(stdout), 1);\n    }\n    if (freopen_s(&unused, \"CONOUT$\", \"w\", stderr)) {\n      _dup2(_fileno(stdout), 2);\n    }\n    std::ios::sync_with_stdio();\n    FlutterDesktopResyncOutputStreams();\n  }\n}\n\nstd::vector<std::string> GetCommandLineArguments() {\n  // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.\n  int argc;\n  wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);\n  if (argv == nullptr) {\n    return std::vector<std::string>();\n  }\n\n  std::vector<std::string> command_line_arguments;\n\n  // Skip the first argument as it's the binary name.\n  for (int i = 1; i < argc; i++) {\n    command_line_arguments.push_back(Utf8FromUtf16(argv[i]));\n  }\n\n  ::LocalFree(argv);\n\n  return command_line_arguments;\n}\n\nstd::string Utf8FromUtf16(const wchar_t* utf16_string) {\n  if (utf16_string == nullptr) {\n    return std::string();\n  }\n  int target_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, nullptr, 0, nullptr, nullptr);\n  if (target_length == 0) {\n    return std::string();\n  }\n  std::string utf8_string;\n  utf8_string.resize(target_length);\n  int converted_length = ::WideCharToMultiByte(\n      CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,\n      -1, utf8_string.data(),\n      target_length, nullptr, nullptr);\n  if (converted_length == 0) {\n    return std::string();\n  }\n  return utf8_string;\n}\n"
  },
  {
    "path": "windows/runner/utils.h",
    "content": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the process, and redirects stdout and stderr to\n// it for both the runner and the Flutter library.\nvoid CreateAndAttachConsole();\n\n// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string\n// encoded in UTF-8. Returns an empty std::string on failure.\nstd::string Utf8FromUtf16(const wchar_t* utf16_string);\n\n// Gets the command line arguments passed in as a std::vector<std::string>,\n// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.\nstd::vector<std::string> GetCommandLineArguments();\n\n#endif  // RUNNER_UTILS_H_\n"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "content": "#include \"win32_window.h\"\n\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWindowClassName[] = L\"FLUTTER_RUNNER_WIN32_WINDOW\";\n\n// The number of Win32Window objects that currently exist.\nstatic int g_active_window_count = 0;\n\nusing EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);\n\n// Scale helper to convert logical scaler values to physical using passed in\n// scale factor\nint Scale(int source, double scale_factor) {\n  return static_cast<int>(source * scale_factor);\n}\n\n// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.\n// This API is only needed for PerMonitor V1 awareness mode.\nvoid EnableFullDpiSupportIfAvailable(HWND hwnd) {\n  HMODULE user32_module = LoadLibraryA(\"User32.dll\");\n  if (!user32_module) {\n    return;\n  }\n  auto enable_non_client_dpi_scaling =\n      reinterpret_cast<EnableNonClientDpiScaling*>(\n          GetProcAddress(user32_module, \"EnableNonClientDpiScaling\"));\n  if (enable_non_client_dpi_scaling != nullptr) {\n    enable_non_client_dpi_scaling(hwnd);\n    FreeLibrary(user32_module);\n  }\n}\n\n}  // namespace\n\n// Manages the Win32Window's window class registration.\nclass WindowClassRegistrar {\n public:\n  ~WindowClassRegistrar() = default;\n\n  // Returns the singleton registar instance.\n  static WindowClassRegistrar* GetInstance() {\n    if (!instance_) {\n      instance_ = new WindowClassRegistrar();\n    }\n    return instance_;\n  }\n\n  // Returns the name of the window class, registering the class if it hasn't\n  // previously been registered.\n  const wchar_t* GetWindowClass();\n\n  // Unregisters the window class. Should only be called if there are no\n  // instances of the window.\n  void UnregisterWindowClass();\n\n private:\n  WindowClassRegistrar() = default;\n\n  static WindowClassRegistrar* instance_;\n\n  bool class_registered_ = false;\n};\n\nWindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;\n\nconst wchar_t* WindowClassRegistrar::GetWindowClass() {\n  if (!class_registered_) {\n    WNDCLASS window_class{};\n    window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);\n    window_class.lpszClassName = kWindowClassName;\n    window_class.style = CS_HREDRAW | CS_VREDRAW;\n    window_class.cbClsExtra = 0;\n    window_class.cbWndExtra = 0;\n    window_class.hInstance = GetModuleHandle(nullptr);\n    window_class.hIcon =\n        LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));\n    window_class.hbrBackground = 0;\n    window_class.lpszMenuName = nullptr;\n    window_class.lpfnWndProc = Win32Window::WndProc;\n    RegisterClass(&window_class);\n    class_registered_ = true;\n  }\n  return kWindowClassName;\n}\n\nvoid WindowClassRegistrar::UnregisterWindowClass() {\n  UnregisterClass(kWindowClassName, nullptr);\n  class_registered_ = false;\n}\n\nWin32Window::Win32Window() {\n  ++g_active_window_count;\n}\n\nWin32Window::~Win32Window() {\n  --g_active_window_count;\n  Destroy();\n}\n\nbool Win32Window::CreateAndShow(const std::wstring& title,\n                                const Point& origin,\n                                const Size& size) {\n  Destroy();\n\n  const wchar_t* window_class =\n      WindowClassRegistrar::GetInstance()->GetWindowClass();\n\n  const POINT target_point = {static_cast<LONG>(origin.x),\n                              static_cast<LONG>(origin.y)};\n  HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);\n  UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);\n  double scale_factor = dpi / 96.0;\n\n  HWND window = CreateWindow(\n      window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,\n      Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),\n      Scale(size.width, scale_factor), Scale(size.height, scale_factor),\n      nullptr, nullptr, GetModuleHandle(nullptr), this);\n\n  if (!window) {\n    return false;\n  }\n\n  return OnCreate();\n}\n\n// static\nLRESULT CALLBACK Win32Window::WndProc(HWND const window,\n                                      UINT const message,\n                                      WPARAM const wparam,\n                                      LPARAM const lparam) noexcept {\n  if (message == WM_NCCREATE) {\n    auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);\n    SetWindowLongPtr(window, GWLP_USERDATA,\n                     reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));\n\n    auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);\n    EnableFullDpiSupportIfAvailable(window);\n    that->window_handle_ = window;\n  } else if (Win32Window* that = GetThisFromHandle(window)) {\n    return that->MessageHandler(window, message, wparam, lparam);\n  }\n\n  return DefWindowProc(window, message, wparam, lparam);\n}\n\nLRESULT\nWin32Window::MessageHandler(HWND hwnd,\n                            UINT const message,\n                            WPARAM const wparam,\n                            LPARAM const lparam) noexcept {\n  switch (message) {\n    case WM_DESTROY:\n      window_handle_ = nullptr;\n      Destroy();\n      if (quit_on_close_) {\n        PostQuitMessage(0);\n      }\n      return 0;\n\n    case WM_DPICHANGED: {\n      auto newRectSize = reinterpret_cast<RECT*>(lparam);\n      LONG newWidth = newRectSize->right - newRectSize->left;\n      LONG newHeight = newRectSize->bottom - newRectSize->top;\n\n      SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,\n                   newHeight, SWP_NOZORDER | SWP_NOACTIVATE);\n\n      return 0;\n    }\n    case WM_SIZE: {\n      RECT rect = GetClientArea();\n      if (child_content_ != nullptr) {\n        // Size and position the child window.\n        MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,\n                   rect.bottom - rect.top, TRUE);\n      }\n      return 0;\n    }\n\n    case WM_ACTIVATE:\n      if (child_content_ != nullptr) {\n        SetFocus(child_content_);\n      }\n      return 0;\n  }\n\n  return DefWindowProc(window_handle_, message, wparam, lparam);\n}\n\nvoid Win32Window::Destroy() {\n  OnDestroy();\n\n  if (window_handle_) {\n    DestroyWindow(window_handle_);\n    window_handle_ = nullptr;\n  }\n  if (g_active_window_count == 0) {\n    WindowClassRegistrar::GetInstance()->UnregisterWindowClass();\n  }\n}\n\nWin32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {\n  return reinterpret_cast<Win32Window*>(\n      GetWindowLongPtr(window, GWLP_USERDATA));\n}\n\nvoid Win32Window::SetChildContent(HWND content) {\n  child_content_ = content;\n  SetParent(content, window_handle_);\n  RECT frame = GetClientArea();\n\n  MoveWindow(content, frame.left, frame.top, frame.right - frame.left,\n             frame.bottom - frame.top, true);\n\n  SetFocus(child_content_);\n}\n\nRECT Win32Window::GetClientArea() {\n  RECT frame;\n  GetClientRect(window_handle_, &frame);\n  return frame;\n}\n\nHWND Win32Window::GetHandle() {\n  return window_handle_;\n}\n\nvoid Win32Window::SetQuitOnClose(bool quit_on_close) {\n  quit_on_close_ = quit_on_close;\n}\n\nbool Win32Window::OnCreate() {\n  // No-op; provided for subclasses.\n  return true;\n}\n\nvoid Win32Window::OnDestroy() {\n  // No-op; provided for subclasses.\n}\n"
  },
  {
    "path": "windows/runner/win32_window.h",
    "content": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <memory>\n#include <string>\n\n// A class abstraction for a high DPI-aware Win32 Window. Intended to be\n// inherited from by classes that wish to specialize with custom\n// rendering and input handling\nclass Win32Window {\n public:\n  struct Point {\n    unsigned int x;\n    unsigned int y;\n    Point(unsigned int x, unsigned int y) : x(x), y(y) {}\n  };\n\n  struct Size {\n    unsigned int width;\n    unsigned int height;\n    Size(unsigned int width, unsigned int height)\n        : width(width), height(height) {}\n  };\n\n  Win32Window();\n  virtual ~Win32Window();\n\n  // Creates and shows a win32 window with |title| and position and size using\n  // |origin| and |size|. New windows are created on the default monitor. Window\n  // sizes are specified to the OS in physical pixels, hence to ensure a\n  // consistent size to will treat the width height passed in to this function\n  // as logical pixels and scale to appropriate for the default monitor. Returns\n  // true if the window was created successfully.\n  bool CreateAndShow(const std::wstring& title,\n                     const Point& origin,\n                     const Size& size);\n\n  // Release OS resources associated with window.\n  void Destroy();\n\n  // Inserts |content| into the window tree.\n  void SetChildContent(HWND content);\n\n  // Returns the backing Window handle to enable clients to set icon and other\n  // window properties. Returns nullptr if the window has been destroyed.\n  HWND GetHandle();\n\n  // If true, closing this window will quit the application.\n  void SetQuitOnClose(bool quit_on_close);\n\n  // Return a RECT representing the bounds of the current client area.\n  RECT GetClientArea();\n\n protected:\n  // Processes and route salient window messages for mouse handling,\n  // size change and DPI. Delegates handling of these to member overloads that\n  // inheriting classes can handle.\n  virtual LRESULT MessageHandler(HWND window,\n                                 UINT const message,\n                                 WPARAM const wparam,\n                                 LPARAM const lparam) noexcept;\n\n  // Called when CreateAndShow is called, allowing subclass window-related\n  // setup. Subclasses should return false if setup fails.\n  virtual bool OnCreate();\n\n  // Called when Destroy is called.\n  virtual void OnDestroy();\n\n private:\n  friend class WindowClassRegistrar;\n\n  // OS callback called by message pump. Handles the WM_NCCREATE message which\n  // is passed when the non-client area is being created and enables automatic\n  // non-client DPI scaling so that the non-client area automatically\n  // responsponds to changes in DPI. All other messages are handled by\n  // MessageHandler.\n  static LRESULT CALLBACK WndProc(HWND const window,\n                                  UINT const message,\n                                  WPARAM const wparam,\n                                  LPARAM const lparam) noexcept;\n\n  // Retrieves a class instance pointer for |window|\n  static Win32Window* GetThisFromHandle(HWND const window) noexcept;\n\n  bool quit_on_close_ = false;\n\n  // window handle for top level window.\n  HWND window_handle_ = nullptr;\n\n  // window handle for hosted content.\n  HWND child_content_ = nullptr;\n};\n\n#endif  // RUNNER_WIN32_WINDOW_H_\n"
  }
]