Full Code of niuhuan/nhentai-cross for AI

master e253a48a0804 cached
173 files
378.6 KB
105.0k tokens
443 symbols
1 requests
Download .txt
Showing preview only (425K chars total). Download the full file or copy to clipboard to get everything.
Repository: niuhuan/nhentai-cross
Branch: master
Commit: e253a48a0804
Files: 173
Total size: 378.6 KB

Directory structure:
gitextract_vgtt9c51/

├── .github/
│   └── workflows/
│       └── Release.yml
├── .gitignore
├── .metadata
├── LICENSE
├── README-zh.md
├── README.md
├── analysis_options.yaml
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── build.gradle
│   │   └── src/
│   │       ├── debug/
│   │       │   └── AndroidManifest.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── niuhuan/
│   │       │   │       └── nhentai/
│   │       │   │           └── MainActivity.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── launch_background.xml
│   │       │       ├── drawable-v21/
│   │       │       │   └── launch_background.xml
│   │       │       ├── values/
│   │       │       │   └── styles.xml
│   │       │       └── values-night/
│   │       │           └── styles.xml
│   │       └── profile/
│   │           └── AndroidManifest.xml
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   └── settings.gradle
├── ci/
│   ├── cmd/
│   │   ├── check_asset/
│   │   │   └── main.go
│   │   ├── check_release/
│   │   │   └── main.go
│   │   └── upload_asset/
│   │       └── main.go
│   ├── commons/
│   │   └── types.go
│   ├── go.mod
│   ├── linux_font.yaml
│   ├── version.code.txt
│   └── version.info.txt
├── go/
│   ├── .gitignore
│   ├── cmd/
│   │   ├── init.go
│   │   ├── main.go
│   │   ├── options.go
│   │   └── plugin.go
│   ├── go.mod
│   ├── go.sum
│   ├── hover.yaml
│   ├── mobile/
│   │   ├── bind-android-debug.sh
│   │   ├── bind-android.sh
│   │   ├── bind-ios-debug.sh
│   │   ├── bind-ios.sh
│   │   ├── lib/
│   │   │   └── .keep
│   │   └── mobile.go
│   ├── nhentai/
│   │   ├── client.go
│   │   ├── common.go
│   │   ├── constant/
│   │   │   ├── constant.go
│   │   │   ├── os.go
│   │   │   └── time.go
│   │   ├── database/
│   │   │   ├── active/
│   │   │   │   └── active.go
│   │   │   ├── cache/
│   │   │   │   └── cache.go
│   │   │   └── properties/
│   │   │       └── properties.go
│   │   ├── decodes.go
│   │   ├── download.go
│   │   ├── locks.go
│   │   └── nhentai.go
│   └── packaging/
│       ├── darwin-bundle/
│       │   └── {{.applicationName}} {{.version}}.app/
│       │       └── Contents/
│       │           └── Info.plist.tmpl
│       └── linux-appimage/
│           ├── AppRun.tmpl
│           └── {{.packageName}}.desktop.tmpl
├── ios/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   └── Release.xcconfig
│   ├── Podfile
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── LaunchImage.imageset/
│   │   │       ├── Contents.json
│   │   │       └── README.md
│   │   ├── Base.lproj/
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   ├── contents.xcworkspacedata
│   │   │   └── xcshareddata/
│   │   │       ├── IDEWorkspaceChecks.plist
│   │   │       └── WorkspaceSettings.xcsettings
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   └── Runner.xcworkspace/
│       ├── contents.xcworkspacedata
│       └── xcshareddata/
│           ├── IDEWorkspaceChecks.plist
│           └── WorkspaceSettings.xcsettings
├── l10n.yaml
├── lib/
│   ├── basic/
│   │   ├── channels/
│   │   │   └── nhentai.dart
│   │   ├── common/
│   │   │   ├── common.dart
│   │   │   ├── cross.dart
│   │   │   └── error_types.dart
│   │   ├── configs/
│   │   │   ├── proxy.dart
│   │   │   ├── reader_direction.dart
│   │   │   ├── reader_type.dart
│   │   │   ├── themes.dart
│   │   │   └── version.dart
│   │   └── entities/
│   │       └── entities.dart
│   ├── l10n/
│   │   ├── app_en.arb
│   │   └── app_zh.arb
│   ├── main.dart
│   ├── main_desktop.dart
│   └── screens/
│       ├── comic_downloads_screen.dart
│       ├── comic_info_screen.dart
│       ├── comic_reader_screen.dart
│       ├── comic_search_screen.dart
│       ├── comics_screen.dart
│       ├── components/
│       │   ├── Badged.dart
│       │   ├── actions.dart
│       │   ├── content_builder.dart
│       │   ├── content_error.dart
│       │   ├── content_loading.dart
│       │   ├── images.dart
│       │   ├── mouse_and_touch_scroll_behavior.dart
│       │   └── pager.dart
│       ├── file_photo_view_screen.dart
│       ├── init_screen.dart
│       ├── settings_screen.dart
│       └── webview_screen.dart
├── linux/
│   ├── .gitignore
│   ├── CMakeLists.txt
│   ├── flutter/
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── macos/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── Flutter-Debug.xcconfig
│   │   ├── Flutter-Release.xcconfig
│   │   └── GeneratedPluginRegistrant.swift
│   ├── Podfile
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   └── AppIcon.appiconset/
│   │   │       └── Contents.json
│   │   ├── Base.lproj/
│   │   │   └── MainMenu.xib
│   │   ├── Configs/
│   │   │   ├── AppInfo.xcconfig
│   │   │   ├── Debug.xcconfig
│   │   │   ├── Release.xcconfig
│   │   │   └── Warnings.xcconfig
│   │   ├── DebugProfile.entitlements
│   │   ├── Info.plist
│   │   ├── MainFlutterWindow.swift
│   │   └── Release.entitlements
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   └── xcshareddata/
│   │   │       └── IDEWorkspaceChecks.plist
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   └── Runner.xcworkspace/
│       ├── contents.xcworkspacedata
│       └── xcshareddata/
│           └── IDEWorkspaceChecks.plist
├── pubspec.yaml
├── scripts/
│   ├── README.md
│   ├── bind-android-debug.sh
│   ├── bind-ios-arm64.sh
│   ├── bind-ios.sh
│   ├── build-apk-arm.sh
│   ├── build-apk-arm64.sh
│   ├── build-apk-x64.sh
│   ├── build-apk-x86.sh
│   ├── build-ipa.sh
│   ├── build-macos-dmg.sh
│   ├── sign-apk-github-actions.sh
│   ├── thin-payload.sh
│   └── version.sh
├── test/
│   └── widget_test.dart
└── windows/
    ├── .gitignore
    ├── CMakeLists.txt
    ├── flutter/
    │   ├── CMakeLists.txt
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    └── runner/
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── runner.exe.manifest
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/Release.yml
================================================
name: Release

on:
  workflow_dispatch:

env:
  go_version: '1.17'
  flutter_channel: 'stable'
  GH_TOKEN: ${{ secrets.GH_TOKEN }}

