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) | 简体中文
[](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)
[](https://github.com/niuhuan/nhentai-cross/releases)
[](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拦截器不一定有效, 请使用科学上网测试本软件的可用性
## 软件截图
#### 漫画列表

#### 漫画详情

#### 漫画阅读器

## 技术架构

================================================
FILE: README.md
================================================
# NHENTAI-CROSS
## English | [简体中文](README-zh.md)
[](https://raw.githubusercontent.com/niuhuan/nhentai-cross/master/LICENSE)
[](https://github.com/niuhuan/nhentai-cross/releases)
[](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

#### Comic info

#### Comic reader

## Technical architecture

================================================
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), ¶msStruct)
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), ¶msStruct)
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), ¶msStruct)
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), ¶msStruct)
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), ¶msStruct)
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), ¶msStruct)
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
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
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[\n\n[\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.