jobs:

  ci-pass:
    name: CI is green
    runs-on: ubuntu-latest
    needs:
      - check_release
      - build_release_assets
    steps:
      - run: exit 0

  check_release:
    name: Check release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          repository: ${{ github.event.inputs.repo }}
          ref: 'master'
      - name: Checkout submodules
        run: git submodule update --init --recursive
      - uses: actions/setup-go@v2
        with:
          go-version: ${{ env.go_version }}
      - name: Check release
        run: |
          cd ci
          go run ./cmd/check_release

  build_release_assets:
    name: Build release assets
    needs:
      - check_release
    strategy:
      fail-fast: false
      matrix:
        config:
          - target: linux
            host: ubuntu-latest
            flutter_version: '2.10.3'
          - target: windows
            host: windows-latest
            flutter_version: '2.10.3'
          - target: macos
            host: macos-12
            flutter_version: '2.10.3'
          - target: ios
            host: macos-12
            flutter_version: '3.3.4'
          - target: android-arm32
            host: ubuntu-latest
            flutter_version: '3.3.4'
          - target: android-arm64
            host: ubuntu-latest
            flutter_version: '3.3.4'
          - target: android-x86_64
            host: ubuntu-latest
            flutter_version: '3.3.4'

    runs-on: ${{ matrix.config.host }}

    env:
      TARGET: ${{ matrix.config.target }}
      flutter_version: ${{ matrix.config.flutter_version }}

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup golang
        uses: actions/setup-go@v2
        with:
          go-version: ${{ env.go_version }}

      - id: check_asset
        name: Check asset
        run: |
          cd ci
          go run ./cmd/check_asset

      - name: Setup flutter
        if: steps.check_asset.outputs.skip_build != 'true'
        uses: subosito/flutter-action@v2
        with:
          channel: ${{ env.flutter_channel }}
          flutter-version: ${{ env.flutter_version }}
          architecture: x64

      - name: Setup java (Android)
        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )
        uses: actions/setup-java@v3
        with:
          java-version: 8
          distribution: 'zulu'

      - name: Setup android tools (Android)
        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )
        uses: maxim-lobanov/setup-android-tools@v1
        with:
          packages: |
            platform-tools
            platforms;android-32
            build-tools;30.0.2
            ndk;22.1.7171670

      - name: Setup msys2 (Windows)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'
        uses: msys2/setup-msys2@v2
        with:
          install: gcc make

      - name: Install dependencies (Linux)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'
        env:
          ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
        run: |
          curl -JOL https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
          chmod a+x appimagetool-x86_64.AppImage
          mkdir -p ${GITHUB_WORKSPACE}/bin
          mv appimagetool-x86_64.AppImage ${GITHUB_WORKSPACE}/bin/appimagetool
          echo ::add-path::${GITHUB_WORKSPACE}/bin
          sudo apt-get update
          sudo apt-get install -y libgl1-mesa-dev xorg-dev

      - name: Install hover (desktop)
        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'linux' || matrix.config.target == 'windows' || matrix.config.target == 'macos')
        run: |
          go install github.com/go-flutter-desktop/hover@latest

      - name: Install go mobile (mobile)
        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' )
        run: |
          go install golang.org/x/mobile/cmd/gomobile@latest

      - name: Set-Version (All)
        if: steps.check_asset.outputs.skip_build != 'true'
        run: |
          cd ci
          cp version.code.txt ../lib/assets/version.txt

      - name: Upgrade deps version (Android)
        if: steps.check_asset.outputs.skip_build != 'true' && !startsWith(matrix.config.host, 'macos') && startsWith(matrix.config.flutter_version, '3')
        run: |
          sed -i "s/another_xlider: 1.0.1+2/another_xlider: ^1.0.1+2/g" pubspec.yaml
          sed -i "s/flutter_styled_toast: 2.0.0/flutter_styled_toast: ^2.0.0/g" pubspec.yaml
          sed -i "s/filesystem_picker: 2.0.0/filesystem_picker: ^2.0.0/g" pubspec.yaml

      - name: Build (windows)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'windows'
        run: |
          hover build windows
          cd go\build\outputs\windows-release
          DEL flutter_engine.pdb
          DEL flutter_engine.exp
          DEL flutter_engine.lib
          Compress-Archive * ../../../../build/build.zip

      - name: Build (macos)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'macos'
        run: |
          hover build darwin-dmg
          mv go/build/outputs/darwin-dmg-release/*.dmg build/build.dmg

      - name: Build (linux)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'linux'
        run: |
          curl -JOL https://github.com/junmer/source-han-serif-ttf/raw/master/SubsetTTF/CN/SourceHanSerifCN-Regular.ttf
          mkdir -p fonts
          mv SourceHanSerifCN-Regular.ttf fonts/Roboto.ttf
          cat ci/linux_font.yaml >> pubspec.yaml
          hover build linux-appimage
          mv go/build/outputs/linux-appimage-release/*.AppImage build/build.AppImage

      - name: Build (ios)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'ios'
        run: |
          /usr/libexec/PlistBuddy -c 'Add :application-identifier string niuhuan.nhentai' ios/Runner/Info.plist
          sh scripts/build-ipa.sh

      - name: Build (android-arm32)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm32'
        run: |
          sh scripts/build-apk-arm.sh

      - name: Build (android-arm64)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-arm64'
        run: |
          sh scripts/build-apk-arm64.sh

      - name: Build (android-x86_64)
        if: steps.check_asset.outputs.skip_build != 'true' && matrix.config.target == 'android-x86_64'
        run: |
          sh scripts/build-apk-x64.sh

      - name: Sign APK (Android)
        if: steps.check_asset.outputs.skip_build != 'true' && ( matrix.config.target == 'android-arm32' || matrix.config.target == 'android-arm64' || matrix.config.target == 'android-x86_64' )
        env:
          KEY_FILE_BASE64: ${{ secrets.KEY_FILE_BASE64 }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: |
          sh scripts/sign-apk-github-actions.sh

      - name: Upload Asset (All)
        if: steps.check_asset.outputs.skip_build != 'true'
        run: |
          cd ci
          go run ./cmd/upload_asset



================================================
FILE: .gitignore
================================================
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/

# Web related
lib/generated_plugin_registrant.dart

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

/go/mobile/lib/*.jar
/go/mobile/lib/*.aar
/go/mobile/lib/*.xcframework/

/lib/assets/version.txt
desiredFileName.txt


================================================
FILE: .metadata
================================================
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
  revision: 18116933e77adc82f80866c928266a5b4f1ed645
  channel: stable

project_type: app


================================================
FILE: LICENSE
================================================
Copyright (c) 2021-2022 niuhuan

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README-zh.md
================================================
# NHENTAI-CROSS

## [English](README.md) | 简体中文

[![license](https://img.shields.io/github/license/niuhuan/nhentai-cross)](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)
[![releases](https://img.shields.io/github/v/release/niuhuan/nhentai-cross)](https://github.com/niuhuan/nhentai-cross/releases)
[![downloads](https://img.shields.io/github/downloads/niuhuan/nhentai-cross/total)](https://github.com/niuhuan/nhentai-cross/releases)

一个美观且跨平台的*NHentai客户端*,支持桌面端与移动端(Mac/Windows/Linux/Android/IOS)。

内置DNS拦截器,可以让部分国家和地区免代理使用网络加速,请您在遵守当地法律的情况下使用。(需要在设置中开启)

如果您觉得此软件对您有帮助,可以star进行支持。同时欢迎您issue,一起让软件变得更好。

仓库地址 [https://github.com/niuhuan/nhentai-cross](https://github.com/niuhuan/nhentai-cross)

## 使用前必读

- 根据地区的不同DNS拦截器不一定有效, 请使用科学上网测试本软件的可用性

## 软件截图

#### 漫画列表

![](images/comic_list.png)

#### 漫画详情

![](images/comic_info.png)

#### 漫画阅读器

![](images/comic_reader.png)

## 技术架构

![](images/technologies.png)


================================================
FILE: README.md
================================================
# NHENTAI-CROSS

##  English | [简体中文](README-zh.md)

[![license](https://img.shields.io/github/license/niuhuan/nhentai-cross)](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)
[![releases](https://img.shields.io/github/v/release/niuhuan/nhentai-cross)](https://github.com/niuhuan/nhentai-cross/releases)
[![downloads](https://img.shields.io/github/downloads/niuhuan/nhentai-cross/total)](https://github.com/niuhuan/nhentai-cross/releases)

A beautiful and cross platform *NHentai Client*. Support desktop and mobile phone (Mac/Windows/Linux/Android/IOS).

## Must readme

The official website uses the page to prevent DDoS attacks.

## Captures

#### Comic list

![](images/comic_list.png)

#### Comic info

![](images/comic_info.png)

#### Comic reader

![](images/comic_reader.png)


## Technical architecture

![](images/technologies.png)


================================================
FILE: analysis_options.yaml
================================================
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
  # The lint rules applied to this project can be customized in the
  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
  # included above or to enable additional rules. A list of all available lints
  # and their documentation is published at
  # https://dart-lang.github.io/linter/lints/index.html.
  #
  # Instead of disabling a lint rule for the entire project in the
  # section below, it can also be suppressed for a single line of code
  # or a specific dart file by using the `// ignore: name_of_lint` and
  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
  # producing the lint.
  rules:
    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options


================================================
FILE: android/.gitignore
================================================
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks


================================================
FILE: android/app/build.gradle
================================================
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    compileSdkVersion 33

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "niuhuan.nhentai"
        minSdkVersion 19
        targetSdkVersion 31
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    buildTypes {
        release {
            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

flutter {
    source '../..'
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
    implementation fileTree(dir: "../../go/mobile/lib", include: ["*.jar", "*.aar"])
}


================================================
FILE: android/app/src/debug/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="niuhuan.nhentai">
    <!-- Flutter needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>


================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="niuhuan.nhentai">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
    </queries>

    <application
        android:label="nhentai"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <!-- Displays an Android View that continues showing the launch screen
                 Drawable until Flutter paints its first frame, then this splash
                 screen fades out. A splash screen is useful to avoid any visual
                 gap between the end of Android's launch screen and the painting of
                 Flutter's first frame. -->
            <meta-data
              android:name="io.flutter.embedding.android.SplashScreenDrawable"
              android:resource="@drawable/launch_background"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <!-- Accepts URIs that begin with https://YOUR_HOST -->
                <data
                        android:scheme="https"
                        android:host="nhentai.net"
                        android:pathPrefix="/g" />
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>


================================================
FILE: android/app/src/main/kotlin/niuhuan/nhentai/MainActivity.kt
================================================
package niuhuan.nhentai

import android.content.ContentValues
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.DisplayMetrics
import android.util.Log
import android.view.Display
import android.view.KeyEvent
import android.view.WindowManager
import androidx.annotation.NonNull
import androidx.annotation.RequiresApi
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.sync.Mutex
import mobile.Mobile
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.file.Files
import java.util.concurrent.Executors

class MainActivity: FlutterActivity() {

    // 为什么换成换成线程池而不继续使用携程 : 下载图片速度慢会占满携程造成拥堵, 接口无法请求
    private val pool = Executors.newCachedThreadPool { runnable ->
        Thread(runnable).also { it.isDaemon = true }
    }
    private val uiThreadHandler = Handler(Looper.getMainLooper())
    private val scope = CoroutineScope(newSingleThreadContext("worker-scope"))

    private val notImplementedToken = Any()
    private fun MethodChannel.Result.withCoroutine(exec: () -> Any?) {
        pool.submit {
            try {
                val data = exec()
                uiThreadHandler.post {
                    when (data) {
                        notImplementedToken -> {
                            notImplemented()
                        }
                        is Unit, null -> {
                            success(null)
                        }
                        else -> {
                            success(data)
                        }
                    }
                }
            } catch (e: Exception) {
                Log.e("Method", "Exception", e)
                uiThreadHandler.post {
                    error("", e.message, "")
                }
            }

        }
    }

    @RequiresApi(Build.VERSION_CODES.KITKAT)
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        Mobile.initApplication(androidDataLocal())
        // Method Channel
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "nhentai").setMethodCallHandler { call, result ->
            result.withCoroutine {
                when (call.method) {
                    "flatInvoke" -> {
                        Mobile.flatInvoke(
                                call.argument("method")!!,
                                call.argument("params")!!
                        )
                    }
                    "saveFileToImage" -> {
                        saveFileToImage(call.argument("path")!!)
                    }
                    "androidGetModes" -> {
                        modes()
                    }
                    "androidSetMode" -> {
                        setMode(call.argument("mode")!!)
                    }
                    "androidGetVersion" -> Build.VERSION.SDK_INT
                    // 现在的文件储存路径, 默认路径返回空字符串 ""
                    "dataLocal" -> androidDataLocal()
                    // 迁移到那个地方, 如果是空字符串则迁移会默认位置
                    "migrate" -> androidMigrate(call.argument("path")!!)
                    // 获取可以迁移数据地址
                    "androidGetExtendDirs" -> androidGetExtendDirs()
                    "androidSecureFlag" -> androidSecureFlag(call.argument("flag")!!)
                    "convertToPNG" -> convertToPNG(call.argument("path")!!)
                    else -> {
                        notImplementedToken
                    }
                }
            }
        }

        //
        val eventMutex = Mutex()
        var eventSink: EventChannel.EventSink? = null
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, "flatEvent")
                .setStreamHandler(object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        events?.let { events ->
                            scope.launch {
                                eventMutex.lock()
                                eventSink = events
                                eventMutex.unlock()
                            }
                        }
                    }

                    override fun onCancel(arguments: Any?) {
                        scope.launch {
                            eventMutex.lock()
                            eventSink = null
                            eventMutex.unlock()
                        }
                    }
                })
        Mobile.eventNotify { message ->
            scope.launch {
                eventMutex.lock()
                try {
                    eventSink?.let {
                        uiThreadHandler.post {
                            it.success(message)
                        }
                    }
                } finally {
                    eventMutex.unlock()
                }
            }
        }

        //
        EventChannel(flutterEngine.dartExecutor.binaryMessenger, "volume_button")
                .setStreamHandler(volumeStreamHandler)

    }

    // save_image
    private fun saveFileToImage(path: String) {
        BitmapFactory.decodeFile(path)?.let { bitmap ->
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, System.currentTimeMillis().toString())
                put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one
                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
                    put(MediaStore.MediaColumns.IS_PENDING, 1)
                }
            }
            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)?.let { uri ->
                contentResolver.openOutputStream(uri)?.use { fos ->
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { //this one
                    contentValues.clear()
                    contentValues.put(MediaStore.Video.Media.IS_PENDING, 0)
                    contentResolver.update(uri, contentValues, null, null)
                }
            }
        }
    }

    private fun androidDataLocal(): String {
        val localFile = File(context!!.filesDir.absolutePath, "data.local")
        if (localFile.exists()) {
            val path = String(FileInputStream(localFile).use { it.readBytes() })
            if (File(path).isDirectory) {
                return path
            }
        }
        return context!!.filesDir.absolutePath
    }

    private fun androidGetExtendDirs(): String {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            val result = context!!.getExternalFilesDirs("")?.toMutableList()?.also {
                it.add(context!!.filesDir.absoluteFile)
            }?.joinToString("|")
            if (result != null) {
                return result
            }
        }
        throw Exception("System version too low")
    }

    private fun androidMigrate(path: String) {
        val current = androidDataLocal()
        if (current == path) {
            return
        }
        // 删除位置配置文件
        if (File(current, "data.local").exists()) {
            File(current, "data.local").delete()
        }
        // 目标位置文件夹不存在就创建,存在则清理
        val target = File(path)
        if (!target.exists()) {
            target.mkdirs()
        }
        target.listFiles().forEach { delete(it) }
        // 移动所有文件夹

        File(current).listFiles().forEach {
            move(it, File(target, it.name))
        }
        val localFile = File(context!!.filesDir.absolutePath, "data.local")
        if (path == context!!.filesDir.absolutePath) {
            localFile.delete()
        } else {
            FileOutputStream(localFile).use { it.write(path.toByteArray()) }
        }
    }

    private fun delete(f: File) {
        f.delete()
    }

    private fun move(f: File, t: File) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            if (f.isDirectory) {
                Files.createDirectories(t.toPath())
                f.listFiles().forEach { move(it, File(t, it.name)) }
                Files.delete(f.toPath())
            } else {
                Files.move(f.toPath(), t.toPath())
            }
        } else {
            if (f.isDirectory) {
                t.mkdirs()
                f.listFiles().forEach { move(it, File(t, it.name)) }
                f.delete()
            } else {
                FileOutputStream(t).use { o ->
                    FileInputStream(f).use { i ->
                        o.write(i.readBytes())
                    }
                }
                f.delete()
            }
        }
    }

    // fps mods
    private fun mixDisplay(): Display? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            display?.let {
                return it
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            windowManager.defaultDisplay?.let {
                return it
            }
        }
        return null
    }

    private fun modes(): List<String> {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mixDisplay()?.let { display ->
                return display.supportedModes.map { mode ->
                    mode.toString()
                }
            }
        }
        return ArrayList()
    }

    private fun setMode(string: String) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mixDisplay()?.let { display ->
                if (string == "") {
                    uiThreadHandler.post {
                        window.attributes = window.attributes.also { attr ->
                            attr.preferredDisplayModeId = 0
                        }
                    }
                    return
                }
                return display.supportedModes.forEach { mode ->
                    if (mode.toString() == string) {
                        uiThreadHandler.post {
                            window.attributes = window.attributes.also { attr ->
                                attr.preferredDisplayModeId = mode.modeId
                            }
                        }
                        return
                    }
                }
            }
        }
    }

// volume_buttons

    private var volumeEvents: EventChannel.EventSink? = null

    private val volumeStreamHandler = object : EventChannel.StreamHandler {

        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
            volumeEvents = events
        }

        override fun onCancel(arguments: Any?) {
            volumeEvents = null
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        volumeEvents?.let {
            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                uiThreadHandler.post {
                    it.success("DOWN")
                }
                return true
            }
            if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                uiThreadHandler.post {
                    it.success("UP")
                }
                return true
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    private fun androidSecureFlag(flag: Boolean) {
        uiThreadHandler.post {
            if (flag) {
                window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
            } else {
                window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
            }
        }
    }

    private fun convertToPNG(path: String): ByteArray {
        BitmapFactory.decodeFile(path)?.let { bitmap ->
            val maxWidth =
                    when {
                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> windowManager.currentWindowMetrics.bounds.width()
                        Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 -> {
                            val displayMetrics = DisplayMetrics()
                            windowManager.defaultDisplay.getRealMetrics(displayMetrics)
                            displayMetrics.widthPixels
                        }
                        else -> throw Exception("not support")
                    }
            if (bitmap.width > maxWidth) {
                val newHeight = maxWidth * bitmap.height / bitmap.width
                val newImage = Bitmap.createScaledBitmap(bitmap, maxWidth, newHeight, true)
                return compressBitMap(newImage)
            }
            return compressBitMap(bitmap)
        }
        throw Exception("error pic")
    }

    private fun compressBitMap(bitmap: Bitmap): ByteArray {
        val bos = ByteArrayOutputStream()
        bos.use { bos ->
            Log.d("BITMAP", bitmap.width.toString())
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
        }
        return bos.toByteArray()
    }

}


================================================
FILE: android/app/src/main/res/drawable/launch_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@android:color/white" />

    <!-- You can insert your own image assets here -->
    <!-- <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/launch_image" />
    </item> -->
</layer-list>


================================================
FILE: android/app/src/main/res/drawable-v21/launch_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="?android:colorBackground" />

    <!-- You can insert your own image assets here -->
    <!-- <item>
        <bitmap
            android:gravity="center"
            android:src="@mipmap/launch_image" />
    </item> -->
</layer-list>


================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
        <!-- Show a splash screen on the activity. Automatically removed when
             Flutter draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>
    <!-- Theme applied to the Android Window as soon as the process has started.
         This theme determines the color of the Android Window while your
         Flutter UI initializes, as well as behind your Flutter UI while its
         running.
         
         This Theme is only used starting with V2 of Flutter's Android embedding. -->
    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
        <item name="android:windowBackground">?android:colorBackground</item>
        <item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
    </style>
</resources>


================================================
FILE: android/app/src/main/res/values-night/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <!-- Show a splash screen on the activity. Automatically removed when
             Flutter draws its first frame -->
        <item name="android:windowBackground">@drawable/launch_background</item>
    </style>
    <!-- Theme applied to the Android Window as soon as the process has started.
         This theme determines the color of the Android Window while your
         Flutter UI initializes, as well as behind your Flutter UI while its
         running.
         
         This Theme is only used starting with V2 of Flutter's Android embedding. -->
    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
        <item name="android:windowBackground">?android:colorBackground</item>
        <item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
    </style>
</resources>


================================================
FILE: android/app/src/profile/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="niuhuan.nhentai">
    <!-- Flutter needs it to communicate with the running application
         to allow setting breakpoints, to provide hot reload, etc.
    -->
    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>


================================================
FILE: android/build.gradle
================================================
buildscript {
    ext.kotlin_version = '1.6.10'
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip


================================================
FILE: android/gradle.properties
================================================
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true


================================================
FILE: android/settings.gradle
================================================
include ':app'

def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()

assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"


================================================
FILE: ci/cmd/check_asset/main.go
================================================
package main

import (
        "ci/commons"
        "encoding/json"
        "fmt"
        "io/ioutil"
        "net/http"
        "os"
        "strings"
)

const owner = "niuhuan"
const repo = "nhentai-cross"
const ua = "niuhuan nhentai-cross ci"

func main() {
        // get ghToken
        ghToken := os.Getenv("GH_TOKEN")
        if ghToken == "" {
                println("Env ${GH_TOKEN} is not set")
                os.Exit(1)
        }
        // get version
        var version commons.Version
        codeFile, err := ioutil.ReadFile("version.code.txt")
        if err != nil {
                panic(err)
        }
        version.Code = strings.TrimSpace(string(codeFile))
        infoFile, err := ioutil.ReadFile("version.info.txt")
        if err != nil {
                panic(err)
        }
        version.Info = strings.TrimSpace(string(infoFile))
        // get target
        target := os.Getenv("TARGET")
        if target == "" {
                println("Env ${TARGET} is not set")
                os.Exit(1)
        }
        //
        var releaseFileName string
        switch target {
        case "macos":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-macos-intel.dmg", version.Code)
        case "ios":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-ios-nosign.ipa", version.Code)
        case "windows":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-windows-x86_64.zip", version.Code)
        case "linux":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-linux-x86_64.AppImage", version.Code)
        case "android-arm32":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-arm32.apk", version.Code)
        case "android-arm64":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-arm64.apk", version.Code)
        case "android-x86_64":
                releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-x86_64.apk", version.Code)
        }
        // get version
        getReleaseRequest, err := http.NewRequest(
                "GET",
                fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/tags/%v", owner, repo, version.Code),
                nil,
        )
        if err != nil {
                panic(err)
        }
        getReleaseRequest.Header.Set("User-Agent", ua)
        getReleaseRequest.Header.Set("Authorization", "token "+ghToken)
        getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)
        if err != nil {
                panic(err)
        }
        defer getReleaseResponse.Body.Close()
        if getReleaseResponse.StatusCode == 404 {
                panic("NOT FOUND RELEASE")
        }
        buff, err := ioutil.ReadAll(getReleaseResponse.Body)
        if err != nil {
                panic(err)
        }
        var release commons.Release
        err = json.Unmarshal(buff, &release)
        if err != nil {
                println(string(buff))
                panic(err)
        }
        for _, asset := range release.Assets {
                if asset.Name == releaseFileName {
                        println("::set-output name=skip_build::true")
                        os.Exit(0)
                }
        }
        print("::set-output name=skip_build::false")
}


================================================
FILE: ci/cmd/check_release/main.go
================================================
package main

import (
	"bytes"
	"ci/commons"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"strings"
)

const owner = "niuhuan"
const repo = "nhentai-cross"
const ua = "niuhuan nhentai-cross ci"
const mainBranch = "master"

func main() {
	// get ghToken
	ghToken := os.Getenv("GH_TOKEN")
	if ghToken == "" {
		println("Env ${GH_TOKEN} is not set")
		os.Exit(1)
	}
	// get version
	var version commons.Version
	codeFile, err := ioutil.ReadFile("version.code.txt")
	if err != nil {
		panic(err)
	}
	version.Code = strings.TrimSpace(string(codeFile))
	infoFile, err := ioutil.ReadFile("version.info.txt")
	if err != nil {
		panic(err)
	}
	version.Info = strings.TrimSpace(string(infoFile))
	// get version
	getReleaseRequest, err := http.NewRequest(
		"GET",
		fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/tags/%v", owner, repo, version.Code),
		nil,
	)
	if err != nil {
		panic(nil)
	}
	getReleaseRequest.Header.Set("User-Agent", ua)
	getReleaseRequest.Header.Set("Authorization", "token "+ghToken)
	getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)
	if err != nil {
		panic(nil)
	}
	defer getReleaseResponse.Body.Close()
	if getReleaseResponse.StatusCode == 404 {
		url := fmt.Sprintf("https://api.github.com/repos/%v/%v/releases", owner, repo)
		body := map[string]interface{}{
			"tag_name":         version.Code,
			"target_commitish": mainBranch,
			"name":             version.Code,
			"body":             version.Info,
		}
		var buff []byte
		buff, err = json.Marshal(&body)
		if err != nil {
			panic(err)
		}
		var createReleaseRequest *http.Request
		createReleaseRequest, err = http.NewRequest("POST", url, bytes.NewBuffer(buff))
		if err != nil {
			panic(nil)
		}
		createReleaseRequest.Header.Set("User-Agent", ua)
		createReleaseRequest.Header.Set("Authorization", "token "+ghToken)
		var createReleaseResponse *http.Response
		createReleaseResponse, err = http.DefaultClient.Do(createReleaseRequest)
		if err != nil {
			panic(nil)
		}
		defer createReleaseResponse.Body.Close()
		if createReleaseResponse.StatusCode != 201 {
			buff, err = ioutil.ReadAll(createReleaseResponse.Body)
			if err != nil {
				panic(err)
			}
			println(string(buff))
			panic("NOT 201")
		}
	}
}


================================================
FILE: ci/cmd/upload_asset/main.go
================================================
package main

import (
	"ci/commons"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"strings"
)

const owner = "niuhuan"
const repo = "nhentai-cross"
const ua = "niuhuan nhentai-cross ci"

func main() {
	// get ghToken
	ghToken := os.Getenv("GH_TOKEN")
	if ghToken == "" {
		println("Env ${GH_TOKEN} is not set")
		os.Exit(1)
	}
	// get version
	var version commons.Version
	codeFile, err := ioutil.ReadFile("version.code.txt")
	if err != nil {
		panic(err)
	}
	version.Code = strings.TrimSpace(string(codeFile))
	infoFile, err := ioutil.ReadFile("version.info.txt")
	if err != nil {
		panic(err)
	}
	version.Info = strings.TrimSpace(string(infoFile))
	// get target
	target := os.Getenv("TARGET")
	if target == "" {
		println("Env ${TARGET} is not set")
		os.Exit(1)
	}
	//
	var releaseFilePath string
	var releaseFileName string
	var contentType string
	var contentLength int64
	switch target {
	case "macos":
		releaseFilePath = "build/build.dmg"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-macos-intel.dmg", version.Code)
		contentType = "application/octet-stream"
	case "ios":
		releaseFilePath = "build/nosign.ipa"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-ios-nosign.ipa", version.Code)
		contentType = "application/octet-stream"
	case "windows":
		releaseFilePath = "build/build.zip"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-windows-x86_64.zip", version.Code)
		contentType = "application/octet-stream"
	case "linux":
		releaseFilePath = "build/build.AppImage"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-linux-x86_64.AppImage", version.Code)
		contentType = "application/octet-stream"
	case "android-arm32":
		releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-arm32.apk", version.Code)
		contentType = "application/octet-stream"
	case "android-arm64":
		releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-arm64.apk", version.Code)
		contentType = "application/octet-stream"
	case "android-x86_64":
		releaseFilePath = "build/app/outputs/flutter-apk/app-release.apk"
		releaseFileName = fmt.Sprintf("nhentai-cross-%v-android-x86_64.apk", version.Code)
		contentType = "application/octet-stream"
	}
	releaseFilePath = path.Join("..", releaseFilePath)
	info, err := os.Stat(releaseFilePath)
	if err != nil {
		panic(err)
	}
	contentLength = info.Size()
	// get version
	getReleaseRequest, err := http.NewRequest(
		"GET",
		fmt.Sprintf("https://api.github.com/repos/%v/%v/releases/tags/%v", owner, repo, version.Code),
		nil,
	)
	if err != nil {
		panic(err)
	}
	getReleaseRequest.Header.Set("User-Agent", ua)
	getReleaseRequest.Header.Set("Authorization", "token "+ghToken)
	getReleaseResponse, err := http.DefaultClient.Do(getReleaseRequest)
	if err != nil {
		panic(err)
	}
	defer getReleaseResponse.Body.Close()
	if getReleaseResponse.StatusCode == 404 {
		panic("NOT FOUND RELEASE")
	}
	buff, err := ioutil.ReadAll(getReleaseResponse.Body)
	if err != nil {
		panic(err)
	}
	var release commons.Release
	err = json.Unmarshal(buff, &release)
	if err != nil {
		println(string(buff))
		panic(err)
	}
	file, err := os.Open(releaseFilePath)
	if err != nil {
		panic(err)
	}
	defer file.Close()
	uploadUrl := fmt.Sprintf("https://uploads.github.com/repos/%v/%v/releases/%v/assets?name=%v", owner, repo, release.Id, releaseFileName)
	uploadRequest, err := http.NewRequest("POST", uploadUrl, file)
	if err != nil {
		panic(err)
	}
	uploadRequest.Header.Set("User-Agent", ua)
	uploadRequest.Header.Set("Authorization", "token "+ghToken)
	uploadRequest.Header.Set("Content-Type", contentType)
	uploadRequest.ContentLength = contentLength
	uploadResponse, err := http.DefaultClient.Do(uploadRequest)
	if err != nil {
		panic(err)
	}
	if uploadResponse.StatusCode != 201 {
		buff, err = ioutil.ReadAll(uploadResponse.Body)
		if err != nil {
			panic(err)
		}
		println(string(buff))
		panic("NOT 201")
	}
}


================================================
FILE: ci/commons/types.go
================================================
package commons

import "time"

type Version struct {
	Code string `json:"code"`
	Info string `json:"info"`
}

type Release struct {
	Url             string    `json:"url"`
	HtmlUrl         string    `json:"html_url"`
	AssetsUrl       string    `json:"assets_url"`
	UploadUrl       string    `json:"upload_url"`
	TarballUrl      string    `json:"tarball_url"`
	ZipballUrl      string    `json:"zipball_url"`
	DiscussionUrl   string    `json:"discussion_url"`
	Id              int       `json:"id"`
	NodeId          string    `json:"node_id"`
	TagName         string    `json:"tag_name"`
	TargetCommitish string    `json:"target_commitish"`
	Name            string    `json:"name"`
	Body            string    `json:"body"`
	Draft           bool      `json:"draft"`
	Prerelease      bool      `json:"prerelease"`
	CreatedAt       time.Time `json:"created_at"`
	PublishedAt     time.Time `json:"published_at"`
	Author          struct {
		Login             string `json:"login"`
		Id                int    `json:"id"`
		NodeId            string `json:"node_id"`
		AvatarUrl         string `json:"avatar_url"`
		GravatarId        string `json:"gravatar_id"`
		Url               string `json:"url"`
		HtmlUrl           string `json:"html_url"`
		FollowersUrl      string `json:"followers_url"`
		FollowingUrl      string `json:"following_url"`
		GistsUrl          string `json:"gists_url"`
		StarredUrl        string `json:"starred_url"`
		SubscriptionsUrl  string `json:"subscriptions_url"`
		OrganizationsUrl  string `json:"organizations_url"`
		ReposUrl          string `json:"repos_url"`
		EventsUrl         string `json:"events_url"`
		ReceivedEventsUrl string `json:"received_events_url"`
		Type              string `json:"type"`
		SiteAdmin         bool   `json:"site_admin"`
	} `json:"author"`
	Assets []struct {
		Url                string    `json:"url"`
		BrowserDownloadUrl string    `json:"browser_download_url"`
		Id                 int       `json:"id"`
		NodeId             string    `json:"node_id"`
		Name               string    `json:"name"`
		Label              string    `json:"label"`
		State              string    `json:"state"`
		ContentType        string    `json:"content_type"`
		Size               int       `json:"size"`
		DownloadCount      int       `json:"download_count"`
		CreatedAt          time.Time `json:"created_at"`
		UpdatedAt          time.Time `json:"updated_at"`
		Uploader           struct {
			Login             string `json:"login"`
			Id                int    `json:"id"`
			NodeId            string `json:"node_id"`
			AvatarUrl         string `json:"avatar_url"`
			GravatarId        string `json:"gravatar_id"`
			Url               string `json:"url"`
			HtmlUrl           string `json:"html_url"`
			FollowersUrl      string `json:"followers_url"`
			FollowingUrl      string `json:"following_url"`
			GistsUrl          string `json:"gists_url"`
			StarredUrl        string `json:"starred_url"`
			SubscriptionsUrl  string `json:"subscriptions_url"`
			OrganizationsUrl  string `json:"organizations_url"`
			ReposUrl          string `json:"repos_url"`
			EventsUrl         string `json:"events_url"`
			ReceivedEventsUrl string `json:"received_events_url"`
			Type              string `json:"type"`
			SiteAdmin         bool   `json:"site_admin"`
		} `json:"uploader"`
	} `json:"assets"`
}

================================================
FILE: ci/go.mod
================================================
module "ci"

================================================
FILE: ci/linux_font.yaml
================================================

  fonts:
  - family: Roboto
    fonts:
      - asset: fonts/Roboto.ttf



================================================
FILE: ci/version.code.txt
================================================
v0.0.9

================================================
FILE: ci/version.info.txt
================================================
- Cross DDoS (only phone)
- 通过ddos (仅手机端)


================================================
FILE: go/.gitignore
================================================
build
.last_goflutter_check
.last_go-flutter_check


================================================
FILE: go/cmd/init.go
================================================
package main

import (
	"errors"
	"nhentai/nhentai"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"
)

func init() {
	nhentai.InitNHentai(documentPath())
}

func documentPath() string {
	applicationDir, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}
	switch runtime.GOOS {
	case "windows":
		// applicationDir = winDocumentPath.Join(applicationDir, "AppData", "Roaming", "nhentai")
		file, err := exec.LookPath(os.Args[0])
		if err != nil {
			panic(err)
		}
		winDocumentPath, err := filepath.Abs(file)
		if err != nil {
			panic(err)
		}
		i := strings.LastIndex(winDocumentPath, "/")
		if i < 0 {
			i = strings.LastIndex(winDocumentPath, "\\")
		}
		if i < 0 {
			panic(errors.New(" can't find \"/\" or \"\\\""))
		}
		applicationDir = path.Join(winDocumentPath[0:i+1], "data")
	case "darwin":
		applicationDir = path.Join(applicationDir, "Library", "Application Support", "nhentai")
	case "linux":
		applicationDir = path.Join(applicationDir, ".nhentai")
	default:
		panic(errors.New("not supported system"))
	}
	if _, err = os.Stat(applicationDir); err != nil {
		if os.IsNotExist(err) {
			err = os.MkdirAll(applicationDir, os.FileMode(0700))
			if err != nil {
				panic(err)
			}
		} else {
			panic(err)
		}
	}
	return applicationDir
}


================================================
FILE: go/cmd/main.go
================================================
package main

import (
	"fmt"
	"github.com/go-flutter-desktop/go-flutter"
	"github.com/pkg/errors"
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"nhentai/nhentai/database/properties"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

// vmArguments may be set by hover at compile-time
var vmArguments string

func main() {
	// DO NOT EDIT, add options in options.go
	mainOptions := []flutter.Option{
		flutter.OptionVMArguments(strings.Split(vmArguments, ";")),
		flutter.WindowIcon(iconProvider),
	}
	// 窗口初始化大小的处理
	widthStr, _ := properties.LoadProperty("window_width", "600")
	heightStr, _ := properties.LoadProperty("window_height", "900")
	width, _ := strconv.Atoi(widthStr)
	height, _ := strconv.Atoi(heightStr)
	if width <= 0 {
		width = 600
	}
	if height <= 0 {
		height = 900
	}
	var runOptions []flutter.Option
	runOptions = append(runOptions, flutter.WindowInitialDimensions(width, height))
	fullScreen, _ := properties.LoadBoolProperty("full_screen", false)
	if fullScreen {
		runOptions = append(runOptions, flutter.WindowMode(flutter.WindowModeMaximize))
	}
	// ------
	err := flutter.Run(append(append(runOptions, options...), mainOptions...)...)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func iconProvider() ([]image.Image, error) {
	execPath, err := os.Executable()
	if err != nil {
		return nil, errors.Wrap(err, "failed to resolve executable path")
	}
	execPath, err = filepath.EvalSymlinks(execPath)
	if err != nil {
		return nil, errors.Wrap(err, "failed to eval symlinks for executable path")
	}
	imgFile, err := os.Open(filepath.Join(filepath.Dir(execPath), "assets", "icon.png"))
	if err != nil {
		return nil, errors.Wrap(err, "failed to open assets/icon.png")
	}
	img, _, err := image.Decode(imgFile)
	if err != nil {
		return nil, errors.Wrap(err, "failed to decode image")
	}
	return []image.Image{img}, nil
}


================================================
FILE: go/cmd/options.go
================================================
package main

import (
	"github.com/go-flutter-desktop/go-flutter"
	"github.com/go-flutter-desktop/plugins/url_launcher"
	"github.com/miguelpruivo/flutter_file_picker/go"
)

var options = []flutter.Option{
	flutter.AddPlugin(&file_picker.FilePickerPlugin{}),
	flutter.AddPlugin(&url_launcher.UrlLauncherPlugin{}),
	flutter.AddPlugin(&Plugin{}),
}


================================================
FILE: go/cmd/plugin.go
================================================
package main

import (
	"errors"
	"github.com/go-flutter-desktop/go-flutter/plugin"
	"github.com/go-gl/glfw/v3.3/glfw"
	"nhentai/nhentai"
	"nhentai/nhentai/database/properties"
	"strconv"
)

type Plugin struct {
}

func (p *Plugin) InitPlugin(messenger plugin.BinaryMessenger) error {
	channel := plugin.NewMethodChannel(messenger, "nhentai", plugin.StandardMethodCodec{})
	channel.HandleFunc("flatInvoke", func(arguments interface{}) (interface{}, error) {
		if argumentsMap, ok := arguments.(map[interface{}]interface{}); ok {
			if method, ok := argumentsMap["method"].(string); ok {
				if params, ok := argumentsMap["params"].(string); ok {
					return nhentai.FlatInvoke(method, params)
				}
			}
		}
		return "", errors.New("method not found (nhentai channel)")
	})
	return nil
}

func (p *Plugin) InitPluginGLFW(window *glfw.Window) error {
	window.SetSizeCallback(func(w *glfw.Window, width int, height int) {
		go func() {
			properties.SaveProperty("window_width", strconv.Itoa(width))
			properties.SaveProperty("window_height", strconv.Itoa(height))
		}()
	})
	window.SetMaximizeCallback(func(w *glfw.Window, iconified bool) {
		go func() {
			properties.SaveProperty("full_screen", strconv.FormatBool(iconified))
		}()
	})
	return nil
}


================================================
FILE: go/go.mod
================================================
module nhentai

go 1.17

require (
	github.com/go-flutter-desktop/go-flutter v0.44.0
	github.com/go-flutter-desktop/plugins/url_launcher v0.1.3
	github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958
	github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35
	github.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d
	github.com/pkg/errors v0.9.1
	golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
	gorm.io/driver/sqlite v1.3.1
	gorm.io/gorm v1.23.4
)

require (
	github.com/PuerkitoBio/goquery v1.8.0 // indirect
	github.com/Xuanwo/go-locale v1.1.0 // indirect
	github.com/andybalholm/cascadia v1.3.1 // indirect
	github.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7 // indirect
	github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 // indirect
	github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f // indirect
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/jinzhu/now v1.1.4 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/mattn/go-sqlite3 v1.14.9 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	golang.org/x/mobile v0.0.0-20221012134814-c746ac228303 // indirect
	golang.org/x/mod v0.4.2 // indirect
	golang.org/x/net v0.0.0-20220615171555-694bf12d69de // indirect
	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
	golang.org/x/text v0.3.7 // indirect
	golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)


================================================
FILE: go/go.sum
================================================
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg=
github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7 h1:qA8Mdjwrlv/r/aMqArqO0IMHUiy6ApdW4+8DtKr7PvA=
github.com/gen2brain/dlgs v0.0.0-20190708095831-3854608588f7/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU=
github.com/go-flutter-desktop/go-flutter v0.30.0/go.mod h1:NCryd/AqiRbYSd8pMzQldYkgH1tZIFGt2ToUghZcWGA=
github.com/go-flutter-desktop/go-flutter v0.44.0 h1:0ab9WP2qxZCy2egXyjD6T7cBkORz2f9/LkETJ0F2PN8=
github.com/go-flutter-desktop/go-flutter v0.44.0/go.mod h1:Q1oxIBX2aX6rC+bWhN9aC4C05uJLXfnSNN2aNKNyBUM=
github.com/go-flutter-desktop/plugins/url_launcher v0.1.3 h1:28/xUmm3ZMqLtnlJF7zB9ZdJmF74h+cYKt65evU0pnQ=
github.com/go-flutter-desktop/plugins/url_launcher v0.1.3/go.mod h1:R8dUEKFt7nuCgeJ9UtZ3Apg1Ib9i20GDsVJl/7uma5E=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784 h1:1Zi56D0LNfvkzM+BdoxKryvUEdyWO7LP8oRT+oSYJW0=
github.com/go-gl/gl v0.0.0-20211025173605-bda47ffaa784/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211024062804-40e447a793be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f h1:TyqzGm2z1h3AGhjOoRYyeLcW4WlW81MDQkWa+rx/000=
github.com/gopherjs/gopherjs v0.0.0-20190915194858-d3ddacdb130f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35 h1:LoiQwzAk4gm6SSechIAywbcaBhkc/NABfLJ94JxOU8Y=
github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20220310123445-443808b9cd35/go.mod h1:csuW+TFyYKtiUwNvcvhcpyX4quPI7Pvv0SUogdqCW4I=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d h1:+V2zonBI5M7UmqbBk1szY6RaGx5RMx71hO0TrQ3WLZ0=
github.com/niuhuan/nhentai-go v0.0.0-20220617164634-57974195259d/go.mod h1:4Vj8GRJFi4AAQgz3dRefIcPezroV0/a5Bu2oTgashQc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=
github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg=
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
golang.org/x/mobile v0.0.0-20221012134814-c746ac228303 h1:K4fp1rDuJBz0FCPAWzIJwnzwNEM7S6yobdZzMrZ/Zws=
golang.org/x/mobile v0.0.0-20221012134814-c746ac228303/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220615171555-694bf12d69de h1:ogOG2+P6LjO2j55AkRScrkB2BFpd+Z8TY2wcM0Z3MGo=
golang.org/x/net v0.0.0-20220615171555-694bf12d69de/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 h1:YuekqPskqwCCPM79F1X5Dhv4ezTCj+Ki1oNwiafxkA0=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=
gorm.io/driver/sqlite v1.3.1/go.mod h1:wJx0hJspfycZ6myN38x1O/AqLtNS6c5o9TndewFbELg=
gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=


================================================
FILE: go/hover.yaml
================================================
#application-name: "nhentai" # Uncomment to modify this value.
#executable-name: "nhentai" # Uncomment to modify this value. Only lowercase a-z, numbers, underscores and no spaces
#package-name: "nhentai" # Uncomment to modify this value. Only lowercase a-z, numbers and no underscores or spaces
organization-name: "com.nhentai"
license: "" # MANDATORY: Fill in your SPDX license name: https://spdx.org/licenses
target: lib/main_desktop.dart
# opengl: "none" # Uncomment this line if you have trouble with your OpenGL driver (https://github.com/go-flutter-desktop/go-flutter/issues/272)
docker: false
engine-version: "" # change to a engine version commit


================================================
FILE: go/mobile/bind-android-debug.sh
================================================
gomobile bind -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./


================================================
FILE: go/mobile/bind-android.sh
================================================
gomobile bind -target=android/arm -o lib/Mobile.aar ./


================================================
FILE: go/mobile/bind-ios-debug.sh
================================================
gomobile bind -target=ios -o lib/Mobile.xcframework ./


================================================
FILE: go/mobile/bind-ios.sh
================================================
gomobile bind -target=ios -o lib/Mobile.xcframework ./


================================================
FILE: go/mobile/lib/.keep
================================================


================================================
FILE: go/mobile/mobile.go
================================================
package mobile

import (
	"errors"
	"nhentai/nhentai"
	"nhentai/nhentai/constant"
	"os"
	"path"
)

func Migration(source, target string) {
	constant.ObtainDir(source)
	constant.ObtainDir(target)
	cacheDIr := path.Join(source, "cache")
	downloadDir := path.Join(source, "download")
	databaseDir := path.Join(source, "database")

	cacheE, _ := exists(cacheDIr)
	downloadE, _ := exists(downloadDir)
	databaseE, _ := exists(databaseDir)

	if cacheE {
		os.Rename(cacheDIr, path.Join(target, "cache"))
	}

	if downloadE {
		os.Rename(downloadDir, path.Join(target, "download"))
	}

	if databaseE {
		os.Rename(databaseDir, path.Join(target, "database"))
	}

}

func exists(name string) (bool, error) {
	_, err := os.Stat(name)
	if err == nil {
		return true, nil
	}
	if errors.Is(err, os.ErrNotExist) {
		return false, nil
	}
	return false, err
}

func InitApplication(application string) {
	nhentai.InitNHentai(application)
}

func FlatInvoke(method string, params string) (string, error) {
	return nhentai.FlatInvoke(method, params)
}

func EventNotify(notify EventNotifyHandler) {
	// controller.EventNotify = notify.OnNotify
}

type EventNotifyHandler interface {
	OnNotify(message string)
}


================================================
FILE: go/nhentai/client.go
================================================
package nhentai

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	source "github.com/niuhuan/nhentai-go"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"nhentai/nhentai/database/active"
	"nhentai/nhentai/database/cache"
	"nhentai/nhentai/database/properties"
	"os"
	"path"
	"strconv"
	"time"
)

var dialer = &net.Dialer{
	Timeout:   30 * time.Second,
	KeepAlive: 30 * time.Second,
}

var client = &source.Client{
	Client: http.Client{
		Transport: &http.Transport{
			Proxy:                 nil,
			TLSHandshakeTimeout:   time.Second * 20,
			ExpectContinueTimeout: time.Second * 20,
			ResponseHeaderTimeout: time.Second * 20,
			IdleConnTimeout:       time.Second * 20,
		},
	},
}

func initClient() {
	proxy, _ := properties.LoadProperty("proxy", "")
	setProxy(proxy)
}

func setProxy(proxyUrlString string) (string, error) {
	var proxy func(_ *http.Request) (*url.URL, error)
	if proxyUrlString != "" {
		proxyUrl, proxyErr := url.Parse(proxyUrlString)
		if proxyErr != nil {
			return "", proxyErr
		}
		proxy = func(_ *http.Request) (*url.URL, error) {
			return proxyUrl, proxyErr
		}
	}
	properties.SaveProperty("proxy", proxyUrlString)
	client.Client.Transport.(*http.Transport).Proxy = proxy
	return "", nil
}

func getProxy(_ string) (string, error) {
	return properties.LoadProperty("proxy", "")
}

func comics(params string) (string, error) {
	page, err := strconv.Atoi(params)
	if err != nil {
		return "", err
	}
	return cacheable(
		fmt.Sprintf("COMICS$%d", page),
		time.Hour,
		func() (interface{}, error) {
			return client.Comics(page)
		},
	)
}

func comicsByTagName(params string) (string, error) {
	var paramsStruct struct {
		TagName string `json:"tag_name"`
		Page    int    `json:"page"`
	}
	err := json.Unmarshal([]byte(params), &paramsStruct)
	if err != nil {
		return "", err
	}
	return cacheable(
		fmt.Sprintf("COMICS_BY_TAG$%s$%d", paramsStruct.TagName, paramsStruct.Page),
		time.Hour,
		func() (interface{}, error) {
			return client.ComicsByTagName(paramsStruct.TagName, paramsStruct.Page)
		},
	)
}

func comicsBySearchRaw(params string) (string, error) {
	var paramsStruct struct {
		Raw  string `json:"raw"`
		Page int    `json:"page"`
	}
	err := json.Unmarshal([]byte(params), &paramsStruct)
	if err != nil {
		return "", err
	}
	return cacheable(
		fmt.Sprintf("COMICS_BY_SEARCH_RAW$%s$%d", paramsStruct.Raw, paramsStruct.Page),
		time.Hour,
		func() (interface{}, error) {
			return client.ComicByRawCondition(paramsStruct.Raw, paramsStruct.Page)
		},
	)
}

func comicInfo(params string) (string, error) {
	id, err := strconv.Atoi(params)
	if err != nil {
		return "", err
	}
	return cacheable(
		fmt.Sprintf("COMIC_INFO$%d", id),
		time.Hour,
		func() (interface{}, error) {
			return client.ComicInfo(id)
		},
	)
}

func cacheImageByUrlPath(url string) (string, error) {
	lock := HashLock(url)
	lock.Lock()
	defer lock.Unlock()
	// downloadPage
	p1 := active.FindDownloadPageByUrl(url)
	if p1 != nil {
		return path.Join(downloadPath, p1.DownloadLocalPath), nil
	}
	// downloadPageThumb
	p2 := active.FindDownloadPageThumbByUrl(url)
	if p2 != nil {
		return path.Join(downloadPath, p2.DownloadLocalPath), nil
	}
	// downloadCover
	p3 := active.FindDownloadCoverByUrl(url)
	if p3 != nil {
		return path.Join(downloadPath, p3.DownloadLocalPath), nil
	}
	// downloadCoverThumb
	p4 := active.FindDownloadCoverThumbByUrl(url)
	if p4 != nil {
		return path.Join(downloadPath, p4.DownloadLocalPath), nil
	}
	// cache
	cache := cache.FindImageCache(url)
	// no cache
	if cache == nil {
		remote, err := decodeAndSaveImage(url)
		if err != nil {
			return "", err
		}
		cache = remote
	}
	return cacheImagePath(cache.LocalPath), nil
}

func decodeAndSaveImage(url string) (*cache.ImageCache, error) {
	buff, err := decodeFromUrl(url)
	if err != nil {
		println(fmt.Sprintf("decode error : %s : %s", url, err.Error()))
		return nil, err
	}
	local := fmt.Sprintf("%x", md5.Sum([]byte(url)))
	real := cacheImagePath(local)
	err = ioutil.WriteFile(
		real,
		buff, os.FileMode(0600),
	)
	if err != nil {
		return nil, err
	}
	imageCache := cache.ImageCache{
		Url:       url,
		LocalPath: local,
	}
	err = cache.SaveImageCache(&imageCache)
	return &imageCache, err
}


================================================
FILE: go/nhentai/common.go
================================================
package nhentai

import (
	"bytes"
	"encoding/json"
	"image"
	"image/jpeg"
	"io/ioutil"
	"nhentai/nhentai/database/cache"
	"os"
	"path"
	"time"
)

// PING

// @
// "104.27.195.88:443"

// t.nhentai.net
//185.177.127.78 (3115417422)
//185.177.127.77 (3115417421)
//23.237.126.122 (401440378)

// t5.nhentai.net
// 185.177.127.77 (3115417421)

// i.nhentai.net
//185.177.127.78 (3115417422)
//23.237.126.122 (401440378)
//185.177.127.77 (3115417421)

func availableWebAddresses(_ string) (string, error) {
	return serialize([]string{
		"104.21.66.123:443",
		"172.67.159.231:443",
	}, nil)
}

func availableImgAddresses(_ string) (string, error) {
	return serialize([]string{
		"185.107.44.3:443",
		"185.177.127.78:443",
		"185.177.127.77:443",
	}, nil)
}

func cacheable(key string, expire time.Duration, reload func() (interface{}, error)) (string, error) {
	// CACHE
	cacheable, err := cache.LoadCache(key, expire)
	if err != nil {
		return "", err
	}
	if cacheable != "" {
		return cacheable, nil
	}
	// RELOAD
	cacheable, err = serialize(reload())
	if err != nil {
		return "", err
	}
	// push to cache (if cache error )
	_ = cache.SaveCache(key, cacheable)
	// return
	return cacheable, nil
}

// 将interface序列化成字符串, 方便与flutter通信
func serialize(point interface{}, err error) (string, error) {
	if err != nil {
		return "", err
	}
	buff, err := json.Marshal(point)
	return string(buff), nil
}

func convertImageToJPEG100(params string) (string, error) {
	var paramsStruct struct {
		Path string `json:"path"`
		Dir  string `json:"dir"`
	}
	err := json.Unmarshal([]byte(params), &paramsStruct)
	if err != nil {
		return "", err
	}
	buff, err := ioutil.ReadFile(paramsStruct.Path)
	if err != nil {
		return "", err
	}
	reader := bytes.NewReader(buff)
	i, _, err := image.Decode(reader)
	if err != nil {
		return "", err
	}
	to := path.Join(paramsStruct.Dir, path.Base(paramsStruct.Path)+".jpg")
	stream, err := os.Create(to)
	if err != nil {
		return "", err
	}
	defer stream.Close()
	return "", jpeg.Encode(stream, i, &jpeg.Options{Quality: 100})
}


================================================
FILE: go/nhentai/constant/constant.go
================================================
package constant

import (
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
	"hash/fnv"
	"os"
	"sync"
)

var (
	CreateDirMode  = os.FileMode(0700)
	CreateFileMode = os.FileMode(0600)
	GormConfig     = &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
	}
)

var hashMutex []*sync.Mutex

func init() {
	for i := 0; i < 32; i++ {
		hashMutex = append(hashMutex, &sync.Mutex{})
	}
}

// HashLock Hash一样的图片不同时处理
func HashLock(key string) *sync.Mutex {
	hash := fnv.New32()
	hash.Write([]byte(key))
	return hashMutex[int(hash.Sum32()%uint32(len(hashMutex)))]
}


================================================
FILE: go/nhentai/constant/os.go
================================================
package constant

import (
	"os"
	"strings"
)

func ObtainDir(dir string) {
	if _, err := os.Stat(dir); err != nil {
		if os.IsNotExist(err) {
			err = os.MkdirAll(dir, CreateDirMode)
			if err != nil {
				panic(err)
			}
		} else {
			panic(err)
		}
	}
}

func ReasonableFileName(title string) string {
	title = strings.ReplaceAll(title, "\\", "_")
	title = strings.ReplaceAll(title, "/", "_")
	title = strings.ReplaceAll(title, "*", "_")
	title = strings.ReplaceAll(title, "?", "_")
	title = strings.ReplaceAll(title, "<", "_")
	title = strings.ReplaceAll(title, ">", "_")
	title = strings.ReplaceAll(title, "|", "_")
	return title
}


================================================
FILE: go/nhentai/constant/time.go
================================================
package constant

import "time"

// Timestamp 获取当前的Unix时间戳
func Timestamp() int64 {
	return time.Now().UnixNano() / int64(time.Millisecond)
}


================================================
FILE: go/nhentai/database/active/active.go
================================================
package active

import (
	"errors"
	"github.com/niuhuan/nhentai-go"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"nhentai/nhentai/constant"
	"path"
	"sync"
)

var client = nhentai.Client{}
var mutex = sync.Mutex{}
var db *gorm.DB

func Init(databaseDir string) {
	var err error
	db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "download.db")), constant.GormConfig)
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(&ViewLog{})
	db.AutoMigrate(&ViewLogTag{})
	db.AutoMigrate(&Download{})
	db.AutoMigrate(&DownloadTag{})
	db.AutoMigrate(&DownloadCover{})
	db.AutoMigrate(&DownloadCoverThumb{})
	db.AutoMigrate(&DownloadPage{})
	db.AutoMigrate(&DownloadPageThumb{})
}

type ViewLog struct {
	ID            int    `gorm:"primarykey" json:"id"`
	MediaId       int    `json:"media_id"`
	TitleEnglish  string `json:"title_english"`
	TitleJapanese string `json:"title_japanese"`
	TitlePretty   string `json:"title_pretty"`
	Scanlator     string `json:"scanlator"`
	UploadDate    int    `json:"upload_date"`
	NumPages      int    `json:"num_pages"`
	NumFavorites  int    `json:"num_favorites"`
	LastViewTime  int64  `json:"last_view_time"`
	LastViewIndex int    `json:"last_view_index"`
}

type ViewLogTag struct {
	ComicId int    `gorm:"primarykey" json:"comic_id"`
	ID      int    `gorm:"primarykey" json:"id"`
	Name    string `json:"name"`
	Count   int    `json:"count"`
	Type    string `json:"type"`
	Url     string `json:"url"`
}

type Download struct {
	ID                  int    `gorm:"primarykey" json:"id"`
	MediaId             int    `json:"media_id"`
	TitleEnglish        string `json:"title_english"`
	TitleJapanese       string `json:"title_japanese"`
	TitlePretty         string `json:"title_pretty"`
	Scanlator           string `json:"scanlator"`
	UploadDate          int    `json:"upload_date"`
	NumPages            int    `json:"num_pages"`
	NumFavorites        int    `json:"num_favorites"`
	DownloadCreatedTime int64  `json:"download_created_time"`
	DownloadStatus      int    `json:"download_status"` // 未完成, 1成功, 2失败
}

type DownloadTag struct {
	ComicId int    `gorm:"primarykey" json:"comic_id"`
	ID      int    `gorm:"primarykey" json:"id"`
	Name    string `json:"name"`
	Count   int    `json:"count"`
	Type    string `json:"type"`
	Url     string `json:"url"`
}

type DownloadCover struct {
	ComicId           int    `gorm:"primarykey" json:"comic_id"`
	Url               string `gorm:"index:idx_url" json:"url"`
	T                 string `json:"t"`
	W                 int    `json:"w"`
	H                 int    `json:"h"`
	DownloadStatus    int    `json:"download_status"` // 未完成, 1成功, 2失败
	DownloadLocalPath string
}

type DownloadCoverThumb struct {
	ComicId           int    `gorm:"primarykey" json:"comic_id"`
	Url               string `gorm:"index:idx_url" json:"url"`
	T                 string `json:"t"`
	W                 int    `json:"w"`
	H                 int    `json:"h"`
	DownloadStatus    int    `json:"download_status"` // 未完成, 1成功, 2失败
	DownloadLocalPath string
}

type DownloadPage struct {
	ComicId           int    `gorm:"primarykey" json:"comic_id"`
	PageIndex         int    `gorm:"primarykey" json:"page_index"`
	Num               int    `json:"num"`
	Url               string `gorm:"index:idx_url" json:"url"`
	T                 string `json:"t"`
	W                 int    `json:"w"`
	H                 int    `json:"h"`
	DownloadStatus    int    `json:"download_status"` // 未完成, 1成功, 2失败
	DownloadLocalPath string
}

type DownloadPageThumb struct {
	ComicId           int    `gorm:"primarykey" json:"comic_id"`
	PageIndex         int    `gorm:"primarykey" json:"page_index"`
	Num               int    `json:"num"`
	Url               string `gorm:"index:idx_url" json:"url"`
	DownloadStatus    int    `json:"download_status"` // 未完成, 1成功, 2失败
	DownloadLocalPath string
}

func SaveViewInfo(info nhentai.ComicInfo) error {
	viewLog := takeViewLog(info, 0)
	tags := takeViewLogTags(info.Tags, info.Id)
	mutex.Lock()
	defer mutex.Unlock()
	return db.Transaction(func(tx *gorm.DB) error {
		err := tx.Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "id"}},
			DoUpdates: clause.AssignmentColumns([]string{
				"id",
				"media_id",
				"title_english",
				"title_japanese",
				"title_pretty",
				"scanlator",
				"upload_date",
				"num_pages",
				"num_favorites",
				"last_view_time",
			}),
		}).Create(&viewLog).Error
		if err != nil {
			return err
		}
		return saveViewTagInTx(tx, tags)
	})
}

func SaveViewIndex(info nhentai.ComicInfo, index int) error {
	viewLog := takeViewLog(info, index)
	tags := takeViewLogTags(info.Tags, info.Id)
	mutex.Lock()
	defer mutex.Unlock()
	return db.Transaction(func(tx *gorm.DB) error {
		err := tx.Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "id"}},
			DoUpdates: clause.AssignmentColumns([]string{
				"id",
				"media_id",
				"title_english",
				"title_japanese",
				"title_pretty",
				"scanlator",
				"upload_date",
				"num_pages",
				"num_favorites",
				"last_view_time",
				"last_view_index",
			}),
		}).Create(&viewLog).Error
		if err != nil {
			return err
		}
		return saveViewTagInTx(tx, tags)
	})
}

func saveViewTagInTx(tx *gorm.DB, tags []ViewLogTag) error {
	var err error
	for i := 0; i < len(tags); i++ {
		err = tx.Clauses(clause.OnConflict{
			Columns: []clause.Column{
				{Name: "comic_id"},
				{Name: "id"},
			},
			DoUpdates: clause.AssignmentColumns([]string{
				"comic_id",
				"id",
				"type",
				"count",
				"name",
				"url",
			}),
		}).Create(&tags[i]).Error
		if err != nil {
			return err
		}
	}
	return nil
}

func takeViewLogTags(infoTags []nhentai.ComicInfoTag, comicId int) []ViewLogTag {
	tags := make([]ViewLogTag, len(infoTags))
	for i := 0; i < len(infoTags); i++ {
		tags[i] = ViewLogTag{
			ComicId: comicId,
			ID:      infoTags[i].Id,
			Type:    infoTags[i].Type,
			Count:   infoTags[i].Count,
			Name:    infoTags[i].Name,
			Url:     infoTags[i].Url,
		}
	}
	return tags
}

func takeViewLog(info nhentai.ComicInfo, index int) ViewLog {
	return ViewLog{
		ID:            info.Id,
		MediaId:       info.MediaId,
		TitleEnglish:  info.Title.English,
		TitleJapanese: info.Title.Japanese,
		TitlePretty:   info.Title.Pretty,
		Scanlator:     info.Scanlator,
		UploadDate:    info.UploadDate,
		NumPages:      info.NumPages,
		NumFavorites:  info.NumFavorites,
		LastViewTime:  constant.Timestamp(),
		LastViewIndex: index,
	}
}

func LoadLastViewIndexByComicId(comicId int) (int, error) {
	mutex.Lock()
	defer mutex.Unlock()
	var viewLog ViewLog
	err := db.Where("id = ?", comicId).Find(&viewLog).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return 0, nil
		}
		return 0, err
	}
	return viewLog.LastViewIndex, nil
}

func CreateDownload(info nhentai.ComicInfo) error {
	download := Download{
		ID:                  info.Id,
		MediaId:             info.MediaId,
		TitleEnglish:        info.Title.English,
		TitleJapanese:       info.Title.Japanese,
		TitlePretty:         info.Title.Pretty,
		Scanlator:           info.Scanlator,
		UploadDate:          info.UploadDate,
		NumPages:            info.NumPages,
		NumFavorites:        info.NumFavorites,
		DownloadCreatedTime: constant.Timestamp(),
		DownloadStatus:      0,
	}
	tags := takeDownloadTags(info.Tags, info.Id)
	cover := DownloadCover{
		ComicId:           info.Id,
		Url:               client.CoverUrl(info.MediaId, info.Images.Cover.T),
		T:                 info.Images.Cover.T,
		W:                 info.Images.Cover.W,
		H:                 info.Images.Cover.H,
		DownloadStatus:    0,
		DownloadLocalPath: "",
	}
	coverThumb := DownloadCoverThumb{
		ComicId:           info.Id,
		Url:               client.ThumbnailUrl(info.MediaId, info.Images.Thumbnail.T),
		T:                 info.Images.Thumbnail.T,
		W:                 info.Images.Thumbnail.W,
		H:                 info.Images.Thumbnail.H,
		DownloadStatus:    0,
		DownloadLocalPath: "",
	}
	pages := make([]DownloadPage, len(info.Images.Pages))
	pagesThumbs := make([]DownloadPageThumb, len(info.Images.Pages))
	for i := 0; i < len(info.Images.Pages); i++ {
		pages[i] = DownloadPage{
			ComicId:           info.Id,
			PageIndex:         i,
			Num:               i + 1,
			Url:               client.PageUrl(info.MediaId, i+1, info.Images.Pages[i].T),
			T:                 info.Images.Pages[i].T,
			W:                 info.Images.Pages[i].W,
			H:                 info.Images.Pages[i].H,
			DownloadStatus:    0,
			DownloadLocalPath: "",
		}
		pagesThumbs[i] = DownloadPageThumb{
			ComicId:           info.Id,
			PageIndex:         i,
			Num:               i + 1,
			Url:               client.PageThumbnailUrl(info.MediaId, i+1, info.Images.Pages[i].T),
			DownloadStatus:    0,
			DownloadLocalPath: "",
		}
	}
	mutex.Lock()
	defer mutex.Unlock()
	return db.Transaction(func(tx *gorm.DB) error {
		err := tx.Where("id = ?", download.ID).First(&Download{}).Error
		if err == nil {
			return errors.New("download exists")
		}
		if err != gorm.ErrRecordNotFound {
			return err
		}
		err = tx.Save(&download).Error
		if err != nil {
			return err
		}
		for i := 0; i < len(tags); i++ {
			tx.Save(&(tags[i]))
			if err != nil {
				return err
			}
		}
		err = tx.Save(&cover).Error
		if err != nil {
			return err
		}
		err = tx.Save(&coverThumb).Error
		if err != nil {
			return err
		}
		for i := 0; i < len(pages); i++ {
			tx.Save(&(pages[i]))
			if err != nil {
				return err
			}
		}
		for i := 0; i < len(pagesThumbs); i++ {
			tx.Save(&(pagesThumbs[i]))
			if err != nil {
				return err
			}
		}
		return nil
	})
}

func takeDownloadTags(infoTags []nhentai.ComicInfoTag, comicId int) []DownloadTag {
	tags := make([]DownloadTag, len(infoTags))
	for i := 0; i < len(infoTags); i++ {
		tags[i] = DownloadTag{
			ComicId: comicId,
			ID:      infoTags[i].Id,
			Type:    infoTags[i].Type,
			Count:   infoTags[i].Count,
			Name:    infoTags[i].Name,
			Url:     infoTags[i].Url,
		}
	}
	return tags
}

func LoadFirstNeedDownload() (*Download, error) {
	mutex.Lock()
	defer mutex.Unlock()
	download := Download{}
	err := db.Where("download_status = 0").Order("download_created_time DESC").First(&download).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, nil
		}
		return nil, err
	}
	return &download, nil
}

func TheDownloadCover(id int) (*DownloadCover, error) {
	mutex.Lock()
	defer mutex.Unlock()
	cover := DownloadCover{}
	err := db.Where("comic_id = ?", id).First(&cover).Error
	return &cover, err
}

func TheDownloadCoverThumb(id int) (*DownloadCoverThumb, error) {
	mutex.Lock()
	defer mutex.Unlock()
	coverThumb := DownloadCoverThumb{}
	err := db.Where("comic_id = ?", id).First(&coverThumb).Error
	return &coverThumb, err
}

func TheDownloadNeedDownloadPages(id int, limit int) ([]DownloadPage, error) {
	mutex.Lock()
	defer mutex.Unlock()
	var pages []DownloadPage
	return pages, db.Where("comic_id = ? AND download_status = 0", id).
		Order("page_index ASC").Limit(limit).
		Find(&pages).Error
}

func TheDownloadNeedDownloadPageThumbs(id int, limit int) ([]DownloadPageThumb, error) {
	mutex.Lock()
	defer mutex.Unlock()
	var pages []DownloadPageThumb
	return pages, db.Where("comic_id = ? AND download_status = 0", id).
		Order("page_index ASC").Limit(limit).
		Find(&pages).Error
}

func SaveDownloadCoverStatus(comicId int, status int, path string) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Model(&DownloadCover{}).Where("comic_id = ?", comicId).Updates(map[string]interface{}{
		"download_status":     status,
		"download_local_path": path,
	}).Error
}

func SaveDownloadCoverThumbStatus(comicId int, status int, path string) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Model(&DownloadCoverThumb{}).Where("comic_id = ?", comicId).Updates(map[string]interface{}{
		"download_status":     status,
		"download_local_path": path,
	}).Error
}

func SaveDownloadPageStatus(comicId int, pageIndex, status int, path string) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Model(&DownloadPage{}).Where("comic_id = ? AND page_index = ?", comicId, pageIndex).Updates(map[string]interface{}{
		"download_status":     status,
		"download_local_path": path,
	}).Error
}

func SaveDownloadPageThumbStatus(comicId int, pageIndex, status int, path string) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Model(&DownloadPageThumb{}).Where("comic_id = ? AND page_index = ?", comicId, pageIndex).Updates(map[string]interface{}{
		"download_status":     status,
		"download_local_path": path,
	}).Error
}

func DownloadCoverOk(comicId int) bool {
	mutex.Lock()
	defer mutex.Unlock()
	var covers []DownloadCover
	err := db.Where("comic_id = ?", comicId).Group("download_status").Find(&covers).Error
	if err != nil {
		panic(err)
	}
	return len(covers) == 1 && covers[0].DownloadStatus == 1
}

func DownloadCoverThumbOk(comicId int) bool {
	mutex.Lock()
	defer mutex.Unlock()
	var covers []DownloadCoverThumb
	err := db.Where("comic_id = ?", comicId).Group("download_status").Find(&covers).Error
	if err != nil {
		panic(err)
	}
	return len(covers) == 1 && covers[0].DownloadStatus == 1
}

func DownloadPageOk(comicId int) bool {
	mutex.Lock()
	defer mutex.Unlock()
	var pages []DownloadPage
	err := db.Where("comic_id = ?", comicId).Group("download_status").Find(&pages).Error
	if err != nil {
		panic(err)
	}
	return len(pages) == 1 && pages[0].DownloadStatus == 1
}

func DownloadPageThumbOk(comicId int) bool {
	mutex.Lock()
	defer mutex.Unlock()
	var pages []DownloadPageThumb
	err := db.Where("comic_id = ?", comicId).Group("download_status").Find(&pages).Error
	if err != nil {
		panic(err)
	}
	return len(pages) == 1 && pages[0].DownloadStatus == 1
}

func SaveDownloadStatus(id int, status int) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Model(&Download{}).Where("id = ?", id).Updates(map[string]interface{}{
		"download_status": status,
	}).Error
}

func FindDownloadPageByUrl(url string) *DownloadPage {
	mutex.Lock()
	defer mutex.Unlock()
	page := DownloadPage{}
	err := db.Where("url = ? AND download_status = 1", url).First(&page).Error
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			panic(err)
		}
		return nil
	}
	return &page
}

func FindDownloadPageThumbByUrl(url string) *DownloadPageThumb {
	mutex.Lock()
	defer mutex.Unlock()
	page := DownloadPageThumb{}
	err := db.Where("url = ? AND download_status = 1", url).First(&page).Error
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			panic(err)
		}
		return nil
	}
	return &page
}

func FindDownloadCoverByUrl(url string) *DownloadCover {
	mutex.Lock()
	defer mutex.Unlock()
	page := DownloadCover{}
	err := db.Where("url = ? AND download_status = 1", url).First(&page).Error
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			panic(err)
		}
		return nil
	}
	return &page
}

func FindDownloadCoverThumbByUrl(url string) *DownloadCoverThumb {
	mutex.Lock()
	defer mutex.Unlock()
	page := DownloadCoverThumb{}
	err := db.Where("url = ? AND download_status = 1", url).First(&page).Error
	if err != nil {
		if err != gorm.ErrRecordNotFound {
			panic(err)
		}
		return nil
	}
	return &page
}

func HasDownload(comicId int) bool {
	mutex.Lock()
	defer mutex.Unlock()
	var download Download
	err := db.Where("id = ?", comicId).First(&download).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return false
		}
		panic(err)
	}
	return true
}

func ListDownloadComicInfo() []DownloadComicInfo {
	mutex.Lock()
	defer mutex.Unlock()
	var err error
	var downloads []Download
	var tags []DownloadTag
	var thumbs []DownloadCoverThumb
	var covers []DownloadCover
	var pages []DownloadPage
	err = db.Find(&downloads).Error
	if err != nil {
		panic(err)
	}
	err = db.Find(&tags).Error
	if err != nil {
		panic(err)
	}
	err = db.Find(&covers).Error
	if err != nil {
		panic(err)
	}
	err = db.Find(&thumbs).Error
	if err != nil {
		panic(err)
	}
	err = db.Order("page_index ASC").Find(&pages).Error
	if err != nil {
		panic(err)
	}
	var infos = make([]DownloadComicInfo, len(downloads))
	for i := 0; i < len(infos); i++ {
		infos[i] = DownloadComicInfo{
			ComicInfo: nhentai.ComicInfo{
				Id:      downloads[i].ID,
				MediaId: downloads[i].MediaId,
				Title: nhentai.ComicInfoTitle{
					English:  downloads[i].TitleEnglish,
					Japanese: downloads[i].TitleJapanese,
					Pretty:   downloads[i].TitlePretty,
				},
				Images: nhentai.ComicInfoImages{
					Pages:     filterPages(pages, downloads[i].ID),
					Cover:     filterCover(covers, downloads[i].ID),
					Thumbnail: filterCoverThumb(thumbs, downloads[i].ID),
				},
				Scanlator:    downloads[i].Scanlator,
				UploadDate:   downloads[i].UploadDate,
				Tags:         filterTags(tags, downloads[i].ID),
				NumPages:     downloads[i].NumPages,
				NumFavorites: downloads[i].NumFavorites,
			},
			DownloadStatus: downloads[i].DownloadStatus,
		}
	}
	return infos
}

func filterPages(pages []DownloadPage, id int) []nhentai.ImageInfo {
	rsp := make([]nhentai.ImageInfo, 0)
	for i := 0; i < len(pages); i++ {
		if pages[i].ComicId == id {
			rsp = append(rsp, nhentai.ImageInfo{
				T: pages[i].T,
				W: pages[i].W,
				H: pages[i].H,
			})
		}
	}
	return rsp
}

func filterTags(tags []DownloadTag, id int) []nhentai.ComicInfoTag {
	rsp := make([]nhentai.ComicInfoTag, 0)
	for i := 0; i < len(tags); i++ {
		if tags[i].ComicId == id {
			rsp = append(rsp, nhentai.ComicInfoTag{
				Id:    tags[i].ID,
				Name:  tags[i].Name,
				Count: tags[i].Count,
				Type:  tags[i].Type,
				Url:   tags[i].Url,
			})
		}
	}
	return rsp
}

func filterCover(covers []DownloadCover, id int) nhentai.ImageInfo {
	for i := 0; i < len(covers); i++ {
		if covers[i].ComicId == id {
			return nhentai.ImageInfo{
				T: covers[i].T,
				W: covers[i].W,
				H: covers[i].H,
			}
		}
	}
	return nhentai.ImageInfo{}
}

func filterCoverThumb(covers []DownloadCoverThumb, id int) nhentai.ImageInfo {
	for i := 0; i < len(covers); i++ {
		if covers[i].ComicId == id {
			return nhentai.ImageInfo{
				T: covers[i].T,
				W: covers[i].W,
				H: covers[i].H,
			}
		}
	}
	return nhentai.ImageInfo{}
}

type DownloadComicInfo struct {
	nhentai.ComicInfo
	DownloadStatus int `json:"download_status"`
}

func ResetAllDownload() {
	mutex.Lock()
	defer mutex.Unlock()
	var err error
	err = db.Exec("UPDATE download set download_status = 0 WHERE download_status = 2").Error
	if err != nil {
		panic(err)
	}
	err = db.Exec("UPDATE download_cover set download_status = 0 WHERE download_status = 2").Error
	if err != nil {
		panic(err)
	}
	err = db.Exec("UPDATE download_cover_thumb set download_status = 0 WHERE download_status = 2").Error
	if err != nil {
		panic(err)
	}
	err = db.Exec("UPDATE download_page set download_status = 0 WHERE download_status = 2").Error
	if err != nil {
		panic(err)
	}
	err = db.Exec("UPDATE download_page_thumb set download_status = 0 WHERE download_status = 2").Error
	if err != nil {
		panic(err)
	}
}

func MarkComicDeleting(id int) {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Model(&Download{}).Where("id = ?", id).Update("download_status", 3).Error
	if err != nil {
		panic(err)
	}
}

func LoadFirstNeedDelete() *Download {
	mutex.Lock()
	defer mutex.Unlock()
	download := Download{}
	err := db.Where("download_status = 3").Order("download_created_time ASC").First(&download).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		panic(err)
	}
	return &download
}

func DeletedComic(id int) {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Transaction(func(tx *gorm.DB) error {
		var err error
		err = tx.Unscoped().Delete(&Download{}, "id = ?", id).Error
		if err != nil {
			return err
		}
		err = tx.Unscoped().Delete(&DownloadCover{}, "comic_id = ?", id).Error
		if err != nil {
			return err
		}
		err = tx.Unscoped().Delete(&DownloadPage{}, "comic_id = ?", id).Error
		if err != nil {
			return err
		}
		err = tx.Unscoped().Delete(&DownloadTag{}, "comic_id = ?", id).Error
		if err != nil {
			return err
		}
		err = tx.Unscoped().Delete(&DownloadCoverThumb{}, "comic_id = ?", id).Error
		if err != nil {
			return err
		}
		err = tx.Unscoped().Delete(&DownloadPageThumb{}, "comic_id = ?", id).Error
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		panic(err)
	}
}


================================================
FILE: go/nhentai/database/cache/cache.go
================================================
package cache

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"nhentai/nhentai/constant"
	"path"
	"sync"
	"time"
)

var mutex = sync.Mutex{}
var db *gorm.DB

type NetworkCache struct {
	gorm.Model
	K string `gorm:"index:uk_k,unique"`
	V string
}

type ImageCache struct {
	gorm.Model
	Url       string `gorm:"index:uk_url,unique" json:"fileServer"`
	LocalPath string `json:"localPath"`
	FileSize  int64  `json:"fileSize"`
}

func Init(databaseDir string) {
	var err error
	db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "cache.db")), constant.GormConfig)
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(&NetworkCache{})
	db.AutoMigrate(&ImageCache{})
}

func LoadCache(key string, expire time.Duration) (string, error) {
	mutex.Lock()
	defer mutex.Unlock()
	var cache NetworkCache
	err := db.First(&cache, "k = ? AND updated_at > ?", key, time.Now().Add(expire*-1)).Error
	if err == nil {
		return cache.V, nil
	}
	if gorm.ErrRecordNotFound == err {
		return "", nil
	}
	return "", err
}

func SaveCache(key string, value string) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "k"}},
		DoUpdates: clause.AssignmentColumns([]string{"created_at", "updated_at", "v"}),
	}).Create(&NetworkCache{
		K: key,
		V: value,
	}).Error
}

func RemoveCache(key string) error {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Unscoped().Delete(&NetworkCache{}, "k = ?", key).Error
	if err == gorm.ErrRecordNotFound {
		return nil
	}
	return err
}

func RemoveCaches(like string) error {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Unscoped().Delete(&NetworkCache{}, "k LIKE ?", like).Error
	if err == gorm.ErrRecordNotFound {
		return nil
	}
	return err
}

func RemoveAllCache() error {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Unscoped().Delete(&NetworkCache{}, "1 = 1").Error
	if err != nil {
		return err
	}
	return db.Raw("VACUUM").Error
}

func RemoveEarliestCache(earliest time.Time) error {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Unscoped().Where("strftime('%s',updated_at) < strftime('%s',?)", earliest).
		Delete(&NetworkCache{}).Error
	if err != nil {
		return err
	}
	return db.Raw("VACUUM").Error
}

func SaveImageCache(remote *ImageCache) error {
	mutex.Lock()
	defer mutex.Unlock()
	return db.Clauses(clause.OnConflict{
		Columns: []clause.Column{{Name: "url"}},
		DoUpdates: clause.AssignmentColumns([]string{
			"updated_at",
			"file_size",
			"local_path",
		}),
	}).Create(remote).Error
}

func FindImageCache(url string) *ImageCache {
	mutex.Lock()
	defer mutex.Unlock()
	var imageCache ImageCache
	err := db.First(&imageCache, "url = ?", url).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		} else {
			panic(err)
		}
	}
	return &imageCache
}

func RemoveAllImageCache() error {
	mutex.Lock()
	defer mutex.Unlock()
	err := db.Unscoped().Delete(&ImageCache{}, "1 = 1").Error
	if err != nil {
		return err
	}
	return db.Raw("VACUUM").Error
}

func EarliestImageCache(earliest time.Time, pageSize int) ([]ImageCache, error) {
	mutex.Lock()
	defer mutex.Unlock()
	var images []ImageCache
	err := db.Where("strftime('%s',updated_at) < strftime('%s',?)", earliest).
		Order("updated_at").Limit(pageSize).Find(&images).Error
	return images, err
}

func DeleteImageCache(images []ImageCache) error {
	mutex.Lock()
	defer mutex.Unlock()
	if len(images) == 0 {
		return nil
	}
	ids := make([]uint, len(images))
	for i := 0; i < len(images); i++ {
		ids[i] = images[i].ID
	}
	return db.Unscoped().Model(&ImageCache{}).Delete("id in ?", ids).Error
}


================================================
FILE: go/nhentai/database/properties/properties.go
================================================
package properties

import (
	"errors"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"nhentai/nhentai/constant"
	"path"
	"strconv"
)

var db *gorm.DB

func Init(databaseDir string) {
	var err error
	db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "properties.db")), constant.GormConfig)
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(&Property{})
}

type Property struct {
	gorm.Model
	K string `gorm:"index:uk_k,unique"`
	V string
}

func LoadProperty(name string, defaultValue string) (string, error) {
	var property Property
	err := db.First(&property, "k", name).Error
	if err == nil {
		return property.V, nil
	}
	if gorm.ErrRecordNotFound == err {
		return defaultValue, nil
	}
	panic(errors.New("?"))
}

func SaveProperty(name string, value string) error {
	return db.Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "k"}},
		DoUpdates: clause.AssignmentColumns([]string{"created_at", "updated_at", "v"}),
	}).Create(&Property{
		K: name,
		V: value,
	}).Error
}

func LoadBoolProperty(name string, defaultValue bool) (bool, error) {
	stringValue, err := LoadProperty(name, strconv.FormatBool(defaultValue))
	if err != nil {
		return false, err
	}
	return strconv.ParseBool(stringValue)
}

func LoadIntProperty(name string, defaultValue int) (int, error) {
	var property Property
	err := db.First(&property, "k", name).Error
	if err == nil {
		return strconv.Atoi(property.V)
	}
	if gorm.ErrRecordNotFound == err {
		return defaultValue, nil
	}
	panic(errors.New("?"))
}


================================================
FILE: go/nhentai/decodes.go
================================================
package nhentai

import (
	"errors"
	_ "golang.org/x/image/webp"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"io/ioutil"
	"net/http"
	"nhentai/nhentai/database/cache"
	"sync"
)

var mutexCounter = -1
var busMutex *sync.Mutex
var subMutexes []*sync.Mutex

func init() {
	busMutex = &sync.Mutex{}
	for i := 0; i < 5; i++ {
		subMutexes = append(subMutexes, &sync.Mutex{})
	}
}

// takeMutex 下载图片获取一个锁, 这样只能同时下载5张图片
func takeMutex() *sync.Mutex {
	busMutex.Lock()
	defer busMutex.Unlock()
	mutexCounter = (mutexCounter + 1) % len(subMutexes)
	return subMutexes[mutexCounter]
}

// 下载图片并decode
func decodeFromUrl(url string) ([]byte, error) {
	m := takeMutex()
	m.Lock()
	defer m.Unlock()
	request, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}
	response, err := client.Do(request)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	buff, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	if response.StatusCode != 200 {
		println("NOT 200")
		println(string(buff))
		return nil, errors.New("code is not 200")
	}
	return buff, nil
}

// decodeFromCache 仅下载使用
func decodeFromRemote(url string) ([]byte, error) {
	cache := cache.FindImageCache(url)
	if cache != nil {
		return ioutil.ReadFile(cacheImagePath(cache.LocalPath))
	}
	return nil, errors.New("not found")
}


================================================
FILE: go/nhentai/download.go
================================================
package nhentai

import (
	"fmt"
	"io/ioutil"
	"nhentai/nhentai/constant"
	"nhentai/nhentai/database/active"
	"os"
	"path"
	"sync"
	"time"
)

var initDownloadFlag bool
var downloadThreadCount = 1
var downloadThreadFetch = 100
var downloadRunning = false
var downloadRestart = false

func initDownload() {
	if initDownloadFlag {
		return
	}
	active.ResetAllDownload()
	initDownloadFlag = true
	downloadRunning = true
	go downloadBegin()
}

// 下载周期中, 每个下载单元会调用此方法, 如果返回true应该停止当前动作
func downloadHasStop() bool {
	if !downloadRunning {
		return true
	}
	if downloadRestart {
		downloadRestart = false
		return true
	}
	return false
}

// 删除第一个需要删除的漫画, 成功删除返回true
func downloadDelete() bool {
	needDelete := active.LoadFirstNeedDelete()
	if needDelete != nil {
		relativeFolder := fmt.Sprintf("%d/%d", needDelete.ID, needDelete.MediaId)
		absoluteFolder := path.Join(downloadPath, relativeFolder)
		err := os.RemoveAll(absoluteFolder)
		if err != nil {
			panic(err)
		}
		active.DeletedComic(needDelete.ID)
		return true
	}
	return false
}

// 下载启动/重新启动会暂停三秒
func downloadBegin() {
	time.Sleep(time.Second * 3)
	// 每次下载完一个漫画, 或者启动的时候, 首先进行删除任务
	for downloadDelete() {
	}
	if downloadHasStop() {
		return
	}
	go downloadLoadComic()
}

// 加载第一个需要下载的漫画
func downloadLoadComic() {
	if downloadHasStop() {
		go downloadBegin()
		return
	}
	// 找到第一个要下载的漫画, 查库有错误就停止, 因为这些错误很少出现, 一旦出现必然是严重的, 例如数据库文件突然被删除
	downloadingComic, err := active.LoadFirstNeedDownload()
	if err != nil {
		panic(err)
	}
	if downloadingComic == nil {
		println("没有找到要下载的漫画")
		go downloadBegin()
		return
	}
	go downloadProcessDownloadingComic(downloadingComic)
}

func downloadProcessDownloadingComic(downloadingComic *active.Download) {
	if downloadHasStop() {
		go downloadBegin()
		return
	}
	//
	relativeFolder := fmt.Sprintf("%d/%d", downloadingComic.ID, downloadingComic.MediaId)
	absoluteFolder := path.Join(downloadPath, relativeFolder)
	constant.ObtainDir(absoluteFolder)
	// 下载封面
	cover, err := active.TheDownloadCover(downloadingComic.ID)
	if err != nil {
		panic(err)
	}
	if cover.DownloadStatus == 0 {
		buff, err := downloadDecodeUrl(cover.Url)
		if buff != nil {
			coverName := "cover"
			relativeCover := path.Join(relativeFolder, coverName)
			absoluteCover := path.Join(absoluteFolder, coverName)
			err = ioutil.WriteFile(absoluteCover, buff, constant.CreateFileMode)
			if err != nil {
				panic(err)
			}
			active.SaveDownloadCoverStatus(downloadingComic.ID, 1, relativeCover)
		} else {
			active.SaveDownloadCoverStatus(downloadingComic.ID, 2, "")
		}
	}
	// 下载封面缩略图
	coverThumb, err := active.TheDownloadCoverThumb(downloadingComic.ID)
	if err != nil {
		panic(err)
	}
	if coverThumb.DownloadStatus == 0 {
		buff, err := downloadDecodeUrl(coverThumb.Url)
		if buff != nil {
			coverName := "cover_thumb"
			relativeCoverThumb := path.Join(relativeFolder, coverName)
			absoluteCoverThumb := path.Join(absoluteFolder, coverName)
			err = ioutil.WriteFile(absoluteCoverThumb, buff, constant.CreateFileMode)
			if err != nil {
				panic(err)
			}
			active.SaveDownloadCoverThumbStatus(downloadingComic.ID, 1, relativeCoverThumb)
		} else {
			active.SaveDownloadCoverThumbStatus(downloadingComic.ID, 2, "")
		}
	}
	// 暂停检测
	if downloadHasStop() {
		go downloadBegin()
		return
	}
	// 下载漫画
	// WARNING 无限循环
	for {
		downloadingPictures, err := active.TheDownloadNeedDownloadPages(downloadingComic.ID, downloadThreadFetch)
		if err != nil {
			panic(err)
		}
		if len(downloadingPictures) == 0 {
			break
		}
		// 多线程下载漫画
		hasStop := func() bool {
			channel := make(chan int, downloadThreadCount)
			defer close(channel)
			wg := sync.WaitGroup{}
			for i := 0; i < len(downloadingPictures); i++ {
				// 暂停检测
				if downloadHasStop() {
					wg.Wait()
					return true
				}
				channel <- 0
				wg.Add(1)
				// 不放入携程, 防止i已经变化
				pagePoint := &(downloadingPictures[i])
				go func() {
					// 下载漫画
					buff, err := downloadDecodeUrl(pagePoint.Url)
					if buff != nil {
						pageName := fmt.Sprintf("p_%d", pagePoint.PageIndex)
						relativePageName := path.Join(relativeFolder, pageName)
						absolutePageName := path.Join(absoluteFolder, pageName)
						err = ioutil.WriteFile(absolutePageName, buff, constant.CreateFileMode)
						if err != nil {
							panic(err)
						}
						active.SaveDownloadPageStatus(downloadingComic.ID, pagePoint.PageIndex, 1, relativePageName)
					} else {
						active.SaveDownloadPageStatus(downloadingComic.ID, pagePoint.PageIndex, 2, "")
					}
					// 下载漫画
					<-channel
					wg.Done()
				}()
			}
			wg.Wait()
			return false
		}()
		if hasStop {
			go downloadBegin()
			return
		}
	}
	// 下载漫画
	// WARNING 无限循环
	for {
		downloadingPictureThumbs, err := active.TheDownloadNeedDownloadPageThumbs(downloadingComic.ID, downloadThreadFetch)
		if err != nil {
			panic(err)
		}
		if len(downloadingPictureThumbs) == 0 {
			break
		}
		// 多线程下载漫画
		hasStop := func() bool {
			channel := make(chan int, downloadThreadCount)
			defer close(channel)
			wg := sync.WaitGroup{}
			for i := 0; i < len(downloadingPictureThumbs); i++ {
				// 暂停检测
				if downloadHasStop() {
					wg.Wait()
					return true
				}
				channel <- 0
				wg.Add(1)
				// 不放入携程, 防止i已经变化
				pagePoint := &(downloadingPictureThumbs[i])
				go func() {
					//
					buff, err := downloadDecodeUrl(pagePoint.Url)
					if buff != nil {
						pageThumbName := fmt.Sprintf("p_%d_t", pagePoint.PageIndex)
						relativePageThumbName := path.Join(relativeFolder, pageThumbName)
						absolutePageThumbName := path.Join(absoluteFolder, pageThumbName)
						err = ioutil.WriteFile(absolutePageThumbName, buff, constant.CreateFileMode)
						if err != nil {
							panic(err)
						}
						err = active.SaveDownloadPageThumbStatus(downloadingComic.ID, pagePoint.PageIndex, 1, relativePageThumbName)
						if err != nil {
							panic(err)
						}
					} else {
						err = active.SaveDownloadPageThumbStatus(downloadingComic.ID, pagePoint.PageIndex, 2, "")
						if err != nil {
							panic(err)
						}
					}
					//
					<-channel
					wg.Done()
				}()
			}
			wg.Wait()
			return false
		}()
		if hasStop {
			go downloadBegin()
			return
		}
	}
	// 总结下载进度
	if active.DownloadCoverOk(downloadingComic.ID) && active.DownloadCoverThumbOk(downloadingComic.ID) &&
		active.DownloadPageOk(downloadingComic.ID) && active.DownloadPageThumbOk(downloadingComic.ID) {
		err := active.SaveDownloadStatus(downloadingComic.ID, 1)
		if err != nil {
			panic(err)
		}
	} else {
		err := active.SaveDownloadStatus(downloadingComic.ID, 2)
		if err != nil {
			panic(err)
		}
	}
	go downloadBegin()
}

func downloadDecodeUrl(url string) ([]byte, error) {
	buff, err := decodeFromRemote(url)
	if buff != nil {
		return buff, nil
	}
	for i := 0; i < 5; i++ {
		buff, err = decodeFromUrl(url)
		if err != nil {
			continue
		}
		if buff != nil {
			return buff, nil
		}
	}
	return nil, err
}


================================================
FILE: go/nhentai/locks.go
================================================
package nhentai

import (
	"hash/fnv"
	"sync"
)

var hashMutex []*sync.Mutex

func init() {
	for i := 0; i < 32; i++ {
		hashMutex = append(hashMutex, &sync.Mutex{})
	}
}

// HashLock Hash一样的图片不同时处理
func HashLock(key string) *sync.Mutex {
	hash := fnv.New32()
	hash.Write([]byte(key))
	return hashMutex[int(hash.Sum32()%uint32(len(hashMutex)))]
}




================================================
FILE: go/nhentai/nhentai.go
================================================
package nhentai

import (
	"encoding/json"
	"github.com/niuhuan/nhentai-go"
	"github.com/pkg/errors"
	"io/ioutil"
	"nhentai/nhentai/constant"
	"nhentai/nhentai/database/active"
	"nhentai/nhentai/database/cache"
	"nhentai/nhentai/database/properties"
	"path"
	"strconv"
)

var initFlag bool
var cachePath string
var downloadPath string

func InitNHentai(documentDir string) {
	if initFlag {
		return
	}
	initFlag = true
	databaseDir := path.Join(documentDir, "database")
	constant.ObtainDir(databaseDir)
	properties.Init(databaseDir)
	cache.Init(databaseDir)
	active.Init(databaseDir)
	cachePath = path.Join(documentDir, "cache")
	constant.ObtainDir(cachePath)
	downloadPath = path.Join(documentDir, "download")
	constant.ObtainDir(downloadPath)
	initClient()
	go initDownload()
}

func cacheImagePath(aliasPath string) string {
	return path.Join(cachePath, aliasPath)
}

var methods = map[string]func(string) (string, error){
	"availableWebAddresses":      availableWebAddresses,
	"availableImgAddresses":      availableImgAddresses,
	"setProxy":                   setProxy,
	"getProxy":                   getProxy,
	"comics":                     comics,
	"comicsByTagName":            comicsByTagName,
	"comicsBySearchRaw":          comicsBySearchRaw,
	"comicInfo":                  comicInfo,
	"cacheImageByUrlPath":        cacheImageByUrlPath,
	"loadProperty":               loadProperty,
	"saveProperty":               saveProperty,
	"saveViewInfo":               saveViewInfo,
	"saveViewIndex":              saveViewIndex,
	"loadLastViewIndexByComicId": loadLastViewIndexByComicId,
	"downloadComic":              downloadComic,
	"hasDownload":                hasDownload,
	"listDownloadComicInfo":      listDownloadComicInfo,
	"downloadSetDelete":          downloadSetDelete,
	"httpGet":                    httpGet,
	"convertImageToJPEG100":      convertImageToJPEG100,
	"setCookie":                  setCookie,
	"setUserAgent":               setUserAgent,
}

func setUserAgent(s string) (string, error) {
	client.UserAgent = s
	return "", nil
}

func setCookie(s string) (string, error) {
	client.Cookie = s
	return "", nil
}

func FlatInvoke(method string, params string) (string, error) {
	if method, ok := methods[method]; ok {
		return method(params)
	}
	return "", errors.New("method not found (nhentai main)")
}

func saveProperty(params string) (string, error) {
	var paramsStruct struct {
		Name  string `json:"name"`
		Value string `json:"value"`
	}
	json.Unmarshal([]byte(params), &paramsStruct)
	return "", properties.SaveProperty(paramsStruct.Name, paramsStruct.Value)
}

func loadProperty(params string) (string, error) {
	var paramsStruct struct {
		Name         string `json:"name"`
		DefaultValue string `json:"defaultValue"`
	}
	json.Unmarshal([]byte(params), &paramsStruct)
	return properties.LoadProperty(paramsStruct.Name, paramsStruct.DefaultValue)
}

func saveViewInfo(params string) (string, error) {
	var comic nhentai.ComicInfo
	json.Unmarshal([]byte(params), &comic)
	return "", active.SaveViewInfo(comic)
}

func saveViewIndex(params string) (string, error) {
	var paramsStruct struct {
		Info  nhentai.ComicInfo `json:"info"`
		Index int               `json:"index"`
	}
	json.Unmarshal([]byte(params), &paramsStruct)
	return "", active.SaveViewIndex(paramsStruct.Info, paramsStruct.Index)
}

func loadLastViewIndexByComicId(params string) (string, error) {
	comicId, err := strconv.Atoi(params)
	if err != nil {
		return "", err
	}
	return serialize(active.LoadLastViewIndexByComicId(comicId))
}

func downloadComic(params string) (string, error) {
	var comic nhentai.ComicInfo
	json.Unmarshal([]byte(params), &comic)
	return "", active.CreateDownload(comic)
}

func hasDownload(params string) (string, error) {
	comicId, err := strconv.Atoi(params)
	if err != nil {
		return "", err
	}
	return strconv.FormatBool(active.HasDownload(comicId)), nil
}

func listDownloadComicInfo(s string) (string, error) {
	return serialize(active.ListDownloadComicInfo(), nil)
}

func downloadSetDelete(params string) (string, error) {
	comicId, err := strconv.Atoi(params)
	if err != nil {
		return "", err
	}
	active.MarkComicDeleting(comicId)
	downloadRestart = true
	return "", nil
}

func httpGet(url string) (string, error) {
	rsp, err := client.Get(url)
	if err != nil {
		return "", err
	}
	defer rsp.Body.Close()
	buff, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		return "", err
	}
	return string(buff), nil
}


================================================
FILE: go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>English</string>
        <key>CFBundleExecutable</key>
        <string>{{.executableName}}</string>
        <key>CFBundleGetInfoString</key>
        <string>{{.description}}</string>
        <key>CFBundleIconFile</key>
        <string>icon.icns</string>
        <key>NSHighResolutionCapable</key>
        <true/>
        <key>CFBundleIdentifier</key>
        <string>{{.organizationName}}.{{.packageName}}</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleLongVersionString</key>
        <string>{{.version}}</string>
        <key>CFBundleName</key>
        <string>{{.applicationName}}</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
        <string>{{.version}}</string>
        <key>CFBundleSignature</key>
        <string>{{.organizationName}}.{{.packageName}}</string>
        <key>CFBundleVersion</key>
        <string>{{.version}}</string>
        <key>CSResourcesFileMapped</key>
        <true/>
        <key>NSHumanReadableCopyright</key>
        <string></string>
        <key>NSPrincipalClass</key>
        <string>NSApplication</string>
    </dict>
</plist>


================================================
FILE: go/packaging/linux-appimage/AppRun.tmpl
================================================
#!/bin/sh
cd "$(dirname "$0")"
exec ./build/{{.executableName}}


================================================
FILE: go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl
================================================
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Categories=
Comment={{.description}}
Name={{.applicationName}}
Icon={{.iconPath}}
Exec={{.executablePath}}


================================================
FILE: ios/.gitignore
================================================
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*

# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3


================================================
FILE: ios/Flutter/AppFrameworkInfo.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>en</string>
  <key>CFBundleExecutable</key>
  <string>App</string>
  <key>CFBundleIdentifier</key>
  <string>io.flutter.flutter.app</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>App</string>
  <key>CFBundlePackageType</key>
  <string>FMWK</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleSignature</key>
  <string>????</string>
  <key>CFBundleVersion</key>
  <string>1.0</string>
  <key>MinimumOSVersion</key>
  <string>11.0</string>
</dict>
</plist>


================================================
FILE: ios/Flutter/Debug.xcconfig
================================================
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"


================================================
FILE: ios/Flutter/Release.xcconfig
================================================
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"


================================================
FILE: ios/Podfile
================================================
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end


================================================
FILE: ios/Runner/AppDelegate.swift
================================================
import UIKit
import Flutter
import Mobile

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
      
      let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
      let applicationSupportsPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]

      MobileMigration(documentsPath, applicationSupportsPath)
      MobileInitApplication(applicationSupportsPath)
    
    let controller = self.window.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel.init(name: "nhentai", binaryMessenger: controller as! FlutterBinaryMessenger)
    
    channel.setMethodCallHandler { (call, result) in
        Thread {
            if call.method == "flatInvoke" {
                if let args = call.arguments as? Dictionary<String, Any>,
                   let method = args["method"] as? String,
                   let params = args["params"] as? String{
                    var error: NSError?
                    let data = MobileFlatInvoke(method, params, &error)
                    if error != nil {
                        result(FlutterError(code: "", message: error?.localizedDescription, details: ""))
                    }else{
                        result(data)
                    }
                }else{
                    result(FlutterError(code: "", message: "params error", details: ""))
                }
            }
            else if call.method == "saveFileToImage"{
                if let args = call.arguments as? Dictionary<String, Any>,
                   let path = args["path"] as? String{
                    
                    do {
                        let fileURL: URL = URL(fileURLWithPath: path)
                            let imageData = try Data(contentsOf: fileURL)
                        
                        if let uiImage = UIImage(data: imageData) {
                            UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
                            result("OK")
                        }else{
                            result(FlutterError(code: "", message: "Error loading image ", details: ""))
                        }
                        
                    } catch {
                            result(FlutterError(code: "", message: "Error loading image : \(error)", details: ""))
                    }
                    
                }else{
                    result(FlutterError(code: "", message: "params error", details: ""))
                }
            }
            else{
                result(FlutterMethodNotImplemented)
            }
        }.start()
    }
    
    
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}


================================================
FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "size" : "20x20",
      "idiom" : "iphone",
      "filename" : "Icon-App-20x20@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "20x20",
      "idiom" : "iphone",
      "filename" : "Icon-App-20x20@3x.png",
      "scale" : "3x"
    },
    {
      "size" : "29x29",
      "idiom" : "iphone",
      "filename" : "Icon-App-29x29@1x.png",
      "scale" : "1x"
    },
    {
      "size" : "29x29",
      "idiom" : "iphone",
      "filename" : "Icon-App-29x29@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "29x29",
      "idiom" : "iphone",
      "filename" : "Icon-App-29x29@3x.png",
      "scale" : "3x"
    },
    {
      "size" : "40x40",
      "idiom" : "iphone",
      "filename" : "Icon-App-40x40@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "40x40",
      "idiom" : "iphone",
      "filename" : "Icon-App-40x40@3x.png",
      "scale" : "3x"
    },
    {
      "size" : "60x60",
      "idiom" : "iphone",
      "filename" : "Icon-App-60x60@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "60x60",
      "idiom" : "iphone",
      "filename" : "Icon-App-60x60@3x.png",
      "scale" : "3x"
    },
    {
      "size" : "20x20",
      "idiom" : "ipad",
      "filename" : "Icon-App-20x20@1x.png",
      "scale" : "1x"
    },
    {
      "size" : "20x20",
      "idiom" : "ipad",
      "filename" : "Icon-App-20x20@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "29x29",
      "idiom" : "ipad",
      "filename" : "Icon-App-29x29@1x.png",
      "scale" : "1x"
    },
    {
      "size" : "29x29",
      "idiom" : "ipad",
      "filename" : "Icon-App-29x29@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "40x40",
      "idiom" : "ipad",
      "filename" : "Icon-App-40x40@1x.png",
      "scale" : "1x"
    },
    {
      "size" : "40x40",
      "idiom" : "ipad",
      "filename" : "Icon-App-40x40@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "76x76",
      "idiom" : "ipad",
      "filename" : "Icon-App-76x76@1x.png",
      "scale" : "1x"
    },
    {
      "size" : "76x76",
      "idiom" : "ipad",
      "filename" : "Icon-App-76x76@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "83.5x83.5",
      "idiom" : "ipad",
      "filename" : "Icon-App-83.5x83.5@2x.png",
      "scale" : "2x"
    },
    {
      "size" : "1024x1024",
      "idiom" : "ios-marketing",
      "filename" : "Icon-App-1024x1024@1x.png",
      "scale" : "1x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}


================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
================================================
{
  "images" : [
    {
      "idiom" : "universal",
      "filename" : "LaunchImage.png",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "filename" : "LaunchImage@2x.png",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "filename" : "LaunchImage@3x.png",
      "scale" : "3x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}


================================================
FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
================================================
# Launch Screen Assets

You can customize the launch screen with your own desired assets by replacing the image files in this directory.

You 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.

================================================
FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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">
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="EHf-IW-A2E">
            <objects>
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
                        <viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
                            </imageView>
                        </subviews>
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                        <constraints>
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
                            <constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
                        </constraints>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="53" y="375"/>
        </scene>
    </scenes>
    <resources>
        <image name="LaunchImage" width="168" height="185"/>
    </resources>
</document>


================================================
FILE: ios/Runner/Base.lproj/Main.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<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">
    <device id="retina6_0" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--Flutter View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                    </layoutGuides>
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="-26" y="-76"/>
        </scene>
    </scenes>
</document>


================================================
FILE: ios/Runner/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>UIFileSharingEnabled</key>
	<true/>
	<key>LSSupportsOpeningDocumentsInPlace</key>
	<true/>
	<key>LSApplicationCategoryType</key>
	<string>public.app-category.entertainment</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>Save images</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>Usage images</string>
	<key>CFBundleLocalizations</key>
	<array>
		<string>zh</string>
		<string>en</string>
	</array>
	<key>CFBundleDevelopmentRegion</key>
	<string>$(DEVELOPMENT_LANGUAGE)</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>nhentai</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(FLUTTER_BUILD_NAME)</string>
	<key>CFBundleSignature</key>
	<string>????</string>
	<key>CFBundleVersion</key>
	<string>$(FLUTTER_BUILD_NUMBER)</string>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UIViewControllerBasedStatusBarAppearance</key>
	<false/>
	<key>CADisableMinimumFrameDurationOnPhone</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/Runner/Runner-Bridging-Header.h
================================================
#import "GeneratedPluginRegistrant.h"


================================================
FILE: ios/Runner.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 52;
	objects = {

/* Begin PBXBuildFile section */
		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
		62952ADFC370BEF926A4F77B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */; };
		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
		DDC69A7A275E58C100118CCB /* Mobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDC69A79275E58C100118CCB /* Mobile.xcframework */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
			isa = PBXCopyFilesBuildPhase;
			buildActionMask = 2147483647;
			dstPath = "";
			dstSubfolderSpec = 10;
			files = (
			);
			name = "Embed Frameworks";
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
		4B2752C74691B678911F7767 /* 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>"; };
		5B0F53137A8E31481FC50D89 /* 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>"; };
		68115AA88E6B5269DEDB93D6 /* 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>"; };
		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
		CB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
		DDC69A79275E58C100118CCB /* Mobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Mobile.xcframework; path = ../go/mobile/lib/Mobile.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
		97C146EB1CF9000F007C117D /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				62952ADFC370BEF926A4F77B /* Pods_Runner.framework in Frameworks */,
				DDC69A7A275E58C100118CCB /* Mobile.xcframework in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		74F492A9073C29BACAC4C529 /* Pods */ = {
			isa = PBXGroup;
			children = (
				4B2752C74691B678911F7767 /* Pods-Runner.debug.xcconfig */,
				5B0F53137A8E31481FC50D89 /* Pods-Runner.release.xcconfig */,
				68115AA88E6B5269DEDB93D6 /* Pods-Runner.profile.xcconfig */,
			);
			path = Pods;
			sourceTree = "<group>";
		};
		9740EEB11CF90186004384FC /* Flutter */ = {
			isa = PBXGroup;
			children = (
				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
				9740EEB21CF90195004384FC /* Debug.xcconfig */,
				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
				9740EEB31CF90195004384FC /* Generated.xcconfig */,
			);
			name = Flutter;
			sourceTree = "<group>";
		};
		97C146E51CF9000F007C117D = {
			isa = PBXGroup;
			children = (
				DDC69A79275E58C100118CCB /* Mobile.xcframework */,
				9740EEB11CF90186004384FC /* Flutter */,
				97C146F01CF9000F007C117D /* Runner */,
				97C146EF1CF9000F007C117D /* Products */,
				74F492A9073C29BACAC4C529 /* Pods */,
				DBE6F67E3D483577BA88D014 /* Frameworks */,
			);
			sourceTree = "<group>";
		};
		97C146EF1CF9000F007C117D /* Products */ = {
			isa = PBXGroup;
			children = (
				97C146EE1CF9000F007C117D /* Runner.app */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		97C146F01CF9000F007C117D /* Runner */ = {
			isa = PBXGroup;
			children = (
				97C146FA1CF9000F007C117D /* Main.storyboard */,
				97C146FD1CF9000F007C117D /* Assets.xcassets */,
				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
				97C147021CF9000F007C117D /* Info.plist */,
				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
			);
			path = Runner;
			sourceTree = "<group>";
		};
		DBE6F67E3D483577BA88D014 /* Frameworks */ = {
			isa = PBXGroup;
			children = (
				CB240B0E3D4CEE5A30176A71 /* Pods_Runner.framework */,
			);
			name = Frameworks;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		97C146ED1CF9000F007C117D /* Runner */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
			buildPhases = (
				F3AD040E0E519A573BB3125F /* [CP] Check Pods Manifest.lock */,
				9740EEB61CF901F6004384FC /* Run Script */,
				97C146EA1CF9000F007C117D /* Sources */,
				97C146EB1CF9000F007C117D /* Frameworks */,
				97C146EC1CF9000F007C117D /* Resources */,
				9705A1C41CF9048500538489 /* Embed Frameworks */,
				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
				45B4C6EDA02A2684AB90F0C8 /* [CP] Embed Pods Frameworks */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = Runner;
			productName = Runner;
			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		97C146E61CF9000F007C117D /* Project object */ = {
			isa = PBXProject;
			attributes = {
				LastUpgradeCheck = 1300;
				ORGANIZATIONNAME = "";
				TargetAttributes = {
					97C146ED1CF9000F007C117D = {
						CreatedOnToolsVersion = 7.3.1;
						LastSwiftMigration = 1100;
					};
				};
			};
			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
			compatibilityVersion = "Xcode 9.3";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
			);
			mainGroup = 97C146E51CF9000F007C117D;
			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				97C146ED1CF9000F007C117D /* Runner */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		97C146EC1CF9000F007C117D /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputPaths = (
			);
			name = "Thin Binary";
			outputPaths = (
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
		};
		45B4C6EDA02A2684AB90F0C8 /* [CP] Embed Pods Frameworks */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputFileListPaths = (
				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
			);
			name = "[CP] Embed Pods Frameworks";
			outputFileListPaths = (
				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
			showEnvVarsInLog = 0;
		};
		9740EEB61CF901F6004384FC /* Run Script */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputPaths = (
			);
			name = "Run Script";
			outputPaths = (
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
		};
		F3AD040E0E519A573BB3125F /* [CP] Check Pods Manifest.lock */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputFileListPaths = (
			);
			inputPaths = (
				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
				"${PODS_ROOT}/Manifest.lock",
			);
			name = "[CP] Check Pods Manifest.lock";
			outputFileListPaths = (
			);
			outputPaths = (
				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "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";
			showEnvVarsInLog = 0;
		};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		97C146EA1CF9000F007C117D /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
			isa = PBXVariantGroup;
			children = (
				97C146FB1CF9000F007C117D /* Base */,
			);
			name = Main.storyboard;
			sourceTree = "<group>";
		};
		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
			isa = PBXVariantGroup;
			children = (
				97C147001CF9000F007C117D /* Base */,
			);
			name = LaunchScreen.storyboard;
			sourceTree = "<group>";
		};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
		249021D3217E4FDB00AE95B9 /* Profile */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				FRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;
				GCC_C_LANGUAGE_STANDARD = gnu99;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				SDKROOT = iphoneos;
				SUPPORTED_PLATFORMS = iphoneos;
				TARGETED_DEVICE_FAMILY = "1,2";
				VALIDATE_PRODUCT = YES;
			};
			name = Profile;
		};
		249021D4217E4FDB00AE95B9 /* Profile */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				ENABLE_BITCODE = NO;
				INFOPLIST_FILE = Runner/Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/Frameworks",
				);
				PRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_VERSION = 5.0;
				VERSIONING_SYSTEM = "apple-generic";
			};
			name = Profile;
		};
		97C147031CF9000F007C117D /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = dwarf;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				FRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;
				GCC_C_LANGUAGE_STANDARD = gnu99;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				MTL_ENABLE_DEBUG_INFO = YES;
				ONLY_ACTIVE_ARCH = YES;
				SDKROOT = iphoneos;
				TARGETED_DEVICE_FAMILY = "1,2";
			};
			name = Debug;
		};
		97C147041CF9000F007C117D /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				FRAMEWORK_SEARCH_PATHS = ../go/mobile/lib;
				GCC_C_LANGUAGE_STANDARD = gnu99;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				SDKROOT = iphoneos;
				SUPPORTED_PLATFORMS = iphoneos;
				SWIFT_COMPILATION_MODE = wholemodule;
				SWIFT_OPTIMIZATION_LEVEL = "-O";
				TARGETED_DEVICE_FAMILY = "1,2";
				VALIDATE_PRODUCT = YES;
			};
			name = Release;
		};
		97C147061CF9000F007C117D /* Debug */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				ENABLE_BITCODE = NO;
				INFOPLIST_FILE = Runner/Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/Frameworks",
				);
				PRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
				SWIFT_VERSION = 5.0;
				VERSIONING_SYSTEM = "apple-generic";
			};
			name = Debug;
		};
		97C147071CF9000F007C117D /* Release */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CLANG_ENABLE_MODULES = YES;
				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
				ENABLE_BITCODE = NO;
				INFOPLIST_FILE = Runner/Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/Frameworks",
				);
				PRODUCT_BUNDLE_IDENTIFIER = niuhuan.nhentai;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
				SWIFT_VERSION = 5.0;
				VERSIONING_SYSTEM = "apple-generic";
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				97C147031CF9000F007C117D /* Debug */,
				97C147041CF9000F007C117D /* Release */,
				249021D3217E4FDB00AE95B9 /* Profile */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				97C147061CF9000F007C117D /* Debug */,
				97C147071CF9000F007C117D /* Release */,
				249021D4217E4FDB00AE95B9 /* Profile */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */
	};
	rootObject = 97C146E61CF9000F007C117D /* Project object */;
}


================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>


================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreviewsEnabled</key>
	<false/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
   LastUpgradeVersion = "1300"
   version = "1.3">
   <BuildAction
      parallelizeBuildables = "YES"
      buildImplicitDependencies = "YES">
      <BuildActionEntries>
         <BuildActionEntry
            buildForTesting = "YES"
            buildForRunning = "YES"
            buildForProfiling = "YES"
            buildForArchiving = "YES"
            buildForAnalyzing = "YES">
            <BuildableReference
               BuildableIdentifier = "primary"
               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
               BuildableName = "Runner.app"
               BlueprintName = "Runner"
               ReferencedContainer = "container:Runner.xcodeproj">
            </BuildableReference>
         </BuildActionEntry>
      </BuildActionEntries>
   </BuildAction>
   <TestAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      shouldUseLaunchSchemeArgsEnv = "YES">
      <Testables>
      </Testables>
      <MacroExpansion>
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </MacroExpansion>
      <AdditionalOptions>
      </AdditionalOptions>
   </TestAction>
   <LaunchAction
      buildConfiguration = "Debug"
      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
      launchStyle = "0"
      useCustomWorkingDirectory = "NO"
      ignoresPersistentStateOnLaunch = "NO"
      debugDocumentVersioning = "YES"
      debugServiceExtension = "internal"
      allowLocationSimulation = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
      <AdditionalOptions>
      </AdditionalOptions>
   </LaunchAction>
   <ProfileAction
      buildConfiguration = "Profile"
      shouldUseLaunchSchemeArgsEnv = "YES"
      savedToolIdentifier = ""
      useCustomWorkingDirectory = "NO"
      debugDocumentVersioning = "YES">
      <BuildableProductRunnable
         runnableDebuggingMode = "0">
         <BuildableReference
            BuildableIdentifier = "primary"
            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
            BuildableName = "Runner.app"
            BlueprintName = "Runner"
            ReferencedContainer = "container:Runner.xcodeproj">
         </BuildableReference>
      </BuildableProductRunnable>
   </ProfileAction>
   <AnalyzeAction
      buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction
      buildConfiguration = "Release"
      revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>


================================================
FILE: ios/Runner.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:Runner.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Pods/Pods.xcodeproj">
   </FileRef>
</Workspace>


================================================
FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PreviewsEnabled</key>
	<false/>
</dict>
</plist>


================================================
FILE: l10n.yaml
================================================
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
untranslated-messages-file: desiredFileName.txt


================================================
FILE: lib/basic/channels/nhentai.dart
================================================
import 'dart:convert';
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:nhentai/basic/entities/entities.dart';

const nHentai = NHentai._();

class NHentai {
  const NHentai._();

  static const _channel = MethodChannel("nhentai");

  /// 平铺调用, 为了直接与golang进行通信
  Future<String> _flatInvoke(String method, dynamic params) async {
    return await _channel.invokeMethod("flatInvoke", {
      "method": method,
      "params": params is String ? params : jsonEncode(params),
    });
  }

  /// 设置代理
  Future setProxy(String proxyUrl) {
    return _flatInvoke("setProxy", proxyUrl);
  }

  /// 获取代理
  Future<String> getProxy() {
    return _flatInvoke("getProxy", "");
  }

  /// 获取漫画
  Future<ComicPageData> comics(int page) async {
    return ComicPageData.fromJson(
      jsonDecode(await _flatInvoke("comics", "$page")),
    );
  }

  /// 获取漫画
  Future<ComicPageData> comicsByTagName(String tagName, int page) async {
    return ComicPageData.fromJson(jsonDecode(
      await _flatInvoke("comicsByTagName", {
        "tag_name": tagName,
        "page": page,
      }),
    ));
  }

  /// 获取漫画
  Future<ComicPageData> comicsBySearchRaw(String raw, int page) async {
    return ComicPageData.fromJson(jsonDecode(
      await _flatInvoke("comicsBySearchRaw", {
        "raw": raw,
        "page": page,
      }),
    ));
  }

  /// 漫画详情
  Future<ComicInfo> comicInfo(int comicId) async {
    return ComicInfo.formJson(jsonDecode(
      await _flatInvoke("comicInfo", "$comicId"),
    ));
  }

  /// 加载图片 (返回路径)
  Future<String> cacheImageByUrlPath(String url) async {
    return await _flatInvoke("cacheImageByUrlPath", url);
  }

  /// 手机端保存图片
  Future saveFileToImage(String path) async {
    return _channel.invokeMethod("saveFileToImage", {
      "path": path,
    });
  }

  /// 桌面端保存图片
  Future convertImageToJPEG100(String path, String dir) async {
    return _flatInvoke("convertImageToJPEG100", {
      "path": path,
      "dir": dir,
    });
  }

  /// 安卓版本号
  Future<int> androidVersion() async {
    if (Platform.isAndroid) {
      return await _channel.invokeMethod("androidVersion", {});
    }
    return 0;
  }

  /// 读取配置文件
  Future<String> loadProperty(String propertyName, String defaultValue) async {
    return await _flatInvoke("loadProperty", {
      "name": propertyName,
      "defaultValue": defaultValue,
    });
  }

  /// 保存配置文件
  Future<dynamic> saveProperty(String propertyName, String value) {
    return _flatInvoke("saveProperty", {
      "name": propertyName,
      "value": value,
    });
  }

  /// 更新浏览记录 (打开详情页面时)
  Future<dynamic> saveViewInfo(ComicInfo info) {
    return _flatInvoke("saveViewInfo", info);
  }

  /// 更新浏览记录 (打开页面滚动时)
  Future<dynamic> saveViewIndex(ComicInfo info, int index) {
    return _flatInvoke("saveViewIndex", {
      "info": info,
      "index": index,
    });
  }

  /// 获取最后浏览的页数
  Future<int> loadLastViewIndexByComicId(int comicId) async {
    return int.parse(await _flatInvoke("loadLastViewIndexByComicId", comicId));
  }

  /// 创建一个下载
  Future<dynamic> downloadComic(ComicInfo comicInfo) {
    return _flatInvoke("downloadComic", comicInfo);
  }

  /// 检查有没有下载过
  Future<bool> hasDownload(int comicId) async {
    return "true" == await _flatInvoke("hasDownload", "$comicId");
  }

  /// 获取所有下载列表
  Future<List<DownloadComicInfo>> listDownloadComicInfo() async {
    return List.of(jsonDecode(await _flatInvoke("listDownloadComicInfo", "")))
        .map((e) => DownloadComicInfo.formJson(e))
        .toList()
        .cast<DownloadComicInfo>();
  }

  /// 删除一个下载
  Future<dynamic> downloadSetDelete(int comicId) async {
    return _flatInvoke("downloadSetDelete", "$comicId");
  }

  /// HTTP-GET-STRING
  Future<String> httpGet(String url) async {
    return await _flatInvoke("httpGet", url);
  }

  Future setCookie(String cookies) async {
    return await _flatInvoke("setCookie", cookies);
  }

  Future setUserAgent(String s) async {
    return await _flatInvoke("setUserAgent", s);
  }
}


================================================
FILE: lib/basic/common/common.dart
================================================
import 'package:app_links/app_links.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart';

/// 显示一个toast
void defaultToast(BuildContext context, String title) {
  showToast(
    title,
    context: context,
    position: StyledToastPosition.center,
    animation: StyledToastAnimation.scale,
    reverseAnimation: StyledToastAnimation.fade,
    duration: const Duration(seconds: 4),
    animDuration: const Duration(seconds: 1),
    curve: Curves.elasticOut,
    reverseCurve: Curves.linear,
  );
}

/// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true
Future<bool> confirmDialog(BuildContext context, String title,
    {String content = ""}) async {
  return await showDialog(
          context: context,
          builder: (context) => AlertDialog(
                title: Text(title),
                content: SingleChildScrollView(
                  child: ListBody(
                    children: content == ""
                        ? []
                        : <Widget>[
                            Text(content),
                          ],
                  ),
                ),
                actions: <Widget>[
                  MaterialButton(
                    child: Text(AppLocalizations.of(context)!.cancel),
                    onPressed: () {
                      Navigator.of(context).pop(false);
                    },
                  ),
                  MaterialButton(
                    child: Text(AppLocalizations.of(context)!.ok),
                    onPressed: () {
                      Navigator.of(context).pop(true);
                    },
                  ),
                ],
              )) ??
      false;
}

/// 显示一个消息提示框
Future alertDialog(BuildContext context, String title, String content) {
  return showDialog(
      context: context,
      builder: (context) => AlertDialog(
            title: Text(title),
            content: SingleChildScrollView(
              child: ListBody(
                children: <Widget>[
                  Text(content),
                ],
              ),
            ),
            actions: <Widget>[
              MaterialButton(
                child: Text('确定'),
                onPressed: () {
                  Navigator.of(context).pop();
                },
              ),
            ],
          ));
}

/// stream-filter的替代方法
List<T> filteredList<T>(List<T> list, bool Function(T) filter) {
  List<T> result = [];
  list.forEach((element) {
    if (filter(element)) {
      result.add(element);
    }
  });
  return result;
}

/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容
Future<T?> chooseListDialog<T>(
    BuildContext context, String title, List<T> items,
    {String? tips}) async {
  List<Widget> widgets = [];
  if (tips != null) {
    widgets.add(Container(
      padding: const EdgeInsets.fromLTRB(15, 5, 15, 15),
      child: Text(tips),
    ));
  }
  widgets.addAll(items.map((e) => SimpleDialogOption(
        onPressed: () {
          Navigator.of(context).pop(e);
        },
        child: Text('$e'),
      )));

  return showDialog<T>(
    context: context,
    builder: (BuildContext context) {
      return SimpleDialog(
        title: Text(title),
        children: widgets,
      );
    },
  );
}

/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value)
Future<T?> chooseMapDialog<T>(
    BuildContext buildContext, Map<String, T> values, String title) async {
  return await showDialog<T>(
    context: buildContext,
    builder: (BuildContext context) {
      return SimpleDialog(
        title: Text(title),
        children: values.entries
            .map((e) => SimpleDialogOption(
                  child: Text(e.key),
                  onPressed: () {
                    Navigator.of(context).pop(e.value);
                  },
                ))
            .toList(),
      );
    },
  );
}

/// 输入对话框1

var _controller = TextEditingController.fromValue(TextEditingValue(text: ''));

Future<String?> displayTextInputDialog(
  BuildContext context,
  String title,
  String hint,
  String src,
  String desc,
) {
  _controller.text = src;
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text(title),
        content: SingleChildScrollView(
          child: ListBody(
            children: [
              TextField(
                controller: _controller,
                decoration: InputDecoration(hintText: hint),
              ),
              desc.isEmpty
                  ? Container()
                  : Container(
                      padding: const EdgeInsets.only(top: 20, bottom: 10),
                      child: Text(
                        desc,
                        style: TextStyle(
                            fontSize: 12,
                            color: Theme.of(context)
                                .textTheme
                                .bodyText1
                                ?.color
                                ?.withOpacity(.5)),
                      ),
                    ),
            ],
          ),
        ),
        actions: <Widget>[
          MaterialButton(
            child: const Text('取消'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
          MaterialButton(
            child: const Text('确认'),
            onPressed: () {
              Navigator.of(context).pop(_controller.text);
            },
          ),
        ],
      );
    },
  );
}

/// 将字符串前面加0直至满足len位
String add0(int num, int len) {
  var rsp = "$num";
  while (rsp.length < len) {
    rsp = "0$rsp";
  }
  return rsp;
}

/// 格式化时间 2012-34-56
String formatTimeToDate(String str) {
  try {
    var c = DateTime.parse(str);
    return "${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)}";
  } catch (e) {
    return "-";
  }
}
//
// /// 格式化时间 2012-34-56 12:34:56
// String formatTimeToDateTime(String str) {
//   try {
//     var c = DateTime.parse(str).add(Duration(hours: currentTimeOffsetHour()));
//     return "${add0(c.year, 4)}-${add0(c.month, 2)}-${add0(c.day, 2)} ${add0(c.hour, 2)}:${add0(c.minute, 2)}";
//   } catch (e) {
//     return "-";
//   }
// }

/// 输入对话框2

final TextEditingController _textEditController =
    TextEditingController(text: '');

Future<String?> inputString(BuildContext context, String title,
    {String hint = ""}) async {
  _textEditController.clear();
  return showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        content: Card(
          child: SingleChildScrollView(
            child: ListBody(
              children: [
                Text(title),
                Container(
                  child: TextField(
                    controller: _textEditController,
                    decoration: new InputDecoration(
                      labelText: "$hint",
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
        actions: <Widget>[
          MaterialButton(
            onPressed: () {
              Navigator.pop(context);
            },
            child: Text('取消'),
          ),
          MaterialButton(
            onPressed: () {
              Navigator.pop(context, _textEditController.text);
            },
            child: Text('确定'),
          ),
        ],
      );
    },
  );
}


================================================
FILE: lib/basic/common/cross.dart
================================================
/// 与平台交互的操作

import 'dart:io';
import 'package:clipboard/clipboard.dart';
import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/material.dart';
import 'package:nhentai/basic/channels/nhentai.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'common.dart';

/// 复制内容到剪切板
void copyToClipBoard(BuildContext context, String string) {
  if (Platform.isWindows || Platform.isMacOS) {
    FlutterClipboard.copy(string);
    defaultToast(context, "已复制到剪切板");
  } else if (Platform.isAndroid) {
    FlutterClipboard.copy(string);
    defaultToast(context, "已复制到剪切板");
  }
}

/// 打开web页面
Future<dynamic> openUrl(String url) async {
  if (await canLaunch(url)) {
    await launch(
      url,
      forceSafariVC: false,
    );
  }
}

/// 保存图片
Future<dynamic> saveImage(String path, BuildContext context) async {
  Future? future;
  if (Platform.isIOS) {
    future = nHentai.saveFileToImage(path);
  } else if (Platform.isAndroid) {
    future = _saveImageAndroid(path, context);
  } else if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
    String? folder = await chooseFolder(context);
    if (folder != null) {
      future = nHentai.convertImageToJPEG100(path, folder);
    }
  } else {
    defaultToast(context, '暂不支持该平台');
    return;
  }
  if (future == null) {
    defaultToast(context, '保存取消');
    return;
  }
  try {
    await future;
    defaultToast(context, '保存成功');
  } catch (e, s) {
    print("$e\n$s");
    defaultToast(context, '保存失败');
  }
}

/// 保存图片且保持静默, 用于批量导出到相册
Future<dynamic> saveImageQuiet(String path, BuildContext context) async {
  if (Platform.isIOS) {
    return nHentai.saveFileToImage(path);
  } else if (Platform.isAndroid) {
    return _saveImageAndroid(path, context);
  } else {
    throw Exception("only mobile");
  }
}

Future<dynamic> _saveImageAndroid(String path, BuildContext context) async {
  var p = await Permission.storage.request();
  if (!p.isGranted) {
    return;
  }
  return nHentai.saveFileToImage(path);
}

/// 选择一个文件夹用于保存文件
Future<String?> chooseFolder(BuildContext context) async {
  return FilesystemPicker.open(
    title: '选择一个文件夹',
    pickText: '将文件保存到这里',
    context: context,
    fsType: FilesystemType.folder,
    rootDirectory: Directory(await currentChooserRoot()),
  );
}

/// 复制对话框
void confirmCopy(BuildContext context, String content) async {
  if (await confirmDialog(context, "复制", content: content)) {
    copyToClipBoard(context, content);
  }
}

Future<String> currentChooserRoot() async {
  if (Platform.isAndroid) {
    if (await nHentai.androidVersion() >= 30) {
      if (!(await Permission.manageExternalStorage.request()).isGranted) {
        throw Exception("申请权限被拒绝");
      }
    } else {
      if (!(await Permission.storage.request()).isGranted) {
        throw Exception("申请权限被拒绝");
      }
    }
  }
  if (Platform.isWindows) {
    return '/';
  } else if (Platform.isMacOS) {
    return '/Users';
  } else if (Platform.isLinux) {
    return '/';
  } else if (Platform.isAndroid) {
    return '/storage/emulated/0';
  } else {
    throw 'error';
  }
}


================================================
FILE: lib/basic/common/error_types.dart
================================================
const ERROR_TYPE_NETWORK = "NETWORK_ERROR";
const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR";
const ERROR_TYPE_TIME = "TIME_ERROR";
const ERROR_TYPE_UNDER_REVIEW = "UNDER_VIEW_ERROR";

// 错误的类型, 方便照展示和谐的提示
String errorType(String error) {
  // EXCEPTION
  // Get "https://****": net/http: TLS handshake timeout
  // Get "https://****": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused
  // Get "https://****": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
  if (error.contains("timeout") ||
      error.contains("connection refused") ||
      error.contains("deadline") ||
      error.contains("connection abort")) {
    return ERROR_TYPE_NETWORK;
  }
  if (error.contains("permission denied")) {
    return ERROR_TYPE_PERMISSION;
  }
  if (error.contains("time is not synchronize")) {
    return ERROR_TYPE_TIME;
  }
  if (error.contains("under review")) {
    return ERROR_TYPE_UNDER_REVIEW;
  }
  return "";
}


================================================
FILE: lib/basic/configs/proxy.dart
================================================
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:nhentai/basic/channels/nhentai.dart';
import 'package:nhentai/basic/common/common.dart';

const _propertyName = "proxy";
late String _proxy;

Future initProxy() async {
  _proxy = await nHentai.getProxy();
}

String currentProxy() {
  return _proxy;
}

Future chooseProxy(BuildContext context) async {
  final newProxy = await displayTextInputDialog(
      context,
      AppLocalizations.of(context)!.proxy,
      "socks5://host:port/",
      _proxy,
      AppLocalizations.of(context)!.inputProxyDesc);
  if (newProxy != null) {
    await nHentai.saveProperty(_propertyName, newProxy);
    _proxy = newProxy;
  }
}

Widget proxySetting() {
  return StatefulBuilder(
    builder: (BuildContext context, void Function(void Function()) setState) {
      return ListTile(
        title: Text(
          AppLocalizations.of(context)!.proxy,
        ),
        subtitle: Text(_proxy),
        onTap: () async {
          await chooseProxy(context);
          setState(() {});
        },
      );
    },
  );
}


================================================
FILE: lib/basic/configs/reader_direction.dart
================================================
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:nhentai/basic/channels/nhentai.dart';
import 'package:nhentai/basic/common/common.dart';

enum ReaderDirection {
  topToBottom,
  leftToRight,
  rightToLeft,
}

const _propertyName = "readerDirection";
late ReaderDirection _readerDirection;

Future initReaderDirection() async {
  _readerDirection = _fromString(await nHentai.loadProperty(_propertyName, ""));
}

ReaderDirection _fromString(String valueForm) {
  for (var value in ReaderDirection.values) {
    if (value.toString() == valueForm) {
      return value;
    }
  }
  return ReaderDirection.values.first;
}

ReaderDirection currentReaderDirection() {
  return _readerDirection;
}

String readerDirectionName(ReaderDirection direction, BuildContext context) {
  switch (direction) {
    case ReaderDirection.topToBottom:
      return AppLocalizations.of(context)!.topToBottom;
    case ReaderDirection.leftToRight:
      return AppLocalizations.of(context)!.leftToRight;
    case ReaderDirection.rightToLeft:
      return AppLocalizations.of(context)!.rightToLeft;
  }
}

Future chooseReaderDirection(BuildContext context) async {
  final Map<String, ReaderDirection> map = {};
  for (var element in ReaderDirection.values) {
    map[readerDirectionName(element, context)] = element;
  }
  final newReaderDirection = await chooseMapDialog(
    context,
    map,
    AppLocalizations.of(context)!.chooseReaderDirection,
  );
  if (newReaderDirection != null) {
    await nHentai.saveProperty(_propertyName, "$newReaderDirection");
    _readerDirection = newReaderDirection;
  }
}


================================================
FILE: lib/basic/configs/reader_type.dart
================================================
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:nhentai/basic/channels/nhentai.dart';
import 'package:nhentai/basic/common/common.dart';

enum ReaderType {
  webtoon,
  gallery,
}

const _propertyName = "readerType";
late ReaderType _readerType;

Future initReaderType() async {
  _readerType = _fromString(await nHentai.loadProperty(_propertyName, ""));
}

ReaderType _fromString(String valueForm) {
  for (var value in ReaderType.values) {
    if (value.toString() == valueForm) {
      return value;
    }
  }
  return ReaderType.values.first;
}

ReaderType currentReaderType() {
  return _readerType;
}

String readerTypeName(ReaderType type, BuildContext context) {
  switch (type) {
    case ReaderType.webtoon:
      return AppLocalizations.of(context)!.webtoon;
    case ReaderType.gallery:
      return AppLocalizations.of(context)!.gallery;
  }
}

Future chooseReaderType(BuildContext context) async {
  final Map<String, ReaderType> map = {};
  for (var element in ReaderType.values) {
    map[readerTypeName(element, context)] = element;
  }
  final newReaderType = await chooseMapDialog(
    context,
    map,
    AppLocalizations.of(context)!.chooseReaderType,
  );
  if (newReaderType != null) {
    await nHentai.saveProperty(_propertyName, "$newReaderType");
    _readerType = newReaderType;
  }
}


================================================
FILE: lib/basic/configs/themes.dart
================================================
/// 主题

import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:event/event.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nhentai/basic/channels/nhentai.dart';
import 'package:nhentai/basic/common/common.dart';


// 字体相关

const _fontFamilyProperty = "fontFamily";

String? _fontFamily;

Future initFont() async {
  var defaultFont = "";
  _fontFamily = await nHentai.loadProperty(_fontFamilyProperty, defaultFont);
}

ThemeData _fontThemeData(bool dark) {
  return ThemeData(
    brightness: dark ? Brightness.dark : Brightness.light,
    fontFamily: _fontFamily == "" ? null : _fontFamily,
  );
}

Future<void> inputFont(BuildContext context) async {
  var font = await displayTextInputDialog(
    context, "字体", "请输入字体", "$_fontFamily",
    "请输入字体的名称, 例如宋体/黑体, 如果您保存后没有发生变化, 说明字体无法使用或名称错误, 可以去参考C:\\Windows\\Fonts寻找您的字体。",
  );
  if (font != null) {
    await nHentai.saveProperty(_fontFamilyProperty, font);
    _fontFamily = font;
    _changeThemeByCode(_themeCode);
  }
}

Widget fontSetting() {
  return StatefulBuilder(
    builder: (BuildContext context, void Function(void Function()) setState) {
      return ListTile(
        title: Text("字体"),
        subtitle: Text("$_fontFamily"),
        onTap: () async {
          await inputFont(context);
          setState(() {});
        },
      );
    },
  );
}

// 主题相关

// 主题包
abstract class _ThemePackage {
  String code();

  String name();

  ThemeData themeData(ThemeData rawData);
}

class _OriginTheme extends _ThemePackage {
  @override
  String code() => "origin";

  @override
  String name() => "ORIGIN";

  @override
  ThemeData themeData(ThemeData rawData) => rawData;
}

class _PinkTheme extends _ThemePackage {
  @override
  String code() => "pink";

  @override
  String name() => "PINK";

  @override
  ThemeData themeData(ThemeData rawData) =>
      rawData.copyWith(
        brightness: Brightness.light,
        colorScheme: ColorScheme.light(
          secondary: Colors.pink.shade200,
        ),
        appBarTheme: AppBarTheme(
          systemOverlayStyle: SystemUiOverlayStyle.light,
          color: Colors.pink.shade200,
          iconTheme: const IconThemeData(
            color: Colors.white,
          ),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          selectedItemColor: Colors.pink[300],
          unselectedItemColor: Colors.grey[500],
        ),
        dividerColor: Colors.grey.shade200,
        primaryColor: Colors.pink.shade200,
        textSelectionTheme: TextSelectionThemeData(
          cursorColor: Colors.pink.shade200,
          selectionColor: Colors.pink.shade300.withAlpha(150),
          selectionHandleColor: Colors.pink.shade300.withAlpha(200),
        ),
        inputDecorationTheme: InputDecorationTheme(
          focusedBorder: UnderlineInputBorder(
            borderSide: BorderSide(color: Colors.pink.shade200),
          ),
        ),
      );
}

class _BlackTheme extends _ThemePackage {
  @override
  String code() => "black";

  @override
  String name() => "BLACK";

  @override
  ThemeData themeData(ThemeData rawData) =>
      rawData.copyWith(
        brightness: Brightness.light,
        colorScheme: ColorScheme.light(
          secondary: Colors.pink.shade200,
        ),
        appBarTheme: AppBarTheme(
          systemOverlayStyle: SystemUiOverlayStyle.light,
          color: Colors.grey.shade800,
          iconTheme: const IconThemeData(
            color: Colors.white,
          ),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          selectedItemColor: Colors.white,
          unselectedItemColor: Colors.grey[400],
          backgroundColor: Colors.grey.shade800,
        ),
        dividerColor: Colors.grey.shade200,
        primaryColor: Colors.pink.shade200,
        textSelectionTheme: TextSelectionThemeData(
          cursorColor: Colors.pink.shade200,
          selectionColor: Colors.pink.shade300.withAlpha(150),
          selectionHandleColor: Colors.pink.shade300.withAlpha(200),
        ),
        inputDecorationTheme: InputDecorationTheme(
          focusedBorder: UnderlineInputBorder(
            borderSide: BorderSide(color: Colors.pink.shade200),
          ),
        ),
      );
}

class _DarkTheme extends _ThemePackage {
  @override
  String code() => "dark";

  @override
  String name() => "DARK";

  @override
  ThemeData themeData(ThemeData rawData) =>
      rawData.copyWith(
        brightness: Brightness.light,
        colorScheme: ColorScheme.light(
          secondary: Colors.pink.shade200,
        ),
        appBarTheme: const AppBarTheme(
          systemOverlayStyle: SystemUiOverlayStyle.light,
          color: Color(0xFF1E1E1E),
          iconTheme: IconThemeData(
            color: Colors.white,
          ),
        ),
        bottomNavigationBarTheme: BottomNavigationBarThemeData(
          selectedItemColor: Colors.white,
          unselectedItemColor: Colors.grey.shade300,
          backgroundColor: Colors.grey.shade900,
        ),
        primaryColor: Colors.pink.shade200,
        textSelectionTheme: TextSelectionThemeData(
          cursorColor: Colors.pink.shade200,
          selectionColor: Colors.pink.shade300.withAlpha(150),
          selectionHandleColor: Colors.pink.shade300.withAlpha(200),
        ),
        inputDecorationTheme: InputDecorationTheme(
          focusedBorder: UnderlineInputBorder(
            borderSide: BorderSide(color: Colors.pink.shade200),
          ),
        ),
      );
}

class _DustyBlueTheme extends _ThemePackage {
  @override
  String code() => "dustyBlue";

  @override
  String name() => "DUSTY BLUE";

  @override
  ThemeData themeData(ThemeData rawData) =>
      rawData.copyWith(
        scaffoldBackgroundColor: Colo
Download .txt
gitextract_vgtt9c51/

├── .github/
│   └── workflows/
│       └── Release.yml
├── .gitignore
├── .metadata
├── LICENSE
├── README-zh.md
├── README.md
├── analysis_options.yaml
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── build.gradle
│   │   └── src/
│   │       ├── debug/
│   │       │   └── AndroidManifest.xml
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── kotlin/
│   │       │   │   └── niuhuan/
│   │       │   │       └── nhentai/
│   │       │   │           └── MainActivity.kt
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── launch_background.xml
│   │       │       ├── drawable-v21/
│   │       │       │   └── launch_background.xml
│   │       │       ├── values/
│   │       │       │   └── styles.xml
│   │       │       └── values-night/
│   │       │           └── styles.xml
│   │       └── profile/
│   │           └── AndroidManifest.xml
│   ├── build.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   └── settings.gradle
├── ci/
│   ├── cmd/
│   │   ├── check_asset/
│   │   │   └── main.go
│   │   ├── check_release/
│   │   │   └── main.go
│   │   └── upload_asset/
│   │       └── main.go
│   ├── commons/
│   │   └── types.go
│   ├── go.mod
│   ├── linux_font.yaml
│   ├── version.code.txt
│   └── version.info.txt
├── go/
│   ├── .gitignore
│   ├── cmd/
│   │   ├── init.go
│   │   ├── main.go
│   │   ├── options.go
│   │   └── plugin.go
│   ├── go.mod
│   ├── go.sum
│   ├── hover.yaml
│   ├── mobile/
│   │   ├── bind-android-debug.sh
│   │   ├── bind-android.sh
│   │   ├── bind-ios-debug.sh
│   │   ├── bind-ios.sh
│   │   ├── lib/
│   │   │   └── .keep
│   │   └── mobile.go
│   ├── nhentai/
│   │   ├── client.go
│   │   ├── common.go
│   │   ├── constant/
│   │   │   ├── constant.go
│   │   │   ├── os.go
│   │   │   └── time.go
│   │   ├── database/
│   │   │   ├── active/
│   │   │   │   └── active.go
│   │   │   ├── cache/
│   │   │   │   └── cache.go
│   │   │   └── properties/
│   │   │       └── properties.go
│   │   ├── decodes.go
│   │   ├── download.go
│   │   ├── locks.go
│   │   └── nhentai.go
│   └── packaging/
│       ├── darwin-bundle/
│       │   └── {{.applicationName}} {{.version}}.app/
│       │       └── Contents/
│       │           └── Info.plist.tmpl
│       └── linux-appimage/
│           ├── AppRun.tmpl
│           └── {{.packageName}}.desktop.tmpl
├── ios/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── AppFrameworkInfo.plist
│   │   ├── Debug.xcconfig
│   │   └── Release.xcconfig
│   ├── Podfile
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   ├── AppIcon.appiconset/
│   │   │   │   └── Contents.json
│   │   │   └── LaunchImage.imageset/
│   │   │       ├── Contents.json
│   │   │       └── README.md
│   │   ├── Base.lproj/
│   │   │   ├── LaunchScreen.storyboard
│   │   │   └── Main.storyboard
│   │   ├── Info.plist
│   │   └── Runner-Bridging-Header.h
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   ├── contents.xcworkspacedata
│   │   │   └── xcshareddata/
│   │   │       ├── IDEWorkspaceChecks.plist
│   │   │       └── WorkspaceSettings.xcsettings
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   └── Runner.xcworkspace/
│       ├── contents.xcworkspacedata
│       └── xcshareddata/
│           ├── IDEWorkspaceChecks.plist
│           └── WorkspaceSettings.xcsettings
├── l10n.yaml
├── lib/
│   ├── basic/
│   │   ├── channels/
│   │   │   └── nhentai.dart
│   │   ├── common/
│   │   │   ├── common.dart
│   │   │   ├── cross.dart
│   │   │   └── error_types.dart
│   │   ├── configs/
│   │   │   ├── proxy.dart
│   │   │   ├── reader_direction.dart
│   │   │   ├── reader_type.dart
│   │   │   ├── themes.dart
│   │   │   └── version.dart
│   │   └── entities/
│   │       └── entities.dart
│   ├── l10n/
│   │   ├── app_en.arb
│   │   └── app_zh.arb
│   ├── main.dart
│   ├── main_desktop.dart
│   └── screens/
│       ├── comic_downloads_screen.dart
│       ├── comic_info_screen.dart
│       ├── comic_reader_screen.dart
│       ├── comic_search_screen.dart
│       ├── comics_screen.dart
│       ├── components/
│       │   ├── Badged.dart
│       │   ├── actions.dart
│       │   ├── content_builder.dart
│       │   ├── content_error.dart
│       │   ├── content_loading.dart
│       │   ├── images.dart
│       │   ├── mouse_and_touch_scroll_behavior.dart
│       │   └── pager.dart
│       ├── file_photo_view_screen.dart
│       ├── init_screen.dart
│       ├── settings_screen.dart
│       └── webview_screen.dart
├── linux/
│   ├── .gitignore
│   ├── CMakeLists.txt
│   ├── flutter/
│   │   ├── CMakeLists.txt
│   │   ├── generated_plugin_registrant.cc
│   │   ├── generated_plugin_registrant.h
│   │   └── generated_plugins.cmake
│   ├── main.cc
│   ├── my_application.cc
│   └── my_application.h
├── macos/
│   ├── .gitignore
│   ├── Flutter/
│   │   ├── Flutter-Debug.xcconfig
│   │   ├── Flutter-Release.xcconfig
│   │   └── GeneratedPluginRegistrant.swift
│   ├── Podfile
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets/
│   │   │   └── AppIcon.appiconset/
│   │   │       └── Contents.json
│   │   ├── Base.lproj/
│   │   │   └── MainMenu.xib
│   │   ├── Configs/
│   │   │   ├── AppInfo.xcconfig
│   │   │   ├── Debug.xcconfig
│   │   │   ├── Release.xcconfig
│   │   │   └── Warnings.xcconfig
│   │   ├── DebugProfile.entitlements
│   │   ├── Info.plist
│   │   ├── MainFlutterWindow.swift
│   │   └── Release.entitlements
│   ├── Runner.xcodeproj/
│   │   ├── project.pbxproj
│   │   ├── project.xcworkspace/
│   │   │   └── xcshareddata/
│   │   │       └── IDEWorkspaceChecks.plist
│   │   └── xcshareddata/
│   │       └── xcschemes/
│   │           └── Runner.xcscheme
│   └── Runner.xcworkspace/
│       ├── contents.xcworkspacedata
│       └── xcshareddata/
│           └── IDEWorkspaceChecks.plist
├── pubspec.yaml
├── scripts/
│   ├── README.md
│   ├── bind-android-debug.sh
│   ├── bind-ios-arm64.sh
│   ├── bind-ios.sh
│   ├── build-apk-arm.sh
│   ├── build-apk-arm64.sh
│   ├── build-apk-x64.sh
│   ├── build-apk-x86.sh
│   ├── build-ipa.sh
│   ├── build-macos-dmg.sh
│   ├── sign-apk-github-actions.sh
│   ├── thin-payload.sh
│   └── version.sh
├── test/
│   └── widget_test.dart
└── windows/
    ├── .gitignore
    ├── CMakeLists.txt
    ├── flutter/
    │   ├── CMakeLists.txt
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    └── runner/
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── runner.exe.manifest
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h
Download .txt
SYMBOL INDEX (443 symbols across 60 files)

FILE: ci/cmd/check_asset/main.go
  constant owner (line 13) | owner = "niuhuan"
  constant repo (line 14) | repo = "nhentai-cross"
  constant ua (line 15) | ua = "niuhuan nhentai-cross ci"
  function main (line 17) | func main() {

FILE: ci/cmd/check_release/main.go
  constant owner (line 14) | owner = "niuhuan"
  constant repo (line 15) | repo = "nhentai-cross"
  constant ua (line 16) | ua = "niuhuan nhentai-cross ci"
  constant mainBranch (line 17) | mainBranch = "master"
  function main (line 19) | func main() {

FILE: ci/cmd/upload_asset/main.go
  constant owner (line 14) | owner = "niuhuan"
  constant repo (line 15) | repo = "nhentai-cross"
  constant ua (line 16) | ua = "niuhuan nhentai-cross ci"
  function main (line 18) | func main() {

FILE: ci/commons/types.go
  type Version (line 5) | type Version struct
  type Release (line 10) | type Release struct

FILE: go/cmd/init.go
  function init (line 14) | func init() {
  function documentPath (line 18) | func documentPath() string {

FILE: go/cmd/main.go
  function main (line 21) | func main() {
  function iconProvider (line 52) | func iconProvider() ([]image.Image, error) {

FILE: go/cmd/plugin.go
  type Plugin (line 12) | type Plugin struct
    method InitPlugin (line 15) | func (p *Plugin) InitPlugin(messenger plugin.BinaryMessenger) error {
    method InitPluginGLFW (line 30) | func (p *Plugin) InitPluginGLFW(window *glfw.Window) error {

FILE: go/mobile/mobile.go
  function Migration (line 11) | func Migration(source, target string) {
  function exists (line 36) | func exists(name string) (bool, error) {
  function InitApplication (line 47) | func InitApplication(application string) {
  function FlatInvoke (line 51) | func FlatInvoke(method string, params string) (string, error) {
  function EventNotify (line 55) | func EventNotify(notify EventNotifyHandler) {
  type EventNotifyHandler (line 59) | type EventNotifyHandler interface

FILE: go/nhentai/client.go
  function initClient (line 38) | func initClient() {
  function setProxy (line 43) | func setProxy(proxyUrlString string) (string, error) {
  function getProxy (line 59) | func getProxy(_ string) (string, error) {
  function comics (line 63) | func comics(params string) (string, error) {
  function comicsByTagName (line 77) | func comicsByTagName(params string) (string, error) {
  function comicsBySearchRaw (line 95) | func comicsBySearchRaw(params string) (string, error) {
  function comicInfo (line 113) | func comicInfo(params string) (string, error) {
  function cacheImageByUrlPath (line 127) | func cacheImageByUrlPath(url string) (string, error) {
  function decodeAndSaveImage (line 164) | func decodeAndSaveImage(url string) (*cache.ImageCache, error) {

FILE: go/nhentai/common.go
  function availableWebAddresses (line 33) | func availableWebAddresses(_ string) (string, error) {
  function availableImgAddresses (line 40) | func availableImgAddresses(_ string) (string, error) {
  function cacheable (line 48) | func cacheable(key string, expire time.Duration, reload func() (interfac...
  function serialize (line 69) | func serialize(point interface{}, err error) (string, error) {
  function convertImageToJPEG100 (line 77) | func convertImageToJPEG100(params string) (string, error) {

FILE: go/nhentai/constant/constant.go
  function init (line 25) | func init() {
  function HashLock (line 32) | func HashLock(key string) *sync.Mutex {

FILE: go/nhentai/constant/os.go
  function ObtainDir (line 8) | func ObtainDir(dir string) {
  function ReasonableFileName (line 21) | func ReasonableFileName(title string) string {

FILE: go/nhentai/constant/time.go
  function Timestamp (line 6) | func Timestamp() int64 {

FILE: go/nhentai/database/active/active.go
  function Init (line 18) | func Init(databaseDir string) {
  type ViewLog (line 34) | type ViewLog struct
  type ViewLogTag (line 48) | type ViewLogTag struct
  type Download (line 57) | type Download struct
  type DownloadTag (line 71) | type DownloadTag struct
  type DownloadCover (line 80) | type DownloadCover struct
  type DownloadCoverThumb (line 90) | type DownloadCoverThumb struct
  type DownloadPage (line 100) | type DownloadPage struct
  type DownloadPageThumb (line 112) | type DownloadPageThumb struct
  function SaveViewInfo (line 121) | func SaveViewInfo(info nhentai.ComicInfo) error {
  function SaveViewIndex (line 149) | func SaveViewIndex(info nhentai.ComicInfo, index int) error {
  function saveViewTagInTx (line 178) | func saveViewTagInTx(tx *gorm.DB, tags []ViewLogTag) error {
  function takeViewLogTags (line 202) | func takeViewLogTags(infoTags []nhentai.ComicInfoTag, comicId int) []Vie...
  function takeViewLog (line 217) | func takeViewLog(info nhentai.ComicInfo, index int) ViewLog {
  function LoadLastViewIndexByComicId (line 233) | func LoadLastViewIndexByComicId(comicId int) (int, error) {
  function CreateDownload (line 247) | func CreateDownload(info nhentai.ComicInfo) error {
  function takeDownloadTags (line 347) | func takeDownloadTags(infoTags []nhentai.ComicInfoTag, comicId int) []Do...
  function LoadFirstNeedDownload (line 362) | func LoadFirstNeedDownload() (*Download, error) {
  function TheDownloadCover (line 376) | func TheDownloadCover(id int) (*DownloadCover, error) {
  function TheDownloadCoverThumb (line 384) | func TheDownloadCoverThumb(id int) (*DownloadCoverThumb, error) {
  function TheDownloadNeedDownloadPages (line 392) | func TheDownloadNeedDownloadPages(id int, limit int) ([]DownloadPage, er...
  function TheDownloadNeedDownloadPageThumbs (line 401) | func TheDownloadNeedDownloadPageThumbs(id int, limit int) ([]DownloadPag...
  function SaveDownloadCoverStatus (line 410) | func SaveDownloadCoverStatus(comicId int, status int, path string) error {
  function SaveDownloadCoverThumbStatus (line 419) | func SaveDownloadCoverThumbStatus(comicId int, status int, path string) ...
  function SaveDownloadPageStatus (line 428) | func SaveDownloadPageStatus(comicId int, pageIndex, status int, path str...
  function SaveDownloadPageThumbStatus (line 437) | func SaveDownloadPageThumbStatus(comicId int, pageIndex, status int, pat...
  function DownloadCoverOk (line 446) | func DownloadCoverOk(comicId int) bool {
  function DownloadCoverThumbOk (line 457) | func DownloadCoverThumbOk(comicId int) bool {
  function DownloadPageOk (line 468) | func DownloadPageOk(comicId int) bool {
  function DownloadPageThumbOk (line 479) | func DownloadPageThumbOk(comicId int) bool {
  function SaveDownloadStatus (line 490) | func SaveDownloadStatus(id int, status int) error {
  function FindDownloadPageByUrl (line 498) | func FindDownloadPageByUrl(url string) *DownloadPage {
  function FindDownloadPageThumbByUrl (line 512) | func FindDownloadPageThumbByUrl(url string) *DownloadPageThumb {
  function FindDownloadCoverByUrl (line 526) | func FindDownloadCoverByUrl(url string) *DownloadCover {
  function FindDownloadCoverThumbByUrl (line 540) | func FindDownloadCoverThumbByUrl(url string) *DownloadCoverThumb {
  function HasDownload (line 554) | func HasDownload(comicId int) bool {
  function ListDownloadComicInfo (line 568) | func ListDownloadComicInfo() []DownloadComicInfo {
  function filterPages (line 625) | func filterPages(pages []DownloadPage, id int) []nhentai.ImageInfo {
  function filterTags (line 639) | func filterTags(tags []DownloadTag, id int) []nhentai.ComicInfoTag {
  function filterCover (line 655) | func filterCover(covers []DownloadCover, id int) nhentai.ImageInfo {
  function filterCoverThumb (line 668) | func filterCoverThumb(covers []DownloadCoverThumb, id int) nhentai.Image...
  type DownloadComicInfo (line 681) | type DownloadComicInfo struct
  function ResetAllDownload (line 686) | func ResetAllDownload() {
  function MarkComicDeleting (line 712) | func MarkComicDeleting(id int) {
  function LoadFirstNeedDelete (line 721) | func LoadFirstNeedDelete() *Download {
  function DeletedComic (line 735) | func DeletedComic(id int) {

FILE: go/nhentai/database/cache/cache.go
  type NetworkCache (line 16) | type NetworkCache struct
  type ImageCache (line 22) | type ImageCache struct
  function Init (line 29) | func Init(databaseDir string) {
  function LoadCache (line 39) | func LoadCache(key string, expire time.Duration) (string, error) {
  function SaveCache (line 53) | func SaveCache(key string, value string) error {
  function RemoveCache (line 65) | func RemoveCache(key string) error {
  function RemoveCaches (line 75) | func RemoveCaches(like string) error {
  function RemoveAllCache (line 85) | func RemoveAllCache() error {
  function RemoveEarliestCache (line 95) | func RemoveEarliestCache(earliest time.Time) error {
  function SaveImageCache (line 106) | func SaveImageCache(remote *ImageCache) error {
  function FindImageCache (line 119) | func FindImageCache(url string) *ImageCache {
  function RemoveAllImageCache (line 134) | func RemoveAllImageCache() error {
  function EarliestImageCache (line 144) | func EarliestImageCache(earliest time.Time, pageSize int) ([]ImageCache,...
  function DeleteImageCache (line 153) | func DeleteImageCache(images []ImageCache) error {

FILE: go/nhentai/database/properties/properties.go
  function Init (line 15) | func Init(databaseDir string) {
  type Property (line 24) | type Property struct
  function LoadProperty (line 30) | func LoadProperty(name string, defaultValue string) (string, error) {
  function SaveProperty (line 42) | func SaveProperty(name string, value string) error {
  function LoadBoolProperty (line 52) | func LoadBoolProperty(name string, defaultValue bool) (bool, error) {
  function LoadIntProperty (line 60) | func LoadIntProperty(name string, defaultValue int) (int, error) {

FILE: go/nhentai/decodes.go
  function init (line 19) | func init() {
  function takeMutex (line 27) | func takeMutex() *sync.Mutex {
  function decodeFromUrl (line 35) | func decodeFromUrl(url string) ([]byte, error) {
  function decodeFromRemote (line 61) | func decodeFromRemote(url string) ([]byte, error) {

FILE: go/nhentai/download.go
  function initDownload (line 20) | func initDownload() {
  function downloadHasStop (line 31) | func downloadHasStop() bool {
  function downloadDelete (line 43) | func downloadDelete() bool {
  function downloadBegin (line 59) | func downloadBegin() {
  function downloadLoadComic (line 71) | func downloadLoadComic() {
  function downloadProcessDownloadingComic (line 89) | func downloadProcessDownloadingComic(downloadingComic *active.Download) {
  function downloadDecodeUrl (line 271) | func downloadDecodeUrl(url string) ([]byte, error) {

FILE: go/nhentai/locks.go
  function init (line 10) | func init() {
  function HashLock (line 17) | func HashLock(key string) *sync.Mutex {

FILE: go/nhentai/nhentai.go
  function InitNHentai (line 20) | func InitNHentai(documentDir string) {
  function cacheImagePath (line 38) | func cacheImagePath(aliasPath string) string {
  function setUserAgent (line 67) | func setUserAgent(s string) (string, error) {
  function setCookie (line 72) | func setCookie(s string) (string, error) {
  function FlatInvoke (line 77) | func FlatInvoke(method string, params string) (string, error) {
  function saveProperty (line 84) | func saveProperty(params string) (string, error) {
  function loadProperty (line 93) | func loadProperty(params string) (string, error) {
  function saveViewInfo (line 102) | func saveViewInfo(params string) (string, error) {
  function saveViewIndex (line 108) | func saveViewIndex(params string) (string, error) {
  function loadLastViewIndexByComicId (line 117) | func loadLastViewIndexByComicId(params string) (string, error) {
  function downloadComic (line 125) | func downloadComic(params string) (string, error) {
  function hasDownload (line 131) | func hasDownload(params string) (string, error) {
  function listDownloadComicInfo (line 139) | func listDownloadComicInfo(s string) (string, error) {
  function downloadSetDelete (line 143) | func downloadSetDelete(params string) (string, error) {
  function httpGet (line 153) | func httpGet(url string) (string, error) {

FILE: lib/basic/channels/nhentai.dart
  class NHentai (line 9) | class NHentai {
    method _flatInvoke (line 15) | Future<String> _flatInvoke(String method, dynamic params)
    method setProxy (line 23) | Future setProxy(String proxyUrl)
    method getProxy (line 28) | Future<String> getProxy()
    method comics (line 33) | Future<ComicPageData> comics(int page)
    method comicsByTagName (line 40) | Future<ComicPageData> comicsByTagName(String tagName, int page)
    method comicsBySearchRaw (line 50) | Future<ComicPageData> comicsBySearchRaw(String raw, int page)
    method comicInfo (line 60) | Future<ComicInfo> comicInfo(int comicId)
    method cacheImageByUrlPath (line 67) | Future<String> cacheImageByUrlPath(String url)
    method saveFileToImage (line 72) | Future saveFileToImage(String path)
    method convertImageToJPEG100 (line 79) | Future convertImageToJPEG100(String path, String dir)
    method androidVersion (line 87) | Future<int> androidVersion()
    method loadProperty (line 95) | Future<String> loadProperty(String propertyName, String defaultValue)
    method saveProperty (line 103) | Future<dynamic> saveProperty(String propertyName, String value)
    method saveViewInfo (line 111) | Future<dynamic> saveViewInfo(ComicInfo info)
    method saveViewIndex (line 116) | Future<dynamic> saveViewIndex(ComicInfo info, int index)
    method loadLastViewIndexByComicId (line 124) | Future<int> loadLastViewIndexByComicId(int comicId)
    method downloadComic (line 129) | Future<dynamic> downloadComic(ComicInfo comicInfo)
    method hasDownload (line 134) | Future<bool> hasDownload(int comicId)
    method listDownloadComicInfo (line 139) | Future<List<DownloadComicInfo>> listDownloadComicInfo()
    method downloadSetDelete (line 147) | Future<dynamic> downloadSetDelete(int comicId)
    method httpGet (line 152) | Future<String> httpGet(String url)
    method setCookie (line 156) | Future setCookie(String cookies)
    method setUserAgent (line 160) | Future setUserAgent(String s)

FILE: lib/basic/common/common.dart
  function defaultToast (line 7) | void defaultToast(BuildContext context, String title)
  function confirmDialog (line 22) | Future<bool> confirmDialog(BuildContext context, String title,
  function alertDialog (line 56) | Future alertDialog(BuildContext context, String title, String content)
  function filteredList (line 80) | List<T> filteredList<T>(List<T> list, bool Function(T) filter)
  function chooseListDialog (line 91) | Future<T?> chooseListDialog<T>(
  function chooseMapDialog (line 120) | Future<T?> chooseMapDialog<T>(
  function displayTextInputDialog (line 144) | Future<String?> displayTextInputDialog(
  function add0 (line 202) | String add0(int num, int len)
  function formatTimeToDate (line 211) | String formatTimeToDate(String str)
  function inputString (line 235) | Future<String?> inputString(BuildContext context, String title,

FILE: lib/basic/common/cross.dart
  function copyToClipBoard (line 13) | void copyToClipBoard(BuildContext context, String string)
  function openUrl (line 24) | Future<dynamic> openUrl(String url)
  function saveImage (line 34) | Future<dynamic> saveImage(String path, BuildContext context)
  function saveImageQuiet (line 63) | Future<dynamic> saveImageQuiet(String path, BuildContext context)
  function _saveImageAndroid (line 73) | Future<dynamic> _saveImageAndroid(String path, BuildContext context)
  function chooseFolder (line 82) | Future<String?> chooseFolder(BuildContext context)
  function confirmCopy (line 93) | void confirmCopy(BuildContext context, String content)
  function currentChooserRoot (line 99) | Future<String> currentChooserRoot()

FILE: lib/basic/common/error_types.dart
  function errorType (line 7) | String errorType(String error)

FILE: lib/basic/configs/proxy.dart
  function initProxy (line 9) | Future initProxy()
  function currentProxy (line 13) | String currentProxy()
  function chooseProxy (line 17) | Future chooseProxy(BuildContext context)
  function proxySetting (line 30) | Widget proxySetting()

FILE: lib/basic/configs/reader_direction.dart
  type ReaderDirection (line 6) | enum ReaderDirection {
  function initReaderDirection (line 15) | Future initReaderDirection()
  function _fromString (line 19) | ReaderDirection _fromString(String valueForm)
  function currentReaderDirection (line 28) | ReaderDirection currentReaderDirection()
  function readerDirectionName (line 32) | String readerDirectionName(ReaderDirection direction, BuildContext context)
  function chooseReaderDirection (line 43) | Future chooseReaderDirection(BuildContext context)

FILE: lib/basic/configs/reader_type.dart
  type ReaderType (line 6) | enum ReaderType {
  function initReaderType (line 14) | Future initReaderType()
  function _fromString (line 18) | ReaderType _fromString(String valueForm)
  function currentReaderType (line 27) | ReaderType currentReaderType()
  function readerTypeName (line 31) | String readerTypeName(ReaderType type, BuildContext context)
  function chooseReaderType (line 40) | Future chooseReaderType(BuildContext context)

FILE: lib/basic/configs/themes.dart
  function initFont (line 17) | Future initFont()
  function _fontThemeData (line 22) | ThemeData _fontThemeData(bool dark)
  function inputFont (line 29) | Future<void> inputFont(BuildContext context)
  function fontSetting (line 41) | Widget fontSetting()
  class _ThemePackage (line 59) | abstract class _ThemePackage {
    method code (line 60) | String code()
    method name (line 62) | String name()
    method themeData (line 64) | ThemeData themeData(ThemeData rawData)
  class _OriginTheme (line 67) | class _OriginTheme extends _ThemePackage {
    method code (line 69) | String code()
    method name (line 72) | String name()
    method themeData (line 75) | ThemeData themeData(ThemeData rawData)
  class _PinkTheme (line 78) | class _PinkTheme extends _ThemePackage {
    method code (line 80) | String code()
    method name (line 83) | String name()
    method themeData (line 86) | ThemeData themeData(ThemeData rawData)
  class _BlackTheme (line 118) | class _BlackTheme extends _ThemePackage {
    method code (line 120) | String code()
    method name (line 123) | String name()
    method themeData (line 126) | ThemeData themeData(ThemeData rawData)
  class _DarkTheme (line 159) | class _DarkTheme extends _ThemePackage {
    method code (line 161) | String code()
    method name (line 164) | String name()
    method themeData (line 167) | ThemeData themeData(ThemeData rawData)
  class _DustyBlueTheme (line 199) | class _DustyBlueTheme extends _ThemePackage {
    method code (line 201) | String code()
    method name (line 204) | String name()
    method themeData (line 207) | ThemeData themeData(ThemeData rawData)
  function currentThemeName (line 265) | String currentThemeName()
  function currentThemeData (line 274) | ThemeData? currentThemeData()
  function currentDarkTheme (line 278) | ThemeData? currentDarkTheme()
  function _changeThemeByCode (line 283) | void _changeThemeByCode(String? themeCode)
  function initTheme (line 307) | Future<dynamic> initTheme()
  function chooseTheme (line 316) | Future<dynamic> chooseTheme(BuildContext buildContext)
  function onChange (line 325) | Future onChange(bool? v)
  function themeSetting (line 393) | Widget themeSetting(BuildContext context)

FILE: lib/basic/configs/version.dart
  function initVersion (line 26) | Future initVersion()
  function currentVersion (line 45) | String currentVersion()
  function latestVersion (line 49) | String? latestVersion()
  function latestVersionInfo (line 53) | String? latestVersionInfo()
  function autoCheckNewVersion (line 57) | Future autoCheckNewVersion()
  function manualCheckNewVersion (line 65) | Future manualCheckNewVersion(BuildContext context)
  function dirtyVersion (line 75) | bool dirtyVersion()
  function _versionCheck (line 80) | Future _versionCheck()
  function _periodText (line 94) | String _periodText(BuildContext context)
  function _choosePeriod (line 108) | Future _choosePeriod(BuildContext context)
  function autoUpdateCheckSetting (line 150) | Widget autoUpdateCheckSetting()
  function formatDateTimeToDateTime (line 165) | String formatDateTimeToDateTime(DateTime c)
  class VersionInfo (line 173) | class VersionInfo extends StatefulWidget {
    method createState (line 177) | State<StatefulWidget> createState()
  class _VersionInfoState (line 180) | class _VersionInfoState extends State<VersionInfo> {
    method build (line 182) | Widget build(BuildContext context)
    method _buildNewVersion (line 214) | Widget _buildNewVersion(String? latestVersion)
    method _buildDirty (line 273) | Widget _buildDirty()
    method _buildNewVersionInfo (line 286) | Widget _buildNewVersionInfo(String? latestVersionInfo)

FILE: lib/basic/entities/entities.dart
  class PageData (line 3) | class PageData {
  class ComicPageData (line 13) | class ComicPageData extends PageData {
  class ComicSimple (line 26) | class ComicSimple {
  class ComicInfo (line 48) | class ComicInfo {
    method toJson (line 74) | Map<String, dynamic> toJson()
  class ComicInfoTitle (line 89) | class ComicInfoTitle {
    method toJson (line 100) | Map<String, dynamic> toJson()
  class ComicImages (line 109) | class ComicImages {
    method toJson (line 123) | Map<String, dynamic> toJson()
  class ImageInfo (line 132) | class ImageInfo {
    method toJson (line 143) | Map<String, dynamic> toJson()
  class ComicInfoTag (line 152) | class ComicInfoTag {
    method toJson (line 167) | Map<String, dynamic> toJson()
  class DownloadComicInfo (line 178) | class DownloadComicInfo extends ComicInfo {

FILE: lib/main.dart
  function main (line 14) | void main()
  class MyApp (line 18) | class MyApp extends StatefulWidget {
    method createState (line 23) | State<StatefulWidget> createState()
  class MyAppState (line 26) | class MyAppState extends State<MyApp> {
    method initState (line 29) | void initState()
    method dispose (line 35) | void dispose()
    method _onChangeTheme (line 40) | void _onChangeTheme(EventArgs? args)
    method build (line 45) | Widget build(BuildContext context)

FILE: lib/main_desktop.dart
  function main (line 4) | void main()

FILE: lib/screens/comic_downloads_screen.dart
  class ComicDownloadsScreen (line 13) | class ComicDownloadsScreen extends StatefulWidget {
    method createState (line 17) | State<StatefulWidget> createState()
  class _ComicDownloadsScreenState (line 20) | class _ComicDownloadsScreenState extends State<ComicDownloadsScreen> {
    method initState (line 24) | void initState()
    method build (line 30) | Widget build(BuildContext context)
    method _buildImageCard (line 63) | Widget _buildImageCard(DownloadComicInfo item)
    method _buildDownloadStatus (line 121) | Widget _buildDownloadStatus(int downloadStatus)

FILE: lib/screens/comic_info_screen.dart
  class ComicInfoScreen (line 18) | class ComicInfoScreen extends StatefulWidget {
    method createState (line 26) | State<StatefulWidget> createState()
  class _ComicInfoScreenState (line 29) | class _ComicInfoScreenState extends State<ComicInfoScreen> {
    method _loadComic (line 33) | Future<ComicInfo> _loadComic()
    method initState (line 40) | void initState()
    method build (line 47) | Widget build(BuildContext context)
    method _buildTag (line 287) | Widget _buildTag(ComicInfoTag e)

FILE: lib/screens/comic_reader_screen.dart
  class ComicReaderScreen (line 17) | class ComicReaderScreen extends StatefulWidget {
    method createState (line 23) | State<StatefulWidget> createState()
  class _ComicReaderScreenState (line 26) | class _ComicReaderScreenState extends State<ComicReaderScreen> {
    method _init (line 32) | Future _init()
    method initState (line 40) | void initState()
    method _reload (line 45) | Future _reload()
    method build (line 56) | Widget build(BuildContext context)
  class _ComicReader (line 82) | class _ComicReader extends StatefulWidget {
    method createState (line 100) | State<StatefulWidget> createState()
  class _ComicReaderState (line 110) | abstract class _ComicReaderState extends State<_ComicReader> {
    method _buildViewer (line 113) | Widget _buildViewer()
    method _onFullScreenChange (line 121) | Future _onFullScreenChange(bool fullScreen)
    method _onCurrentChange (line 129) | void _onCurrentChange(int index)
    method initState (line 140) | void initState()
    method dispose (line 149) | void dispose()
    method _onPageControl (line 157) | void _onPageControl(_ReaderControllerEventArgs? args)
    method build (line 176) | Widget build(BuildContext context)
    method _buildFrame (line 185) | Widget _buildFrame()
    method _buildAppBar (line 204) | Widget _buildAppBar()
    method _buildBottomBar (line 216) | Widget _buildBottomBar()
    method _buildEdgePadding (line 229) | Widget _buildEdgePadding()
    method _buildSlider (line 239) | Widget _buildSlider()
    method _appBarHeight (line 317) | double _appBarHeight()
    method _bottomBarHeight (line 321) | double _bottomBarHeight()
  class _SettingPanel (line 326) | class _SettingPanel extends StatefulWidget {
    method createState (line 328) | State<StatefulWidget> createState()
  class _SettingPanelState (line 331) | class _SettingPanelState extends State<_SettingPanel> {
    method build (line 333) | Widget build(BuildContext context)
    method _bottomIcon (line 368) | Widget _bottomIcon({
  class _ComicReaderWebToonState (line 406) | class _ComicReaderWebToonState extends _ComicReaderState {
    method dispose (line 412) | void dispose()
    method _buildViewer (line 418) | Widget _buildViewer()
    method _initOffset (line 508) | double _initOffset()
  class _ComicReaderGalleryState (line 536) | class _ComicReaderGalleryState extends _ComicReaderState {
    method initState (line 541) | void initState()
    method dispose (line 578) | void dispose()
    method _buildViewer (line 584) | Widget _buildViewer()
    method _onGalleryPageChange (line 613) | void _onGalleryPageChange(int to)
  class _ReaderControllerEventArgs (line 627) | class _ReaderControllerEventArgs extends EventArgs {
  function _onVolumeEvent (line 637) | void _onVolumeEvent(dynamic args)
  function addVolumeListen (line 644) | void addVolumeListen()
  function delVolumeListen (line 652) | void delVolumeListen()
  function readerKeyboardHolder (line 659) | Widget readerKeyboardHolder(Widget widget)

FILE: lib/screens/comic_search_screen.dart
  class ComicSearchStruct (line 5) | class ComicSearchStruct {
    method dumpSearchString (line 20) | String dumpSearchString()
  class ComicSearchCondition (line 37) | class ComicSearchCondition {
  class ComicSearchScreen (line 45) | class ComicSearchScreen extends StatefulWidget {
    method createState (line 58) | State<StatefulWidget> createState()
  class _ComicSearchScreenState (line 61) | class _ComicSearchScreenState extends State<ComicSearchScreen> {
    method initState (line 67) | void initState()
    method dispose (line 73) | void dispose()
    method build (line 80) | Widget build(BuildContext context)
    method _buildCondition (line 181) | Widget _buildCondition(ComicSearchCondition condition)
  class _AddTagConditionDialog (line 230) | class _AddTagConditionDialog extends StatefulWidget {
    method createState (line 238) | State<StatefulWidget> createState()
  class _AddTagConditionDialogState (line 241) | class _AddTagConditionDialogState extends State<_AddTagConditionDialog> {
    method build (line 243) | Widget build(BuildContext context)

FILE: lib/screens/comics_screen.dart
  class ComicsScreen (line 14) | class ComicsScreen extends StatefulWidget {
    method createState (line 26) | State<StatefulWidget> createState()
  class _ComicsScreenState (line 29) | class _ComicsScreenState extends State<ComicsScreen> {
    method build (line 31) | Widget build(BuildContext context)

FILE: lib/screens/components/Badged.dart
  class Badged (line 3) | class Badged extends StatelessWidget {
    method build (line 10) | Widget build(BuildContext context)

FILE: lib/screens/components/actions.dart
  function alwaysInActions (line 7) | List<Widget> alwaysInActions()
  class _SettingsAction (line 13) | class _SettingsAction extends StatefulWidget {
    method createState (line 15) | State<StatefulWidget> createState()
  class _SettingsActionState (line 18) | class _SettingsActionState extends State<_SettingsAction> {
    method initState (line 20) | void initState()
    method dispose (line 26) | void dispose()
    method _setState (line 31) | void _setState(_)
    method build (line 36) | Widget build(BuildContext context)

FILE: lib/screens/components/content_builder.dart
  class ContentBuilder (line 5) | class ContentBuilder<T> extends StatelessWidget {
    method build (line 20) | Widget build(BuildContext context)

FILE: lib/screens/components/content_error.dart
  class ContentError (line 7) | class ContentError extends StatelessWidget {
    method build (line 20) | Widget build(BuildContext context)

FILE: lib/screens/components/content_loading.dart
  class ContentLoading (line 4) | class ContentLoading extends StatelessWidget {
    method build (line 10) | Widget build(BuildContext context)

FILE: lib/screens/components/images.dart
  function coverImageUrl (line 12) | String coverImageUrl(int mediaId)
  function pageImageUrl (line 16) | String pageImageUrl(int mediaId, int num)
  class NHentaiImageProvider (line 20) | class NHentaiImageProvider extends ImageProvider<NHentaiImageProvider> {
    method load (line 27) | ImageStreamCompleter load(key, DecoderCallback decode)
    method obtainKey (line 35) | Future<NHentaiImageProvider> obtainKey(ImageConfiguration configuration)
    method _loadAsync (line 39) | Future<ui.Codec> _loadAsync(NHentaiImageProvider key)
  class NHentaiImage (line 47) | class NHentaiImage extends StatefulWidget {
    method createState (line 62) | State<StatefulWidget> createState()
  class _NHentaiImageState (line 65) | class _NHentaiImageState extends State<NHentaiImage> {
    method build (line 69) | Widget build(BuildContext context)
  function pathFutureImage (line 81) | Widget pathFutureImage(Future<String> future, double? width, double? hei...
  function buildError (line 107) | Widget buildError(double? width, double? height)
  function buildLoading (line 115) | Widget buildLoading(double? width, double? height)
  function buildFile (line 133) | Widget buildFile(String file, double? width, double? height,
  class HorizontalStretchNHentaiImage (line 177) | class HorizontalStretchNHentaiImage extends StatelessWidget {
    method build (line 186) | Widget build(BuildContext context)
  class ScaleImageTitle (line 198) | class ScaleImageTitle extends StatelessWidget {
    method build (line 207) | Widget build(BuildContext context)

FILE: lib/screens/components/mouse_and_touch_scroll_behavior.dart
  class MouseAndTouchScrollBehavior (line 6) | class MouseAndTouchScrollBehavior extends MaterialScrollBehavior {

FILE: lib/screens/components/pager.dart
  class Pager (line 12) | class Pager extends StatefulWidget {
    method createState (line 18) | State<StatefulWidget> createState()
  class _PageState (line 21) | class _PageState extends State<Pager> {
    method _join (line 29) | Future _join()
    method _next (line 47) | void _next()
    method _onScroll (line 53) | void _onScroll()
    method initState (line 64) | void initState()
    method dispose (line 72) | void dispose()
    method build (line 79) | Widget build(BuildContext context)
    method _buildLoadingCard (line 100) | Widget _buildLoadingCard()
    method _buildImageCard (line 142) | Widget _buildImageCard(ComicSimple item)

FILE: lib/screens/file_photo_view_screen.dart
  class FilePhotoViewScreen (line 9) | class FilePhotoViewScreen extends StatelessWidget {
    method build (line 15) | Widget build(BuildContext context)

FILE: lib/screens/init_screen.dart
  class InitScreen (line 14) | class InitScreen extends StatefulWidget {
    method createState (line 18) | State<StatefulWidget> createState()
  class _InitScreenState (line 21) | class _InitScreenState extends State<InitScreen> {
    method initState (line 23) | void initState()
    method _init (line 28) | Future<void> _init()
    method build (line 68) | Widget build(BuildContext context)

FILE: lib/screens/settings_screen.dart
  class SettingsScreen (line 7) | class SettingsScreen extends StatelessWidget {
    method build (line 11) | Widget build(BuildContext context)

FILE: lib/screens/webview_screen.dart
  class WebViewScreen (line 8) | class WebViewScreen extends StatefulWidget {
    method createState (line 12) | State<StatefulWidget> createState()
  class _WebViewScreenState (line 15) | class _WebViewScreenState extends State<WebViewScreen> {
    method initState (line 20) | void initState()
    method build (line 26) | Widget build(BuildContext context)

FILE: linux/flutter/generated_plugin_registrant.cc
  function fl_register_plugins (line 11) | void fl_register_plugins(FlPluginRegistry* registry) {

FILE: linux/main.cc
  function main (line 3) | int main(int argc, char** argv) {

FILE: linux/my_application.cc
  type _MyApplication (line 10) | struct _MyApplication {
  function my_application_activate (line 18) | static void my_application_activate(GApplication* application) {
  function gboolean (line 66) | static gboolean my_application_local_command_line(GApplication* applicat...
  function my_application_dispose (line 85) | static void my_application_dispose(GObject* object) {
  function my_application_class_init (line 91) | static void my_application_class_init(MyApplicationClass* klass) {
  function my_application_init (line 97) | static void my_application_init(MyApplication* self) {}
  function MyApplication (line 99) | MyApplication* my_application_new() {

FILE: test/widget_test.dart
  function main (line 13) | void main()

FILE: windows/flutter/generated_plugin_registrant.cc
  function RegisterPlugins (line 13) | void RegisterPlugins(flutter::PluginRegistry* registry) {

FILE: windows/runner/flutter_window.cpp
  function LRESULT (line 40) | LRESULT

FILE: windows/runner/flutter_window.h
  function class (line 12) | class FlutterWindow : public Win32Window {

FILE: windows/runner/main.cpp
  function wWinMain (line 8) | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,

FILE: windows/runner/utils.cpp
  function CreateAndAttachConsole (line 10) | void CreateAndAttachConsole() {
  function GetCommandLineArguments (line 24) | std::vector<std::string> GetCommandLineArguments() {
  function Utf8FromUtf16 (line 44) | std::string Utf8FromUtf16(const wchar_t* utf16_string) {

FILE: windows/runner/win32_window.cpp
  function Scale (line 18) | int Scale(int source, double scale_factor) {
  function EnableFullDpiSupportIfAvailable (line 24) | void EnableFullDpiSupportIfAvailable(HWND hwnd) {
  class WindowClassRegistrar (line 41) | class WindowClassRegistrar {
    method WindowClassRegistrar (line 46) | static WindowClassRegistrar* GetInstance() {
    method WindowClassRegistrar (line 62) | WindowClassRegistrar() = default;
  function wchar_t (line 71) | const wchar_t* WindowClassRegistrar::GetWindowClass() {
  function LRESULT (line 133) | LRESULT CALLBACK Win32Window::WndProc(HWND const window,
  function LRESULT (line 152) | LRESULT
  function Win32Window (line 208) | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
  function RECT (line 224) | RECT Win32Window::GetClientArea() {
  function HWND (line 230) | HWND Win32Window::GetHandle() {

FILE: windows/runner/win32_window.h
  type Size (line 21) | struct Size {
Condensed preview — 173 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (426K chars).
[
  {
    "path": ".github/workflows/Release.yml",
    "chars": 7830,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n\nenv:\n  go_version: '1.17'\n  flutter_channel: 'stable'\n  GH_TOKEN: ${{ secrets.G"
  },
  {
    "path": ".gitignore",
    "chars": 850,
    "preview": "# 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*.i"
  },
  {
    "path": ".metadata",
    "chars": 305,
    "preview": "# This file tracks properties of this Flutter project.\n# Used by Flutter tool to assess capabilities and perform upgrade"
  },
  {
    "path": "LICENSE",
    "chars": 1056,
    "preview": "Copyright (c) 2021-2022 niuhuan\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this so"
  },
  {
    "path": "README-zh.md",
    "chars": 937,
    "preview": "# NHENTAI-CROSS\n\n## [English](README.md) | 简体中文\n\n[![license](https://img.shields.io/github/license/niuhuan/nhentai-cross"
  },
  {
    "path": "README.md",
    "chars": 865,
    "preview": "# NHENTAI-CROSS\n\n##  English | [简体中文](README-zh.md)\n\n[![license](https://img.shields.io/github/license/niuhuan/nhentai-c"
  },
  {
    "path": "analysis_options.yaml",
    "chars": 1453,
    "preview": "# This file configures the analyzer, which statically analyzes Dart code to\n# check for errors, warnings, and lints.\n#\n#"
  },
  {
    "path": "android/.gitignore",
    "chars": 285,
    "preview": "gradle-wrapper.jar\n/.gradle\n/captures/\n/gradlew\n/gradlew.bat\n/local.properties\nGeneratedPluginRegistrant.java\n\n# Remembe"
  },
  {
    "path": "android/app/build.gradle",
    "chars": 2050,
    "preview": "def localProperties = new Properties()\ndef localPropertiesFile = rootProject.file('local.properties')\nif (localPropertie"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "chars": 323,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n    <!-- Flutter nee"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "chars": 3345,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n\n    <uses-permissio"
  },
  {
    "path": "android/app/src/main/kotlin/niuhuan/nhentai/MainActivity.kt",
    "chars": 13577,
    "preview": "package niuhuan.nhentai\n\nimport android.content.ContentValues\nimport android.graphics.Bitmap\nimport android.graphics.Bit"
  },
  {
    "path": "android/app/src/main/res/drawable/launch_background.xml",
    "chars": 434,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmln"
  },
  {
    "path": "android/app/src/main/res/drawable-v21/launch_background.xml",
    "chars": 438,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Modify this file to customize your launch splash screen -->\n<layer-list xmln"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "chars": 1142,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied"
  },
  {
    "path": "android/app/src/main/res/values-night/styles.xml",
    "chars": 1141,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Theme applied"
  },
  {
    "path": "android/app/src/profile/AndroidManifest.xml",
    "chars": 323,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"niuhuan.nhentai\">\n    <!-- Flutter nee"
  },
  {
    "path": "android/build.gradle",
    "chars": 576,
    "preview": "buildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        mavenCentral()\n    }\n\n    de"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "chars": 231,
    "preview": "#Fri Jun 23 08:50:38 CEST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
  },
  {
    "path": "android/gradle.properties",
    "chars": 82,
    "preview": "org.gradle.jvmargs=-Xmx1536M\nandroid.useAndroidX=true\nandroid.enableJetifier=true\n"
  },
  {
    "path": "android/settings.gradle",
    "chars": 462,
    "preview": "include ':app'\n\ndef localPropertiesFile = new File(rootProject.projectDir, \"local.properties\")\ndef properties = new Prop"
  },
  {
    "path": "ci/cmd/check_asset/main.go",
    "chars": 3286,
    "preview": "package main\n\nimport (\n        \"ci/commons\"\n        \"encoding/json\"\n        \"fmt\"\n        \"io/ioutil\"\n        \"net/http\""
  },
  {
    "path": "ci/cmd/check_release/main.go",
    "chars": 2232,
    "preview": "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"
  },
  {
    "path": "ci/cmd/upload_asset/main.go",
    "chars": 3999,
    "preview": "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 "
  },
  {
    "path": "ci/commons/types.go",
    "chars": 3337,
    "preview": "package commons\n\nimport \"time\"\n\ntype Version struct {\n\tCode string `json:\"code\"`\n\tInfo string `json:\"info\"`\n}\n\ntype Rele"
  },
  {
    "path": "ci/go.mod",
    "chars": 11,
    "preview": "module \"ci\""
  },
  {
    "path": "ci/linux_font.yaml",
    "chars": 73,
    "preview": "\n  fonts:\n  - family: Roboto\n    fonts:\n      - asset: fonts/Roboto.ttf\n\n"
  },
  {
    "path": "ci/version.code.txt",
    "chars": 6,
    "preview": "v0.0.9"
  },
  {
    "path": "ci/version.info.txt",
    "chars": 42,
    "preview": "- Cross DDoS (only phone)\n- 通过ddos (仅手机端)\n"
  },
  {
    "path": "go/.gitignore",
    "chars": 51,
    "preview": "build\n.last_goflutter_check\n.last_go-flutter_check\n"
  },
  {
    "path": "go/cmd/init.go",
    "chars": 1272,
    "preview": "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\nf"
  },
  {
    "path": "go/cmd/main.go",
    "chars": 1858,
    "preview": "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/gi"
  },
  {
    "path": "go/cmd/options.go",
    "chars": 347,
    "preview": "package main\n\nimport (\n\t\"github.com/go-flutter-desktop/go-flutter\"\n\t\"github.com/go-flutter-desktop/plugins/url_launcher\""
  },
  {
    "path": "go/cmd/plugin.go",
    "chars": 1252,
    "preview": "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"
  },
  {
    "path": "go/go.mod",
    "chars": 1613,
    "preview": "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/plug"
  },
  {
    "path": "go/go.sum",
    "chars": 12331,
    "preview": "github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngith"
  },
  {
    "path": "go/hover.yaml",
    "chars": 656,
    "preview": "#application-name: \"nhentai\" # Uncomment to modify this value.\n#executable-name: \"nhentai\" # Uncomment to modify this va"
  },
  {
    "path": "go/mobile/bind-android-debug.sh",
    "chars": 95,
    "preview": "gomobile bind -target=android/arm,android/arm64,android/386,android/amd64 -o lib/Mobile.aar ./\n"
  },
  {
    "path": "go/mobile/bind-android.sh",
    "chars": 55,
    "preview": "gomobile bind -target=android/arm -o lib/Mobile.aar ./\n"
  },
  {
    "path": "go/mobile/bind-ios-debug.sh",
    "chars": 55,
    "preview": "gomobile bind -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "go/mobile/bind-ios.sh",
    "chars": 55,
    "preview": "gomobile bind -target=ios -o lib/Mobile.xcframework ./\n"
  },
  {
    "path": "go/mobile/lib/.keep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "go/mobile/mobile.go",
    "chars": 1191,
    "preview": "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"
  },
  {
    "path": "go/nhentai/client.go",
    "chars": 4193,
    "preview": "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\"n"
  },
  {
    "path": "go/nhentai/common.go",
    "chars": 2051,
    "preview": "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"
  },
  {
    "path": "go/nhentai/constant/constant.go",
    "chars": 649,
    "preview": "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\nva"
  },
  {
    "path": "go/nhentai/constant/os.go",
    "chars": 637,
    "preview": "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\ti"
  },
  {
    "path": "go/nhentai/constant/time.go",
    "chars": 142,
    "preview": "package constant\n\nimport \"time\"\n\n// Timestamp 获取当前的Unix时间戳\nfunc Timestamp() int64 {\n\treturn time.Now().UnixNano() / int6"
  },
  {
    "path": "go/nhentai/database/active/active.go",
    "chars": 20201,
    "preview": "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/g"
  },
  {
    "path": "go/nhentai/database/cache/cache.go",
    "chars": 3596,
    "preview": "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\"pa"
  },
  {
    "path": "go/nhentai/database/properties/properties.go",
    "chars": 1520,
    "preview": "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"
  },
  {
    "path": "go/nhentai/decodes.go",
    "chars": 1340,
    "preview": "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/iout"
  },
  {
    "path": "go/nhentai/download.go",
    "chars": 6868,
    "preview": "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\"pat"
  },
  {
    "path": "go/nhentai/locks.go",
    "chars": 349,
    "preview": "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"
  },
  {
    "path": "go/nhentai/nhentai.go",
    "chars": 4444,
    "preview": "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\"nhen"
  },
  {
    "path": "go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl",
    "chars": 1449,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.c"
  },
  {
    "path": "go/packaging/linux-appimage/AppRun.tmpl",
    "chars": 64,
    "preview": "#!/bin/sh\ncd \"$(dirname \"$0\")\"\nexec ./build/{{.executableName}}\n"
  },
  {
    "path": "go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl",
    "chars": 167,
    "preview": "[Desktop Entry]\nVersion=1.0\nType=Application\nTerminal=false\nCategories=\nComment={{.description}}\nName={{.applicationName"
  },
  {
    "path": "ios/.gitignore",
    "chars": 569,
    "preview": "**/dgph\n*.mode1v3\n*.mode2v3\n*.moved-aside\n*.pbxuser\n*.perspectivev3\n**/*sync/\n.sconsign.dblite\n.tags*\n**/.vagrant/\n**/De"
  },
  {
    "path": "ios/Flutter/AppFrameworkInfo.plist",
    "chars": 774,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Flutter/Debug.xcconfig",
    "chars": 107,
    "preview": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Flutter/Release.xcconfig",
    "chars": 109,
    "preview": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"Generated.xcconfig\"\n"
  },
  {
    "path": "ios/Podfile",
    "chars": 1355,
    "preview": "# Uncomment this line to define a global platform for your project\n# platform :ios, '11.0'\n\n# CocoaPods analytics sends "
  },
  {
    "path": "ios/Runner/AppDelegate.swift",
    "chars": 3002,
    "preview": "import UIKit\nimport Flutter\nimport Mobile\n\n@UIApplicationMain\n@objc class AppDelegate: FlutterAppDelegate {\n  override f"
  },
  {
    "path": "ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 2519,
    "preview": "{\n  \"images\" : [\n    {\n      \"size\" : \"20x20\",\n      \"idiom\" : \"iphone\",\n      \"filename\" : \"Icon-App-20x20@2x.png\",\n   "
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json",
    "chars": 391,
    "preview": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"filename\" : \"LaunchImage.png\",\n      \"scale\" : \"1x\"\n    },\n  "
  },
  {
    "path": "ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md",
    "chars": 336,
    "preview": "# Launch Screen Assets\n\nYou can customize the launch screen with your own desired assets by replacing the image files in"
  },
  {
    "path": "ios/Runner/Base.lproj/LaunchScreen.storyboard",
    "chars": 2377,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
  },
  {
    "path": "ios/Runner/Base.lproj/Main.storyboard",
    "chars": 1810,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
  },
  {
    "path": "ios/Runner/Info.plist",
    "chars": 2018,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner/Runner-Bridging-Header.h",
    "chars": 38,
    "preview": "#import \"GeneratedPluginRegistrant.h\"\n"
  },
  {
    "path": "ios/Runner.xcodeproj/project.pbxproj",
    "chars": 22968,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 52;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "chars": 135,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "chars": 3291,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1300\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "ios/Runner.xcworkspace/contents.xcworkspacedata",
    "chars": 224,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodepr"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
    "chars": 226,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "l10n.yaml",
    "chars": 145,
    "preview": "arb-dir: lib/l10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: app_localizations.dart\nuntranslated-messages-f"
  },
  {
    "path": "lib/basic/channels/nhentai.dart",
    "chars": 3995,
    "preview": "import 'dart:convert';\nimport 'dart:io';\n\nimport 'package:flutter/services.dart';\nimport 'package:nhentai/basic/entities"
  },
  {
    "path": "lib/basic/common/common.dart",
    "chars": 7434,
    "preview": "import 'package:app_links/app_links.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package"
  },
  {
    "path": "lib/basic/common/cross.dart",
    "chars": 3156,
    "preview": "/// 与平台交互的操作\n\nimport 'dart:io';\nimport 'package:clipboard/clipboard.dart';\nimport 'package:filesystem_picker/filesystem_"
  },
  {
    "path": "lib/basic/common/error_types.dart",
    "chars": 970,
    "preview": "const ERROR_TYPE_NETWORK = \"NETWORK_ERROR\";\nconst ERROR_TYPE_PERMISSION = \"PERMISSION_ERROR\";\nconst ERROR_TYPE_TIME = \"T"
  },
  {
    "path": "lib/basic/configs/proxy.dart",
    "chars": 1124,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/basic/configs/reader_direction.dart",
    "chars": 1658,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/basic/configs/reader_type.dart",
    "chars": 1385,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/basic/configs/themes.dart",
    "chars": 11760,
    "preview": "/// 主题\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:event/event.dart';\nimport 'package"
  },
  {
    "path": "lib/basic/configs/version.dart",
    "chars": 8835,
    "preview": "import 'package:flutter/gestures.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:async"
  },
  {
    "path": "lib/basic/entities/entities.dart",
    "chars": 4026,
    "preview": "import 'package:flutter/cupertino.dart';\n\nclass PageData {\n  late int pageCount;\n\n  PageData.fromJson(Map<String, dynami"
  },
  {
    "path": "lib/l10n/app_en.arb",
    "chars": 1570,
    "preview": "{\n  \"initializing\": \"Initializing\",\n  \"settings\": \"Settings\",\n  \"none\": \"None\",\n  \"webAddress\": \"Web IP redirect\",\n  \"ch"
  },
  {
    "path": "lib/l10n/app_zh.arb",
    "chars": 1225,
    "preview": "{\n  \"initializing\": \"启动中\",\n  \"settings\": \"设置\",\n  \"none\": \"无\",\n  \"webAddress\": \"网站IP重定向\",\n  \"chooseWebAddress\": \"选择网站IP\","
  },
  {
    "path": "lib/main.dart",
    "chars": 1561,
    "preview": "import 'dart:io';\n\nimport 'package:event/event.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimpo"
  },
  {
    "path": "lib/main_desktop.dart",
    "chars": 151,
    "preview": "import 'main.dart' as original_main;\n\n// This file is the default main entry-point for go-flutter application.\nvoid main"
  },
  {
    "path": "lib/screens/comic_downloads_screen.dart",
    "chars": 4656,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/screens/comic_info_screen.dart",
    "chars": 11690,
    "preview": "import 'dart:io';\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:date_format/date_format"
  },
  {
    "path": "lib/screens/comic_reader_screen.dart",
    "chars": 18588,
    "preview": "import 'dart:async';\nimport 'dart:io';\nimport 'package:another_xlider/another_xlider.dart';\nimport 'package:event/event."
  },
  {
    "path": "lib/screens/comic_search_screen.dart",
    "chars": 8779,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/screens/comics_screen.dart",
    "chars": 2880,
    "preview": "import 'dart:io';\n\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\n\nimport 'package:flutter/material.dart'"
  },
  {
    "path": "lib/screens/components/Badged.dart",
    "chars": 1005,
    "preview": "import 'package:flutter/material.dart';\n\nclass Badged extends StatelessWidget {\n  final String? badge;\n  final Widget ch"
  },
  {
    "path": "lib/screens/components/actions.dart",
    "chars": 1037,
    "preview": "import 'package:flutter/material.dart';\nimport 'package:nhentai/basic/configs/version.dart';\n\nimport '../settings_screen"
  },
  {
    "path": "lib/screens/components/content_builder.dart",
    "chars": 1043,
    "preview": "import 'package:flutter/material.dart';\nimport 'content_error.dart';\nimport 'content_loading.dart';\n\nclass ContentBuilde"
  },
  {
    "path": "lib/screens/components/content_error.dart",
    "chars": 3002,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'dart:ui';\n"
  },
  {
    "path": "lib/screens/components/content_loading.dart",
    "chars": 1245,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\n\nclass ContentLoad"
  },
  {
    "path": "lib/screens/components/images.dart",
    "chars": 6562,
    "preview": "import 'package:flutter/foundation.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:io'"
  },
  {
    "path": "lib/screens/components/mouse_and_touch_scroll_behavior.dart",
    "chars": 317,
    "preview": "import 'dart:ui';\nimport 'package:flutter/material.dart';\n\nfinal mouseAndTouchScrollBehavior = MouseAndTouchScrollBehavi"
  },
  {
    "path": "lib/screens/components/pager.dart",
    "chars": 4363,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'dart:async';\n\nimport 'package:flutter/cupertino.da"
  },
  {
    "path": "lib/screens/file_photo_view_screen.dart",
    "chars": 1611,
    "preview": "import 'dart:io';\n\nimport 'package:flutter/material.dart';\nimport 'package:nhentai/basic/common/common.dart';\nimport 'pa"
  },
  {
    "path": "lib/screens/init_screen.dart",
    "chars": 3247,
    "preview": "import 'dart:io';\n\nimport 'package:app_links/app_links.dart';\nimport 'package:flutter_gen/gen_l10n/app_localizations.dar"
  },
  {
    "path": "lib/screens/settings_screen.dart",
    "chars": 861,
    "preview": "import 'package:flutter_gen/gen_l10n/app_localizations.dart';\nimport 'package:flutter/material.dart';\nimport 'package:nh"
  },
  {
    "path": "lib/screens/webview_screen.dart",
    "chars": 2290,
    "preview": "import 'dart:convert';\n\nimport 'package:flutter/material.dart';\nimport 'package:flutter_inappwebview/flutter_inappwebvie"
  },
  {
    "path": "linux/.gitignore",
    "chars": 18,
    "preview": "flutter/ephemeral\n"
  },
  {
    "path": "linux/CMakeLists.txt",
    "chars": 4038,
    "preview": "cmake_minimum_required(VERSION 3.10)\nproject(runner LANGUAGES CXX)\n\nset(BINARY_NAME \"nhentai\")\nset(APPLICATION_ID \"com.e"
  },
  {
    "path": "linux/flutter/CMakeLists.txt",
    "chars": 2742,
    "preview": "cmake_minimum_required(VERSION 3.10)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provid"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.cc",
    "chars": 434,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <url_lau"
  },
  {
    "path": "linux/flutter/generated_plugin_registrant.h",
    "chars": 303,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUG"
  },
  {
    "path": "linux/flutter/generated_plugins.cmake",
    "chars": 760,
    "preview": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  url_launcher_linux\n)\n\nlist(APPEND FLUTTER_FFI_PLUG"
  },
  {
    "path": "linux/main.cc",
    "chars": 180,
    "preview": "#include \"my_application.h\"\n\nint main(int argc, char** argv) {\n  g_autoptr(MyApplication) app = my_application_new();\n  "
  },
  {
    "path": "linux/my_application.cc",
    "chars": 3712,
    "preview": "#include \"my_application.h\"\n\n#include <flutter_linux/flutter_linux.h>\n#ifdef GDK_WINDOWING_X11\n#include <gdk/gdkx.h>\n#en"
  },
  {
    "path": "linux/my_application.h",
    "chars": 388,
    "preview": "#ifndef FLUTTER_MY_APPLICATION_H_\n#define FLUTTER_MY_APPLICATION_H_\n\n#include <gtk/gtk.h>\n\nG_DECLARE_FINAL_TYPE(MyApplic"
  },
  {
    "path": "macos/.gitignore",
    "chars": 89,
    "preview": "# Flutter-related\n**/Flutter/ephemeral/\n**/Pods/\n\n# Xcode-related\n**/dgph\n**/xcuserdata/\n"
  },
  {
    "path": "macos/Flutter/Flutter-Debug.xcconfig",
    "chars": 125,
    "preview": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xccon"
  },
  {
    "path": "macos/Flutter/Flutter-Release.xcconfig",
    "chars": 127,
    "preview": "#include? \"Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig\"\n#include \"ephemeral/Flutter-Generated.xcc"
  },
  {
    "path": "macos/Flutter/GeneratedPluginRegistrant.swift",
    "chars": 374,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\nimport FlutterMacOS\nimport Foundation\n\nimport app_links_macos\nimport url_launche"
  },
  {
    "path": "macos/Podfile",
    "chars": 1330,
    "preview": "platform :osx, '10.11'\n\n# CocoaPods analytics sends network stats synchronously affecting flutter build latency.\nENV['CO"
  },
  {
    "path": "macos/Runner/AppDelegate.swift",
    "chars": 214,
    "preview": "import Cocoa\nimport FlutterMacOS\n\n@NSApplicationMain\nclass AppDelegate: FlutterAppDelegate {\n  override func application"
  },
  {
    "path": "macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1291,
    "preview": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"app_icon_16.png\",\n      \"scale"
  },
  {
    "path": "macos/Runner/Base.lproj/MainMenu.xib",
    "chars": 23482,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.Cocoa.XIB\" version=\"3.0\" toolsVersion"
  },
  {
    "path": "macos/Runner/Configs/AppInfo.xcconfig",
    "chars": 600,
    "preview": "// Application-level settings for the Runner target.\n//\n// This may be replaced with something auto-generated from metad"
  },
  {
    "path": "macos/Runner/Configs/Debug.xcconfig",
    "chars": 77,
    "preview": "#include \"../../Flutter/Flutter-Debug.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Release.xcconfig",
    "chars": 79,
    "preview": "#include \"../../Flutter/Flutter-Release.xcconfig\"\n#include \"Warnings.xcconfig\"\n"
  },
  {
    "path": "macos/Runner/Configs/Warnings.xcconfig",
    "chars": 580,
    "preview": "WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverl"
  },
  {
    "path": "macos/Runner/DebugProfile.entitlements",
    "chars": 348,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner/Info.plist",
    "chars": 1060,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner/MainFlutterWindow.swift",
    "chars": 393,
    "preview": "import Cocoa\nimport FlutterMacOS\n\nclass MainFlutterWindow: NSWindow {\n  override func awakeFromNib() {\n    let flutterVi"
  },
  {
    "path": "macos/Runner/Release.entitlements",
    "chars": 240,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner.xcodeproj/project.pbxproj",
    "chars": 21319,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 51;\n\tobjects = {\n\n/* Begin PBXAggregateTarget sec"
  },
  {
    "path": "macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme",
    "chars": 3258,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1000\"\n   version = \"1.3\">\n   <BuildAction\n      "
  },
  {
    "path": "macos/Runner.xcworkspace/contents.xcworkspacedata",
    "chars": 152,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:Runner.xcodepr"
  },
  {
    "path": "macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "pubspec.yaml",
    "chars": 757,
    "preview": "name: nhentai\ndescription: nhentai client\npublish_to: 'none'\nversion: 0.0.9+2\n\nenvironment:\n  sdk: \">=2.12.0 <3.0.0\"\n\nde"
  },
  {
    "path": "scripts/README.md",
    "chars": 15,
    "preview": "用于记录作者构建时使用的脚本\n"
  },
  {
    "path": "scripts/bind-android-debug.sh",
    "chars": 183,
    "preview": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -androidapi 19 -target=android/ar"
  },
  {
    "path": "scripts/bind-ios-arm64.sh",
    "chars": 143,
    "preview": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -androidapi 19 -target=ios -o lib"
  },
  {
    "path": "scripts/bind-ios.sh",
    "chars": 145,
    "preview": "# 编译所有架构的依赖\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\n\ngomobile bind -iosversion 11.0 -target=ios -o l"
  },
  {
    "path": "scripts/build-apk-arm.sh",
    "chars": 238,
    "preview": "# 仅构建arm的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile "
  },
  {
    "path": "scripts/build-apk-arm64.sh",
    "chars": 244,
    "preview": "# 仅构建arm64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobil"
  },
  {
    "path": "scripts/build-apk-x64.sh",
    "chars": 243,
    "preview": "# 仅构建x86_64的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobi"
  },
  {
    "path": "scripts/build-apk-x86.sh",
    "chars": 238,
    "preview": "# 仅构建x86的APK\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile "
  },
  {
    "path": "scripts/build-ipa.sh",
    "chars": 355,
    "preview": "# 构建未签名的IPA\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\ncd go/mobile\ngo get golang.org/x/mobile/cmd/gobind\ngomobile b"
  },
  {
    "path": "scripts/build-macos-dmg.sh",
    "chars": 79,
    "preview": "# 构建macos\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\nhover build darwin-dmg\n"
  },
  {
    "path": "scripts/sign-apk-github-actions.sh",
    "chars": 247,
    "preview": "cd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\necho $KEY_FILE_BASE64 > key.jks.base64\nbase64 -d key.jks.base64 > key.jks\n"
  },
  {
    "path": "scripts/thin-payload.sh",
    "chars": 633,
    "preview": "# 精简Payload文件夹 (上传到AppStore会自动区分平台, 此代码仅用于构建非签名ipa)\n\nforeachThin(){\n  for file in $1/*\n  do\n      if test -f $file\n     "
  },
  {
    "path": "scripts/version.sh",
    "chars": 220,
    "preview": "# 设置版本号\n\ncd \"$( cd \"$( dirname \"$0\"  )\" && pwd  )/..\"\n\nif [ \"$1\" == \"set\" ] ; then\n  if [ \"$2\" != \"\" ] ; then\n    echo $"
  },
  {
    "path": "test/widget_test.dart",
    "chars": 1052,
    "preview": "// This is a basic Flutter widget test.\n//\n// To perform an interaction with a widget in your test, use the WidgetTester"
  },
  {
    "path": "windows/.gitignore",
    "chars": 291,
    "preview": "flutter/ephemeral/\n\n# Visual Studio user-specific files.\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# Visual Studio bu"
  },
  {
    "path": "windows/CMakeLists.txt",
    "chars": 3419,
    "preview": "cmake_minimum_required(VERSION 3.15)\nproject(nhentai LANGUAGES CXX)\n\nset(BINARY_NAME \"nhentai\")\n\ncmake_policy(SET CMP006"
  },
  {
    "path": "windows/flutter/CMakeLists.txt",
    "chars": 3489,
    "preview": "cmake_minimum_required(VERSION 3.15)\n\nset(EPHEMERAL_DIR \"${CMAKE_CURRENT_SOURCE_DIR}/ephemeral\")\n\n# Configuration provid"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.cc",
    "chars": 694,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#include \"generated_plugin_registrant.h\"\n\n#include <app_lin"
  },
  {
    "path": "windows/flutter/generated_plugin_registrant.h",
    "chars": 302,
    "preview": "//\n//  Generated file. Do not edit.\n//\n\n// clang-format off\n\n#ifndef GENERATED_PLUGIN_REGISTRANT_\n#define GENERATED_PLUG"
  },
  {
    "path": "windows/flutter/generated_plugins.cmake",
    "chars": 815,
    "preview": "#\n# Generated file, do not edit.\n#\n\nlist(APPEND FLUTTER_PLUGIN_LIST\n  app_links_windows\n  permission_handler_windows\n  u"
  },
  {
    "path": "windows/runner/CMakeLists.txt",
    "chars": 572,
    "preview": "cmake_minimum_required(VERSION 3.15)\nproject(runner LANGUAGES CXX)\n\nadd_executable(${BINARY_NAME} WIN32\n  \"flutter_windo"
  },
  {
    "path": "windows/runner/Runner.rc",
    "chars": 2863,
    "preview": "// Microsoft Visual C++ generated resource script.\n//\n#pragma code_page(65001)\n#include \"resource.h\"\n\n#define APSTUDIO_R"
  },
  {
    "path": "windows/runner/flutter_window.cpp",
    "chars": 1762,
    "preview": "#include \"flutter_window.h\"\n\n#include <optional>\n\n#include \"flutter/generated_plugin_registrant.h\"\n\nFlutterWindow::Flutt"
  },
  {
    "path": "windows/runner/flutter_window.h",
    "chars": 928,
    "preview": "#ifndef RUNNER_FLUTTER_WINDOW_H_\n#define RUNNER_FLUTTER_WINDOW_H_\n\n#include <flutter/dart_project.h>\n#include <flutter/f"
  },
  {
    "path": "windows/runner/main.cpp",
    "chars": 1267,
    "preview": "#include <flutter/dart_project.h>\n#include <flutter/flutter_view_controller.h>\n#include <windows.h>\n\n#include \"flutter_w"
  },
  {
    "path": "windows/runner/resource.h",
    "chars": 432,
    "preview": "//{{NO_DEPENDENCIES}}\n// Microsoft Visual C++ generated include file.\n// Used by Runner.rc\n//\n#define IDI_APP_ICON      "
  },
  {
    "path": "windows/runner/runner.exe.manifest",
    "chars": 859,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersi"
  },
  {
    "path": "windows/runner/utils.cpp",
    "chars": 1651,
    "preview": "#include \"utils.h\"\n\n#include <flutter_windows.h>\n#include <io.h>\n#include <stdio.h>\n#include <windows.h>\n\n#include <iost"
  },
  {
    "path": "windows/runner/utils.h",
    "chars": 672,
    "preview": "#ifndef RUNNER_UTILS_H_\n#define RUNNER_UTILS_H_\n\n#include <string>\n#include <vector>\n\n// Creates a console for the proce"
  },
  {
    "path": "windows/runner/win32_window.cpp",
    "chars": 7024,
    "preview": "#include \"win32_window.h\"\n\n#include <flutter_windows.h>\n\n#include \"resource.h\"\n\nnamespace {\n\nconstexpr const wchar_t kWi"
  },
  {
    "path": "windows/runner/win32_window.h",
    "chars": 3350,
    "preview": "#ifndef RUNNER_WIN32_WINDOW_H_\n#define RUNNER_WIN32_WINDOW_H_\n\n#include <windows.h>\n\n#include <functional>\n#include <mem"
  }
]

About this extraction

This page contains the full source code of the niuhuan/nhentai-cross GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 173 files (378.6 KB), approximately 105.0k tokens, and a symbol index with 443 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!