diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..43f2dec
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,52 @@
+# 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
+
+# PROJECT
+/go/mobile/lib/*.aar
+/go/mobile/lib/*.jar
+/go/mobile/lib/*.framework/
+/go/vendor/
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..56bfc2c
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,10 @@
+# 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: f4abaa0735eba4dfd8f33f73363911d63931fe03
+ channel: stable
+
+project_type: app
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..aeb2ac1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2021-2021 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.
diff --git a/README.md b/README.md
index e69de29..b6d1b5e 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,148 @@
+PIKAPI - 漫画客户端
+========
+[![license](https://img.shields.io/github/license/niuhuan/pikapi-flutter)](https://raw.githubusercontent.com/niuhuan/pikapi-flutter/master/LICENSE)
+[![releases](https://img.shields.io/github/v/release/niuhuan/pikapi-flutter)](https://github.com/niuhuan/pikapi-flutter/releases)
+[![downloads](https://img.shields.io/github/downloads/niuhuan/pikapi-flutter/total)](https://github.com/niuhuan/pikapi-flutter/releases)
+
+- 美观易用且无广告的漫画客户端, 能运行在Windows/MacOS/Linux/Android/IOS中。
+- 本仓库仅作为学习交流使用, 请您遵守当地法律法规以及开源协议。
+- 您的star和issue是对开发者的莫大鼓励, 可以源仓库下载最新的源码/安装包, 表示支持/提出建议。
+- 源仓库地址 [https://github.com/niuhuan/pikapi-flutter](https://github.com/niuhuan/pikapi-flutter)
+- 此项目仅接受简体中文的issues。
+
+## 界面 / 功能
+
+![阅读器](images/reader.png)
+
+### 分流
+
+VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN上访问代理, 使用代理请求分流服务器。
+
+### 漫画分类/搜索
+
+![分类](images/categories_screen.png) ![列表](images/comic_list.png)
+
+### 漫画阅读/下载/导入/导出
+
+您可以在除IOS外导出任意已经完成的下载到zip, 从另外一台设备导入。 导出的zip解压后可以直接使用其中的HTML进行阅读
+
+![导出下载](images/exporting.png)
+
+![HTML预览](images/exporting2.png)
+
+### 游戏
+
+![games](images/games.png)
+![game](images/game.png)
+
+## 特性
+
+- [x] 用户
+ - [x] 登录 / 注册 / 获取个人信息 / 自动打卡
+- [x] 漫画
+ - [x] 分类 / 搜索 / 随机本子 / 看此本子的也在看 / 排行榜
+ - [x] 在分类中搜索 / 按 "分类 / 标签 / 创建人 / 汉化组" 检索
+ - [x] 漫画详情 / 章节 / 看图 / 将图片保存到相册
+ - [x] 收藏 / 喜欢
+ - [x] 获取评论 / 评论 / 评论回复 (社区评论后无法删除, 请谨慎使用)
+- [x] 游戏
+ - [x] 列表 / 详情 / 无广告下载
+- [x] 下载
+ - [x] 导入导出 / 无线共享 / 移动设备与PC设备传输
+- [ ] 聊天室
+- [x] 缓存 / 清理
+- [x] 设备支持
+ - [x] 安卓
+ - [x] 高刷新频率屏幕适配 (90/120/144... Hz)
+ - [x] 安卓10以上随系统进入深色/夜间模式
+
+## 其他说明
+
+- 在ios/android环境 数据文件将会保存在程序自身数据目录中, 删除就会清理
+- 在 windows 数据文件将会保存在程序同一目录
+- 在 macos 数据文件将会"~/Library/Application Support/pikapi"
+- 在 linux 数据文件将会"~/.pikapi"
+
+## 运行 / 构建
+
+这个应用程序使用golang和dart(flutter)作为主要语言, 可以兼容Windows, linux, MacOS, Android, IOS
+
+使用了不同的框架桥接到桌面和移动平台上
+
+- go-flutter => Windows / MacOS / Linux
+- gomobile => Android / IOS
+
+![平台](images/platforms.png)
+
+### 开发环境准备
+
+- [golang](https://golang.org/) (1.16以上版本)
+- [flutter](https://flutter.dev/) (桌面端 Tag 2.2.3 以兼容hover)
+
+### 环境配置
+
+- 将~/go/bin (GoPath/bin) 设置到PATH环境变量内
+- golang开启模块化
+- 设置GoProxy (可选,在中国大陆网络建议设置)
+- 参考地址 [https://goproxy.cn/](https://goproxy.cn/)
+
+### 桌面平台 (go-flutter)
+
+- [安装hover(go-flutter编译脚手架)](https://github.com/go-flutter-desktop/hover)
+ ```shell
+ GO111MODULE=on go get -u -a github.com/go-flutter-desktop/hover
+ ```
+- 执行编译命令 ($system替换为windows/darwin等)
+ ```shell
+ hover run
+ hover build $system
+ ```
+
+### Linux的附加说明
+
+- linux编译可能会遇到的问题
+ ```shell
+ # No package 'gl' found
+ sudo apt install libgl1-mesa-dev
+ # X11/Xlib.h: No such file or directory
+ # 或者更多x11的头找不到等
+ sudo apt install xorg-dev
+ ```
+- 字体不显示
+ 1. 将字体文件复制到项目目录下
+ ```shell
+ cp /usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf fonts/
+ ```
+ 2. 设置flutter打包的字体
+ ```yaml
+ fonts:
+ - family: Roboto
+ fonts:
+ - asset: fonts/DroidSansFallbackFull.ttf
+ ```
+
+### 移动端 (gomobile)
+
+- [安装gomobile](https://github.com/golang/mobile)
+ ```shell
+ go install golang.org/x/mobile/cmd/gomobile@latest
+ go get golang.org/x/mobile/cmd/gobind
+ ```
+- 执行编译命令 (bind-android.sh/bind-ios.sh根据平台选择, $system替换为apk/ipa等)
+ ```shell
+ cd go/mobile
+ sh bind-ios.sh
+ sh bind-android.sh
+ cd ../../
+ flutter build $system
+ ```
+
+## 请您遵守使用规则
+
+本软件或本软件的拓展, 个人或企业不可用于商业用途, 不可上架任何商店
+
+拓展包括但是不限于以下内容
+
+- 使用本软件进行继续开发形成的软件。
+- 引入本软件部分内容为依赖/参考本软件/使用本软件内代码的同时, 包含本软件内一致内容或功能的软件。
+- 直接对本软件进行打包发布
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..0a741cb
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,11 @@
+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
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..3cf3bfa
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,64 @@
+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 30
+
+ 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.pikapi"
+ minSdkVersion 16
+ targetSdkVersion 30
+ 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
+ ndk {
+ abiFilters 'armeabi-v7a'
+ }
+ }
+ }
+}
+
+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"])
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..d811c4f
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dcd3814
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt
new file mode 100644
index 0000000..444d785
--- /dev/null
+++ b/android/app/src/main/kotlin/niuhuan/pikapi/MainActivity.kt
@@ -0,0 +1,278 @@
+package niuhuan.pikapi
+
+import android.content.ContentValues
+import android.content.res.Configuration
+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.view.Display
+import android.view.KeyEvent
+import androidx.annotation.NonNull
+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.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) {
+ uiThreadHandler.post {
+ error("", e.message, "")
+ }
+ }
+
+ }
+ }
+
+ override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
+ super.configureFlutterEngine(flutterEngine)
+ Mobile.initApplication(context!!.filesDir.absolutePath)
+ // Method Channel
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "method").setMethodCallHandler { call, result ->
+ result.withCoroutine {
+ when (call.method) {
+ "flatInvoke" -> {
+ Mobile.flatInvoke(
+ call.argument("method")!!,
+ call.argument("params")!!
+ )
+ }
+ "androidSaveFileToImage" -> {
+ saveImage(call.argument("path")!!)
+ }
+ "androidGetModes" -> {
+ modes()
+ }
+ "androidSetMode" -> {
+ setMode(call.argument("mode")!!)
+ }
+ "androidGetUiMode" -> {
+ uiMode()
+ }
+ "androidGetVersion" -> Build.VERSION.SDK_INT
+ 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)
+
+ //
+ EventChannel(flutterEngine.dartExecutor.binaryMessenger, "ui_mode")
+ .setStreamHandler(uiModeStreamHandler)
+ }
+
+ // save_image
+
+ private fun saveImage(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)
+ }
+ }
+ }
+ }
+
+ // 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 {
+ 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 ->
+ 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)
+ }
+
+ // ui_mode
+
+ private var uiModeEvents: EventChannel.EventSink? = null
+
+ private val uiModeStreamHandler = object : EventChannel.StreamHandler {
+
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ uiModeEvents = events
+ }
+
+ override fun onCancel(arguments: Any?) {
+ uiModeEvents = null
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+ Configuration.UI_MODE_NIGHT_YES -> {
+ uiModeEvents?.let { it.success("NIGHT") }
+ }
+ Configuration.UI_MODE_NIGHT_NO -> {
+ uiModeEvents?.let { it.success("NORMAL") }
+ }
+ }
+ super.onConfigurationChanged(newConfig)
+ }
+
+ private fun uiMode(): String {
+ return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+ Configuration.UI_MODE_NIGHT_YES -> "NIGHT"
+ else -> "NORMAL"
+ }
+ }
+
+
+}
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..f745851
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..cb7d6bc
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..ff94e25
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..da88d84
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..0ec2954
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..449a9f9
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..d74aa35
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..39cb93a
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..9b6ed06
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,29 @@
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..bc6a58a
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#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
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,11 @@
+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"
diff --git a/go/.gitignore b/go/.gitignore
new file mode 100644
index 0000000..fd0e2eb
--- /dev/null
+++ b/go/.gitignore
@@ -0,0 +1,3 @@
+build
+.last_goflutter_check
+.last_go-flutter_check
diff --git a/go/assets/icon.png b/go/assets/icon.png
new file mode 100644
index 0000000..f14bdd1
Binary files /dev/null and b/go/assets/icon.png differ
diff --git a/go/cmd/init.go b/go/cmd/init.go
new file mode 100644
index 0000000..fddcc97
--- /dev/null
+++ b/go/cmd/init.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "path"
+ path2 "path"
+ "path/filepath"
+ "pgo/pikapi/config"
+ "runtime"
+ "strings"
+)
+
+func init() {
+ applicationDir, err := os.UserHomeDir()
+ if err != nil {
+ panic(err)
+ }
+ switch runtime.GOOS {
+ case "windows":
+ // applicationDir = path.Join(applicationDir, "AppData", "Roaming")
+ file, err := exec.LookPath(os.Args[0])
+ if err != nil {
+ panic(err)
+ }
+ path, err := filepath.Abs(file)
+ if err != nil {
+ panic(err)
+ }
+ i := strings.LastIndex(path, "/")
+ if i < 0 {
+ i = strings.LastIndex(path, "\\")
+ }
+ if i < 0 {
+ panic(errors.New(" can't find \"/\" or \"\\\""))
+ }
+ applicationDir = path2.Join(path[0:i+1], "data", "pikapi")
+ case "darwin":
+ applicationDir = path.Join(applicationDir, "Library", "Application Support", "pikapi")
+ case "linux":
+ applicationDir = path.Join(applicationDir, ".pikapi")
+ 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)
+ }
+ }
+ config.InitApplication(applicationDir)
+}
diff --git a/go/cmd/main.go b/go/cmd/main.go
new file mode 100644
index 0000000..166c11c
--- /dev/null
+++ b/go/cmd/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "fmt"
+ "github.com/go-flutter-desktop/go-flutter"
+ "github.com/pkg/errors"
+ "image"
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+ "os"
+ "path/filepath"
+ "pgo/pikapi/database/properties"
+ "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
+ }
+ sizeOption := flutter.WindowInitialDimensions(width, height)
+ options = append(options, sizeOption)
+ //
+ err := flutter.Run(append(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
+}
diff --git a/go/cmd/options.go b/go/cmd/options.go
new file mode 100644
index 0000000..a58f795
--- /dev/null
+++ b/go/cmd/options.go
@@ -0,0 +1,14 @@
+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"
+ "pgo/cmd/plugin/pikapi"
+)
+
+var options = []flutter.Option{
+ flutter.AddPlugin(&pikapi.Plugin{}),
+ flutter.AddPlugin(&file_picker.FilePickerPlugin{}),
+ flutter.AddPlugin(&url_launcher.UrlLauncherPlugin{}),
+}
diff --git a/go/cmd/plugin/pikapi/plugin.go b/go/cmd/plugin/pikapi/plugin.go
new file mode 100644
index 0000000..a09d1f6
--- /dev/null
+++ b/go/cmd/plugin/pikapi/plugin.go
@@ -0,0 +1,72 @@
+package pikapi
+
+import (
+ "errors"
+ "github.com/go-flutter-desktop/go-flutter/plugin"
+ "github.com/go-gl/glfw/v3.3/glfw"
+ "pgo/pikapi/controller"
+ "pgo/pikapi/database/properties"
+ "strconv"
+ "sync"
+)
+
+var eventMutex = sync.Mutex{}
+var eventSink *plugin.EventSink
+
+type EventHandler struct {
+}
+
+func (s *EventHandler) OnListen(arguments interface{}, sink *plugin.EventSink) {
+ eventMutex.Lock()
+ defer eventMutex.Unlock()
+ eventSink = sink
+}
+
+func (s *EventHandler) OnCancel(arguments interface{}) {
+ eventMutex.Lock()
+ defer eventMutex.Unlock()
+ eventSink = nil
+}
+
+const channelName = "method"
+
+type Plugin struct {
+}
+
+func (p *Plugin) InitPlugin(messenger plugin.BinaryMessenger) error {
+
+ channel := plugin.NewMethodChannel(messenger, channelName, 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 controller.FlatInvoke(method, params)
+ }
+ }
+ }
+ return nil, errors.New("params error")
+ })
+
+ exporting := plugin.NewEventChannel(messenger, "flatEvent", plugin.StandardMethodCodec{})
+ exporting.Handle(&EventHandler{})
+
+ controller.EventNotify = func(message string) {
+ eventMutex.Lock()
+ defer eventMutex.Unlock()
+ sink := eventSink
+ if sink != nil {
+ sink.Success(message)
+ }
+ }
+
+ return nil // no error
+}
+
+func (p *Plugin) InitPluginGLFW(window *glfw.Window) error {
+ window.SetSizeCallback(func(w *glfw.Window, width int, height int) {
+ properties.SaveProperty("window_width", strconv.Itoa(width))
+ properties.SaveProperty("window_height", strconv.Itoa(height))
+ })
+ return nil
+}
diff --git a/go/go.mod b/go/go.mod
new file mode 100644
index 0000000..e9d7dfb
--- /dev/null
+++ b/go/go.mod
@@ -0,0 +1,17 @@
+module pgo
+
+go 1.16
+
+require (
+ github.com/PuerkitoBio/goquery v1.7.1
+ github.com/go-flutter-desktop/go-flutter v0.43.0
+ github.com/go-flutter-desktop/plugins/url_launcher v0.1.2
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265
+ github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20210622152105-9f0a811028a0
+ github.com/niuhuan/pica-go v0.0.0-20210923020558-090104e7b1a7
+ github.com/pkg/errors v0.9.1
+ golang.org/x/image v0.0.0-20190802002840-cff245a6509b
+ golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
+ gorm.io/driver/sqlite v1.1.4
+ gorm.io/gorm v1.21.12
+)
diff --git a/go/go.sum b/go/go.sum
new file mode 100644
index 0000000..886738f
--- /dev/null
+++ b/go/go.sum
@@ -0,0 +1,81 @@
+github.com/PuerkitoBio/goquery v1.7.1 h1:oE+T06D+1T7LNrn91B4aERsRIeCLJ/oPSa6xB9FPnz4=
+github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY=
+github.com/Xuanwo/go-locale v1.0.0 h1:oqC32Kyiu2XZq+fxtwEg0mWiv9WyDhyHu+sT5cDkgME=
+github.com/Xuanwo/go-locale v1.0.0/go.mod h1:kB9tcLfr4Sp+ByIE9SE7vbUkXkGQqel2XH3EHpL0haA=
+github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
+github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
+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.43.0 h1:7tdUbGKmHwdsUnBfC/h7zAO3T67cAkKSCWi9ZDFg25A=
+github.com/go-flutter-desktop/go-flutter v0.43.0/go.mod h1:GSCn6XOpB0cnYlK9/BdSwxi99t5YD1XEk0v4agI7SS4=
+github.com/go-flutter-desktop/plugins/url_launcher v0.1.2 h1:oFiIJjotMQvF8rfKWVJrf+1/JgTXShEIsibkiXrQnUw=
+github.com/go-flutter-desktop/plugins/url_launcher v0.1.2/go.mod h1:GYgRDaLDAJRYvaASQk8HEmI8YJurbZGW5VVDIMxwzBU=
+github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
+github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
+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-20201108214237-06ea97f0c265 h1:BcbKYUZo/TKPsiSh7LymK3p+TNAJJW3OfGO/21sBbiA=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+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.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
+github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+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.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
+github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
+github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20210622152105-9f0a811028a0 h1:hXl9AMW20Php3xWlWZr2Acw50tqeblLgtLfLoRCACmA=
+github.com/miguelpruivo/flutter_file_picker/go v0.0.0-20210622152105-9f0a811028a0/go.mod h1:csuW+TFyYKtiUwNvcvhcpyX4quPI7Pvv0SUogdqCW4I=
+github.com/niuhuan/pica-go v0.0.0-20210923020558-090104e7b1a7 h1:E0WsH0UeFvuGiaEb1/tyy35ot76YDJKZ2q0/QjRQMWA=
+github.com/niuhuan/pica-go v0.0.0-20210923020558-090104e7b1a7/go.mod h1:fx2m+OgMeEZf6/TrfblV9i85SjPsOGbnjIL2gohxP4M=
+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.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3/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-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+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.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
+gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
+gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.21.12 h1:3fQM0Eiz7jcJEhPggHEpoYnsGZqynMzverL77DV40RM=
+gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
diff --git a/go/hover.yaml b/go/hover.yaml
new file mode 100644
index 0000000..355dc91
--- /dev/null
+++ b/go/hover.yaml
@@ -0,0 +1,9 @@
+#application-name: "pikapi" # Uncomment to modify this value.
+#executable-name: "pikapi" # Uncomment to modify this value. Only lowercase a-z, numbers, underscores and no spaces
+#package-name: "pikapi" # Uncomment to modify this value. Only lowercase a-z, numbers and no underscores or spaces
+organization-name: "com.pikapi"
+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
diff --git a/go/mobile/bind-android-debug.sh b/go/mobile/bind-android-debug.sh
new file mode 100644
index 0000000..8bf267d
--- /dev/null
+++ b/go/mobile/bind-android-debug.sh
@@ -0,0 +1 @@
+gomobile bind -target=android/arm,android/arm64,android/386 -o lib/Pikapi.aar ./
diff --git a/go/mobile/bind-android.sh b/go/mobile/bind-android.sh
new file mode 100644
index 0000000..7efb1cb
--- /dev/null
+++ b/go/mobile/bind-android.sh
@@ -0,0 +1 @@
+gomobile bind -target=android/arm -o lib/Pikapi.aar ./
diff --git a/go/mobile/bind-ios.sh b/go/mobile/bind-ios.sh
new file mode 100644
index 0000000..3442a6e
--- /dev/null
+++ b/go/mobile/bind-ios.sh
@@ -0,0 +1 @@
+gomobile bind -target=ios -o lib/Pikapi.framework ./
diff --git a/go/mobile/lib/.keep b/go/mobile/lib/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/go/mobile/mobile.go b/go/mobile/mobile.go
new file mode 100644
index 0000000..96535bc
--- /dev/null
+++ b/go/mobile/mobile.go
@@ -0,0 +1,22 @@
+package mobile
+
+import (
+ "pgo/pikapi/config"
+ "pgo/pikapi/controller"
+)
+
+func InitApplication(application string) {
+ config.InitApplication(application)
+}
+
+func FlatInvoke(method string, params string) (string, error) {
+ return controller.FlatInvoke(method, params)
+}
+
+func EventNotify(notify EventNotifyHandler) {
+ controller.EventNotify = notify.OnNotify
+}
+
+type EventNotifyHandler interface {
+ OnNotify(message string)
+}
diff --git a/go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl b/go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl
new file mode 100644
index 0000000..e49ccf2
--- /dev/null
+++ b/go/packaging/darwin-bundle/{{.applicationName}} {{.version}}.app/Contents/Info.plist.tmpl
@@ -0,0 +1,38 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleExecutable
+ {{.executableName}}
+ CFBundleGetInfoString
+ {{.description}}
+ CFBundleIconFile
+ icon.icns
+ NSHighResolutionCapable
+
+ CFBundleIdentifier
+ {{.organizationName}}.{{.packageName}}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLongVersionString
+ {{.version}}
+ CFBundleName
+ {{.applicationName}}
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ {{.version}}
+ CFBundleSignature
+ {{.organizationName}}.{{.packageName}}
+ CFBundleVersion
+ {{.version}}
+ CSResourcesFileMapped
+
+ NSHumanReadableCopyright
+
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/go/packaging/linux-appimage/AppRun.tmpl b/go/packaging/linux-appimage/AppRun.tmpl
new file mode 100644
index 0000000..9272c3d
--- /dev/null
+++ b/go/packaging/linux-appimage/AppRun.tmpl
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd "$(dirname "$0")"
+exec ./build/{{.executableName}}
diff --git a/go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl b/go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl
new file mode 100644
index 0000000..cd17a38
--- /dev/null
+++ b/go/packaging/linux-appimage/{{.packageName}}.desktop.tmpl
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Terminal=false
+Categories=
+Comment={{.description}}
+Name={{.applicationName}}
+Icon={{.iconPath}}
+Exec={{.executablePath}}
diff --git a/go/pikapi/config/common.go b/go/pikapi/config/common.go
new file mode 100644
index 0000000..2c8d1f1
--- /dev/null
+++ b/go/pikapi/config/common.go
@@ -0,0 +1,29 @@
+package config
+
+import (
+ "path"
+ "pgo/pikapi/controller"
+ "pgo/pikapi/database/comic_center"
+ "pgo/pikapi/database/network_cache"
+ "pgo/pikapi/database/properties"
+ "pgo/pikapi/utils"
+)
+
+// InitApplication 初始化文件保存的位置
+func InitApplication(applicationDir string) {
+ println("初始化 : " + applicationDir)
+ var databasesDir, remoteDir, downloadDir, tmpDir string
+ databasesDir = path.Join(applicationDir, "databases")
+ remoteDir = path.Join(applicationDir, "pictures", "remote")
+ downloadDir = path.Join(applicationDir, "download")
+ tmpDir = path.Join(applicationDir, "download")
+ utils.Mkdir(databasesDir)
+ utils.Mkdir(remoteDir)
+ utils.Mkdir(downloadDir)
+ utils.Mkdir(tmpDir)
+ properties.InitDBConnect(databasesDir)
+ network_cache.InitDBConnect(databasesDir)
+ comic_center.InitDBConnect(databasesDir)
+ controller.InitClient()
+ controller.InitPlugin(remoteDir, downloadDir, tmpDir)
+}
diff --git a/go/pikapi/const_value/common.go b/go/pikapi/const_value/common.go
new file mode 100644
index 0000000..58db97d
--- /dev/null
+++ b/go/pikapi/const_value/common.go
@@ -0,0 +1,16 @@
+package const_value
+
+import (
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+ "os"
+)
+
+var (
+ CreateDirMode = os.FileMode(0700)
+ CreateFileMode = os.FileMode(0600)
+ GormConfig = &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Info),
+ }
+)
+
diff --git a/go/pikapi/controller/client.go b/go/pikapi/controller/client.go
new file mode 100644
index 0000000..2ad40e1
--- /dev/null
+++ b/go/pikapi/controller/client.go
@@ -0,0 +1,459 @@
+package controller
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ source "github.com/niuhuan/pica-go"
+ "net"
+ "net/http"
+ "net/url"
+ "pgo/pikapi/database/comic_center"
+ "pgo/pikapi/database/network_cache"
+ "pgo/pikapi/database/properties"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func InitClient() {
+ client.Timeout = time.Second * 60
+ switchAddress, _ = properties.LoadSwitchAddress()
+ proxy, _ := properties.LoadProxy()
+ changeProxyUrl(proxy)
+}
+
+var client = source.Client{}
+var dialer = &net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+}
+
+// SwitchAddress
+// addr = "172.67.7.24:443"
+// addr = "104.20.180.50:443"
+// addr = "172.67.208.169:443"
+var switchAddress = ""
+var switchAddressPattern, _ = regexp.Compile("^.+picacomic\\.com:\\d+$")
+
+func switchAddressContext(ctx context.Context, network, addr string) (net.Conn, error) {
+ if switchAddressPattern.MatchString(addr) && switchAddress != "" {
+ addr = switchAddress
+ }
+ return dialer.DialContext(ctx, network, addr)
+}
+
+func changeProxyUrl(urlStr string) bool {
+ if urlStr == "" {
+ client.Transport = &http.Transport{
+ TLSHandshakeTimeout: time.Second * 10,
+ ExpectContinueTimeout: time.Second * 10,
+ ResponseHeaderTimeout: time.Second * 10,
+ IdleConnTimeout: time.Second * 10,
+ DialContext: switchAddressContext,
+ }
+ return false
+ }
+ client.Transport = &http.Transport{
+ Proxy: func(_ *http.Request) (*url.URL, error) {
+ return url.Parse(urlStr)
+ },
+ TLSHandshakeTimeout: time.Second * 10,
+ ExpectContinueTimeout: time.Second * 10,
+ ResponseHeaderTimeout: time.Second * 10,
+ IdleConnTimeout: time.Second * 10,
+ DialContext: switchAddressContext,
+ }
+ return true
+}
+
+func cacheable(key string, expire time.Duration, reload func() (interface{}, error)) (string, error) {
+ // CACHE
+ cache := network_cache.LoadCache(key, expire)
+ if cache != "" {
+ return cache, nil
+ }
+ // obj
+ obj, err := reload()
+ if err != nil {
+ return "", err
+ }
+ buff, err := json.Marshal(obj)
+ // push to cache
+ if err != nil {
+ return "", err
+ }
+ // return
+ cache = string(buff)
+ network_cache.SaveCache(key, cache)
+ return cache, nil
+}
+
+func categories() (string, error) {
+ key := "CATEGORIES"
+ expire := time.Hour * 3
+ cache := network_cache.LoadCache(key, expire)
+ if cache != "" {
+ return cache, nil
+ }
+ categories, err := client.Categories()
+ if err != nil {
+ return "", err
+ }
+ var dbCategories []comic_center.Category
+ for _, c := range categories {
+ dbCategories = append(dbCategories, comic_center.Category{
+ ID: c.Id,
+ Title: c.Title,
+ Description: c.Description,
+ IsWeb: c.IsWeb,
+ Active: c.Active,
+ Link: c.Link,
+ ThumbOriginalName: c.Thumb.OriginalName,
+ ThumbFileServer: c.Thumb.FileServer,
+ ThumbPath: c.Thumb.Path,
+ })
+ }
+ err = comic_center.UpSetCategories(&dbCategories)
+ if err != nil {
+ return "", err
+ }
+ buff, _ := json.Marshal(&categories)
+ cache = string(buff)
+ network_cache.SaveCache(key, cache)
+ return cache, nil
+}
+
+func comics(params string) (string, error) {
+ var paramsStruct struct {
+ Category string `json:"category"`
+ Tag string `json:"tag"`
+ CreatorId string `json:"creatorId"`
+ ChineseTeam string `json:"chineseTeam"`
+ Sort string `json:"sort"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ return cacheable(
+ fmt.Sprintf("COMICS$%s$%s$%s$%s$%s$%d", paramsStruct.Category, paramsStruct.Tag, paramsStruct.CreatorId, paramsStruct.ChineseTeam, paramsStruct.Sort, paramsStruct.Page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.Comics(paramsStruct.Category, paramsStruct.Tag, paramsStruct.CreatorId, paramsStruct.ChineseTeam, paramsStruct.Sort, paramsStruct.Page)
+ },
+ )
+}
+
+func searchComics(params string) (string, error) {
+ var paramsStruct struct {
+ Categories []string `json:"categories"`
+ Keyword string `json:"keyword"`
+ Sort string `json:"sort"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ categories := paramsStruct.Categories
+ keyword := paramsStruct.Keyword
+ sort := paramsStruct.Sort
+ page := paramsStruct.Page
+ //
+ var categoriesInKey string
+ if len(categories) == 0 {
+ categoriesInKey = ""
+ } else {
+ b, _ := json.Marshal(categories)
+ categoriesInKey = string(b)
+ }
+ return cacheable(
+ fmt.Sprintf("SEARCH$%s$%s$%s$%d", categoriesInKey, keyword, sort, page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.SearchComics(categories, keyword, sort, page)
+ },
+ )
+}
+
+func randomComics() (string, error) {
+ return cacheable(
+ fmt.Sprintf("RANDOM"),
+ time.Millisecond*1,
+ func() (interface{}, error) {
+ return client.RandomComics()
+ },
+ )
+}
+
+func leaderboard(typeName string) (string, error) {
+ return cacheable(
+ fmt.Sprintf("LEADERBOARD$%s", typeName),
+ time.Second*200,
+ func() (interface{}, error) {
+ return client.Leaderboard(typeName)
+ },
+ )
+}
+
+func comicInfo(comicId string) (string, error) {
+ var err error
+ var comic *source.ComicInfo
+ // cache
+ key := fmt.Sprintf("COMIC_INFO$%s", comicId)
+ expire := time.Hour * 24 * 7
+ cache := network_cache.LoadCache(key, expire)
+ if cache != "" {
+ var co source.ComicInfo
+ err = json.Unmarshal([]byte(cache), &co)
+ if err != nil {
+ panic(err)
+ return "", err
+ }
+ comic = &co
+ } else {
+ // get
+ comic, err = client.ComicInfo(comicId)
+ if err != nil {
+ return "", err
+ }
+ var buff []byte
+ buff, err = json.Marshal(comic)
+ if err != nil {
+ return "", err
+ }
+ cache = string(buff)
+ network_cache.SaveCache(key, cache)
+ }
+ // 标记历史记录
+ view := comic_center.ComicView{}
+ view.ID = comicId
+ view.CreatedAt = comic.CreatedAt
+ view.UpdatedAt = comic.UpdatedAt
+ view.Title = comic.Title
+ view.Author = comic.Author
+ view.PagesCount = int32(comic.PagesCount)
+ view.EpsCount = int32(comic.EpsCount)
+ view.Finished = comic.Finished
+ c, _ := json.Marshal(comic.Categories)
+ view.Categories = string(c)
+ view.ThumbOriginalName = comic.Thumb.OriginalName
+ view.ThumbFileServer = comic.Thumb.FileServer
+ view.ThumbPath = comic.Thumb.Path
+ view.LikesCount = int32(comic.LikesCount)
+ view.Description = comic.Description
+ view.ChineseTeam = comic.ChineseTeam
+ t, _ := json.Marshal(comic.Tags)
+ view.Tags = string(t)
+ view.AllowDownload = comic.AllowDownload
+ view.ViewsCount = int32(comic.ViewsCount)
+ view.IsFavourite = comic.IsFavourite
+ view.IsLiked = comic.IsLiked
+ view.CommentsCount = int32(comic.CommentsCount)
+ err = comic_center.ViewComicUpdateInfo(&view)
+ if err != nil {
+ return "", err
+ }
+ // return
+ return cache, nil
+}
+
+func ComicInfoCleanCache(comicId string) {
+ key := fmt.Sprintf("COMIC_INFO$%s", comicId)
+ network_cache.RemoveCache(key)
+}
+
+func epPage(params string) (string, error) {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comicId := paramsStruct.ComicId
+ page := paramsStruct.Page
+ //
+ return cacheable(
+ fmt.Sprintf("COMIC_EP_PAGE$%s$%d", comicId, page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.ComicEpPage(comicId, page)
+ },
+ )
+}
+
+func comicPicturePageWithQuality(params string) (string, error) {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ EpOrder int `json:"epOrder"`
+ Page int `json:"page"`
+ Quality string `json:"quality"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comicId := paramsStruct.ComicId
+ epOrder := paramsStruct.EpOrder
+ page := paramsStruct.Page
+ quality := paramsStruct.Quality
+ //
+ return cacheable(
+ fmt.Sprintf("COMIC_EP_PAGE$%s$%ds$%ds$%s", comicId, epOrder, page, quality),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.ComicPicturePageWithQuality(comicId, epOrder, page, quality)
+ },
+ )
+}
+
+func switchLike(comicId string) (string, error) {
+ point, err := client.SwitchLike(comicId)
+ if err != nil {
+ return "", err
+ }
+ // 更新viewLog里面的favour
+ comic_center.ViewComicUpdateLike(comicId, strings.HasPrefix(*point, "un"))
+ // 删除缓存
+ ComicInfoCleanCache(comicId)
+ return *point, nil
+}
+
+func switchFavourite(comicId string) (string, error) {
+ point, err := client.SwitchFavourite(comicId)
+ if err != nil {
+ return "", err
+ }
+ // 更新viewLog里面的favour
+ comic_center.ViewComicUpdateFavourite(comicId, strings.HasPrefix(*point, "un"))
+ // 删除缓存
+ ComicInfoCleanCache(comicId)
+ return *point, nil
+}
+
+func favouriteComics(params string) (string, error) {
+ var paramsStruct struct {
+ Sort string `json:"sort"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ sort := paramsStruct.Sort
+ page := paramsStruct.Page
+ //
+ point, err := client.FavouriteComics(sort, page)
+ if err != nil {
+ return "", err
+ }
+ str, err := json.Marshal(point)
+ if err != nil {
+ return "", err
+ }
+ return string(str), nil
+}
+
+func recommendation(comicId string) (string, error) {
+ return cacheable(
+ fmt.Sprintf("RECOMMENDATION$%s", comicId),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.ComicRecommendation(comicId)
+ },
+ )
+}
+
+func comments(params string) (string, error) {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comicId := paramsStruct.ComicId
+ page := paramsStruct.Page
+ return cacheable(
+ fmt.Sprintf("COMMENTS$%s$%d", comicId, page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.ComicCommentsPage(comicId, page)
+ },
+ )
+}
+
+func commentChildren(params string) (string, error) {
+ var paramsStruct struct {
+ CommentId string `json:"commentId"`
+ Page int `json:"page"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ commentId := paramsStruct.CommentId
+ page := paramsStruct.Page
+ return cacheable(
+ fmt.Sprintf("COMMENT_CHILDREN$%s$%d", commentId, page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.CommentChildren(commentId, page)
+ },
+ )
+}
+
+func postComment(params string) (string, error) {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ Content string `json:"content"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ err := client.PostComment(paramsStruct.ComicId, paramsStruct.Content)
+ if err != nil {
+ return "", err
+ }
+ network_cache.RemoveCaches("MY_COMMENTS$%")
+ network_cache.RemoveCaches(fmt.Sprintf("COMMENTS$%s$%%", paramsStruct.ComicId))
+ return "", nil
+}
+
+func postChildComment(params string) (string, error) {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ CommentId string `json:"commentId"`
+ Content string `json:"content"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ err := client.PostChildComment(paramsStruct.CommentId, paramsStruct.Content)
+ if err != nil {
+ return "", err
+ }
+ network_cache.RemoveCaches(fmt.Sprintf("COMMENT_CHILDREN$%s$%%", paramsStruct.CommentId))
+ network_cache.RemoveCaches("MY_COMMENTS$%")
+ network_cache.RemoveCaches(fmt.Sprintf("COMMENTS$%s$%%", paramsStruct.ComicId))
+ return "", nil
+}
+
+func myComments(pageStr string) (string, error) {
+ page, err := strconv.Atoi(pageStr)
+ if err != nil {
+ return "", err
+ }
+ return cacheable(
+ fmt.Sprintf("MY_COMMENTS$%d", page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.MyComments(page)
+ },
+ )
+}
+
+func games(pageStr string) (string, error) {
+ page, err := strconv.Atoi(pageStr)
+ if err != nil {
+ return "", err
+ }
+ return cacheable(
+ fmt.Sprintf("GAMES$%d", page),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.GamePage(page)
+ },
+ )
+}
+
+func game(gameId string) (string, error) {
+ return cacheable(
+ fmt.Sprintf("GAME$%s", gameId),
+ time.Hour*2,
+ func() (interface{}, error) {
+ return client.GameInfo(gameId)
+ },
+ )
+}
diff --git a/go/pikapi/controller/common.go b/go/pikapi/controller/common.go
new file mode 100644
index 0000000..932c5a6
--- /dev/null
+++ b/go/pikapi/controller/common.go
@@ -0,0 +1,45 @@
+package controller
+
+import (
+ "encoding/json"
+ "pgo/pikapi/database/comic_center"
+)
+
+var EventNotify func(message string)
+
+func onEvent(function string, content string) {
+ event := EventNotify
+ if event != nil {
+ message := map[string]string{
+ "function": function,
+ "content": content,
+ }
+ buff, err := json.Marshal(message)
+ if err == nil {
+ event(string(buff))
+ } else {
+ print("SEND ERR?")
+ }
+ }
+}
+
+func downloadComicEventSend(comicDownload *comic_center.ComicDownload) {
+ buff, err := json.Marshal(comicDownload)
+ if err == nil {
+ onEvent("DOWNLOAD", string(buff))
+ } else {
+ print("SEND ERR?")
+ }
+}
+
+func notifyExport(str string) {
+ onEvent("EXPORT", str)
+}
+
+func serialize(point interface{}, err error) (string, error) {
+ if err != nil {
+ return "", err
+ }
+ buff, err := json.Marshal(point)
+ return string(buff), nil
+}
diff --git a/go/pikapi/controller/download.go b/go/pikapi/controller/download.go
new file mode 100644
index 0000000..feb6e54
--- /dev/null
+++ b/go/pikapi/controller/download.go
@@ -0,0 +1,307 @@
+package controller
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "pgo/pikapi/const_value"
+ "pgo/pikapi/database/comic_center"
+ utils2 "pgo/pikapi/utils"
+ "time"
+)
+
+var downloadRunning = false
+var downloadRestart = false
+
+var downloadingComic *comic_center.ComicDownload
+var downloadingEp *comic_center.ComicDownloadEp
+var downloadingPicture *comic_center.ComicDownloadPicture
+
+func downloadBackground() {
+ println("后台线程启动")
+ go downloadBegin()
+}
+
+func downloadBegin() {
+ time.Sleep(time.Second * 3)
+ go downloadLoadComic()
+}
+
+func downloadHasStop() bool {
+ if !downloadRunning {
+ go downloadBegin()
+ return true
+ }
+ if downloadRestart {
+ downloadRestart = false
+ go downloadBegin()
+ return true
+ }
+ return false
+}
+
+func downloadDelete() bool {
+ c, e := comic_center.DeletingComic()
+ if e != nil {
+ panic(e)
+ }
+ if c != nil {
+ os.RemoveAll(downloadPath(c.ID))
+ e = comic_center.TrueDelete(c.ID)
+ if e != nil {
+ panic(e)
+ }
+ return true
+ }
+ return false
+}
+
+func downloadLoadComic() {
+ for downloadDelete() {
+ }
+ if downloadHasStop() {
+ return
+ }
+ var err error
+ downloadingComic, err = comic_center.LoadFirstNeedDownload()
+ // 查库有错误就停止
+ if err != nil {
+ panic(err)
+ }
+ go downloadInitComic()
+}
+
+func downloadInitComic() {
+ if downloadHasStop() {
+ return
+ }
+ if downloadingComic == nil {
+ println("没有找到要下载的漫画")
+ go downloadBegin()
+ return
+ }
+ println("正在下载漫画 " + downloadingComic.Title)
+ downloadComicEventSend(downloadingComic)
+ eps, err := comic_center.ListDownloadEpByComicId(downloadingComic.ID)
+ if err != nil {
+ panic(err)
+ }
+ for _, ep := range eps {
+ if !ep.FetchedPictures {
+ println("正在获取章节的图片 " + downloadingComic.Title + " " + ep.Title)
+ for i := 0; i < 5; i++ {
+ if client.Token == "" {
+ continue
+ }
+ err := downloadFetchPictures(&ep)
+ if err != nil {
+ println(err.Error())
+ continue
+ }
+ ep.FetchedPictures = true
+ break
+ }
+ if !ep.FetchedPictures {
+ println("章节的图片获取失败 " + downloadingComic.Title + " " + ep.Title)
+ err = comic_center.EpFailed(ep.ID)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ println("章节的图片获取成功 " + downloadingComic.Title + " " + ep.Title)
+ downloadingComic.SelectedPictureCount = downloadingComic.SelectedPictureCount + ep.SelectedPictureCount
+ downloadComicEventSend(downloadingComic)
+ }
+ }
+ }
+ go downloadLoadEp()
+}
+
+func downloadFetchPictures(downloadEp *comic_center.ComicDownloadEp) error {
+ var list []comic_center.ComicDownloadPicture
+ page := 1
+ for true {
+ rsp, err := client.ComicPicturePage(downloadingComic.ID, int(downloadEp.EpOrder), page)
+ if err != nil {
+ return err
+ }
+ for _, doc := range rsp.Docs {
+ list = append(list, comic_center.ComicDownloadPicture{
+ ID: doc.Id,
+ ComicId: downloadEp.ComicId,
+ EpId: downloadEp.ID,
+ EpOrder: downloadEp.EpOrder,
+ OriginalName: doc.Media.OriginalName,
+ FileServer: doc.Media.FileServer,
+ Path: doc.Media.Path,
+ })
+ }
+ if rsp.Page.Page < rsp.Page.Pages {
+ page++
+ continue
+ }
+ break
+ }
+ err := comic_center.FetchPictures(downloadEp.ComicId, downloadEp.ID, &list)
+ if err != nil {
+ panic(err)
+ }
+ downloadEp.SelectedPictureCount = int32(len(list))
+ return err
+}
+
+func downloadLoadEp() {
+ if downloadHasStop() {
+ return
+ }
+ var err error
+ downloadingEp, err = comic_center.LoadFirstNeedDownloadEp(downloadingComic.ID)
+ if err != nil {
+ panic(err)
+ }
+ go downloadInitEp()
+}
+
+func downloadInitEp() {
+ if downloadingEp == nil {
+ // 所有Ep都下完了, 汇总Download下载情况
+ go downloadSummaryDownload()
+ return
+ }
+ println("正在下载章节 " + downloadingEp.Title)
+ go downloadLoadPicture()
+}
+
+func downloadSummaryDownload() {
+ if downloadHasStop() {
+ return
+ }
+ list, err := comic_center.ListDownloadEpByComicId(downloadingComic.ID)
+ if err != nil {
+ panic(err)
+ }
+ over := true
+ for _, downloadEp := range list {
+ over = over && downloadEp.DownloadFinished
+ }
+ if over {
+ err = comic_center.DownloadSuccess(downloadingComic.ID)
+ if err != nil {
+ panic(err)
+ }
+ downloadingComic.DownloadFinished = true
+ downloadingComic.DownloadFinishedTime = time.Now()
+ } else {
+ err = comic_center.DownloadFailed(downloadingComic.ID)
+ if err != nil {
+ panic(err)
+ }
+ downloadingComic.DownloadFailed = true
+ }
+ downloadComicEventSend(downloadingComic)
+ go downloadLoadComic()
+}
+
+func downloadLoadPicture() {
+ if downloadHasStop() {
+ return
+ }
+ var err error
+ downloadingPicture, err = comic_center.LoadFirstNeedDownloadPicture(downloadingEp.ID)
+ if err != nil {
+ panic(err)
+ }
+ go downloadInitPicture()
+}
+
+func downloadInitPicture() {
+ if downloadHasStop() {
+ return
+ }
+ if downloadingPicture == nil {
+ // 所有图片都下完了, 汇总EP下载情况
+ go downloadSummaryEp()
+ return
+ }
+ println("正在下载图片 " + fmt.Sprintf("%d", downloadingPicture.RankInEp))
+ for i := 0; i < 5; i++ {
+ err := downloadThePicture(downloadingPicture)
+ if err != nil {
+ continue
+ }
+ downloadingPicture.DownloadFinished = true
+ downloadingEp.DownloadPictureCount = downloadingEp.DownloadPictureCount + 1
+ downloadingComic.DownloadPictureCount = downloadingComic.DownloadPictureCount + 1
+ downloadComicEventSend(downloadingComic)
+ break
+ }
+ if !downloadingPicture.DownloadFinished {
+ err := comic_center.PictureFailed(downloadingPicture.ID)
+ if err != nil {
+ panic(err)
+ }
+ }
+ go downloadLoadPicture()
+}
+
+func downloadThePicture(picturePoint *comic_center.ComicDownloadPicture) error {
+ lock := utils2.HashLock(fmt.Sprintf("%s$%s", picturePoint.FileServer, picturePoint.Path))
+ lock.Lock()
+ defer lock.Unlock()
+ picturePath := fmt.Sprintf("%s/%d/%d", picturePoint.ComicId, picturePoint.EpOrder, picturePoint.RankInEp)
+ realPath := downloadPath(picturePath)
+ // 从缓存
+ buff, img, format, err := decodeFromCache(picturePoint.FileServer, picturePoint.Path)
+ if err != nil {
+ // 从网络
+ buff, img, format, err = decodeFromUrl(picturePoint.FileServer, picturePoint.Path)
+ }
+ if err != nil {
+ return err
+ }
+ dir := filepath.Dir(realPath)
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ os.Mkdir(dir, const_value.CreateDirMode)
+ }
+ err = ioutil.WriteFile(downloadPath(picturePath), buff, const_value.CreateFileMode)
+ if err != nil {
+ return err
+ }
+ return comic_center.PictureSuccess(
+ picturePoint.ComicId,
+ picturePoint.EpId,
+ picturePoint.ID,
+ int64(len(buff)),
+ format,
+ int32(img.Bounds().Dx()),
+ int32(img.Bounds().Dy()),
+ picturePath,
+ )
+}
+
+func downloadSummaryEp() {
+ if downloadHasStop() {
+ return
+ }
+ list, err := comic_center.ListDownloadPictureByEpId(downloadingEp.ID)
+ if err != nil {
+ panic(err)
+ }
+ over := true
+ for _, downloadPicture := range list {
+ over = over && downloadPicture.DownloadFinished
+ }
+ if over {
+ err = comic_center.EpSuccess(downloadingEp.ComicId, downloadingEp.ID)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ err = comic_center.EpFailed(downloadingEp.ID)
+ if err != nil {
+ panic(err)
+ }
+ }
+ go downloadLoadEp()
+}
diff --git a/go/pikapi/controller/export.go b/go/pikapi/controller/export.go
new file mode 100644
index 0000000..eff5a46
--- /dev/null
+++ b/go/pikapi/controller/export.go
@@ -0,0 +1,475 @@
+package controller
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/gzip"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ "path"
+ "pgo/pikapi/const_value"
+ "pgo/pikapi/database/comic_center"
+ "time"
+)
+
+var exportingListener net.Listener
+var exportingConn net.Conn
+
+func exportComicUsingSocket(comicId string) (int, error) {
+ var err error
+ exportingListener, err = net.Listen("tcp", ":0")
+ if err != nil {
+ return 0, err
+ }
+ go handleExportingConn(comicId)
+ return exportingListener.Addr().(*net.TCPAddr).Port, nil
+}
+
+func handleExportingConn(comicId string) {
+ defer exportingListener.Close()
+ var err error
+ exportingConn, err = exportingListener.Accept()
+ if err != nil {
+ notifyExport(fmt.Sprintf("导出失败"))
+ println(err.Error())
+ return
+ }
+ defer exportingConn.Close()
+ gw := gzip.NewWriter(exportingConn)
+ defer gw.Close()
+ tw := tar.NewWriter(gw)
+ defer tw.Close()
+ err = exportComicDownloadFetch(comicId, func(path string, size int64) (io.Writer, error) {
+ header := tar.Header{}
+ header.Name = path
+ header.Size = size
+ return tw, tw.WriteHeader(&header)
+ })
+ if err != nil {
+ notifyExport(fmt.Sprintf("导出失败"))
+ } else {
+ notifyExport(fmt.Sprintf("导出成功"))
+ }
+}
+
+func exportComicUsingSocketExit() error {
+ if exportingConn != nil {
+ exportingConn.Close()
+ }
+ if exportingListener != nil {
+ exportingListener.Close()
+ }
+ return nil
+}
+
+func exportComicDownload(params string) error {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ Dir string `json:"dir"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comicId := paramsStruct.ComicId
+ dir := paramsStruct.Dir
+ println(fmt.Sprintf("导出 %s 到 %s", comicId, dir))
+ comic, err := comic_center.FindComicDownloadById(comicId)
+ if err != nil {
+ return err
+ }
+ if comic == nil {
+ return errors.New("not found")
+ }
+ if !comic.DownloadFinished {
+ return errors.New("not download finish")
+ }
+ filePath := path.Join(dir, fmt.Sprintf("%s-%s.zip", comic.Title, time.Now().Format("2006_01_02_15_04_05.999")))
+ println(fmt.Sprintf("ZIP : %s", filePath))
+ fileStream, err := os.Create(filePath)
+ if err != nil {
+ return err
+ }
+ defer fileStream.Close()
+ zipWriter := zip.NewWriter(fileStream)
+ defer zipWriter.Close()
+ return exportComicDownloadFetch(comicId, func(path string, size int64) (io.Writer, error) {
+ header := tar.Header{}
+ header.Name = path
+ header.Size = size
+ return zipWriter.Create(path)
+ })
+}
+
+func exportComicDownloadFetch(comicId string, onWriteFile func(path string, size int64) (io.Writer, error)) error {
+ comic, err := comic_center.FindComicDownloadById(comicId)
+ if err != nil {
+ return err
+ }
+ if comic == nil {
+ return errors.New("not found")
+ }
+ if !comic.DownloadFinished {
+ return errors.New("not download finish")
+ }
+ epList, err := comic_center.ListDownloadEpByComicId(comicId)
+ if err != nil {
+ return err
+ }
+ jsonComic := JsonComicDownload{}
+ jsonComic.ComicDownload = *comic
+ jsonComic.EpList = make([]JsonComicDownloadEp, 0)
+ for _, ep := range epList {
+ jsonEp := JsonComicDownloadEp{}
+ jsonEp.ComicDownloadEp = ep
+ jsonEp.PictureList = make([]JsonComicDownloadPicture, 0)
+ pictures, err := comic_center.ListDownloadPictureByEpId(ep.ID)
+ if err != nil {
+ return err
+ }
+ for _, picture := range pictures {
+ jsonPicture := JsonComicDownloadPicture{}
+ jsonPicture.ComicDownloadPicture = picture
+ jsonPicture.SrcPath = fmt.Sprintf("pictures/%04d_%04d", ep.EpOrder, picture.RankInEp)
+ notifyExport(fmt.Sprintf("正在导出 EP:%d PIC:%d", ep.EpOrder, picture.RankInEp))
+ entryWriter, err := onWriteFile(jsonPicture.SrcPath, jsonPicture.FileSize)
+ if err != nil {
+ return err
+ }
+ source, err := os.Open(downloadPath(picture.LocalPath))
+ if err != nil {
+ return err
+ }
+ _, err = func() (int64, error) {
+ defer source.Close()
+ return io.Copy(entryWriter, source)
+ }()
+ if err != nil {
+ return err
+ }
+ jsonEp.PictureList = append(jsonEp.PictureList, jsonPicture)
+ }
+ jsonComic.EpList = append(jsonComic.EpList, jsonEp)
+ }
+ if comic.ThumbLocalPath != "" {
+ logoBuff, err := ioutil.ReadFile(downloadPath(comic.ThumbLocalPath))
+ if err == nil {
+ entryWriter, err := onWriteFile("logo", int64(len(logoBuff)))
+ if err != nil {
+ return err
+ }
+ _, err = entryWriter.Write(logoBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // JS
+ {
+ buff, err := json.Marshal(&jsonComic)
+ if err != nil {
+ return err
+ }
+ logoBuff := append([]byte("data = "), buff...)
+ if err == nil {
+ entryWriter, err := onWriteFile("data.js", int64(len(logoBuff)))
+ if err != nil {
+ return err
+ }
+ _, err = entryWriter.Write(logoBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // HTML
+ {
+ var htmlBuff = []byte(indexHtml)
+ if err == nil {
+ entryWriter, err := onWriteFile("index.html", int64(len(htmlBuff)))
+ if err != nil {
+ return err
+ }
+ _, err = entryWriter.Write(htmlBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ println("OK")
+ //
+ return nil
+}
+
+const indexHtml = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+func exportComicDownloadToJPG(params string) error {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ Dir string `json:"dir"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comicId := paramsStruct.ComicId
+ dir := paramsStruct.Dir
+ println(fmt.Sprintf("导出 %s 到 %s", comicId, dir))
+ comic, err := comic_center.FindComicDownloadById(comicId)
+ if err != nil {
+ return err
+ }
+ if comic == nil {
+ return errors.New("not found")
+ }
+ if !comic.DownloadFinished {
+ return errors.New("not download finish")
+ }
+ dirPath := path.Join(dir, fmt.Sprintf("%s-%s", comic.Title, time.Now().Format("2006_01_02_15_04_05.999")))
+ println(fmt.Sprintf("DIR : %s", dirPath))
+ err = os.Mkdir(dirPath, const_value.CreateDirMode)
+ if err != nil {
+ return err
+ }
+ err = os.Mkdir(path.Join(dirPath, "pictures"), const_value.CreateDirMode)
+ if err != nil {
+ return err
+ }
+
+ epList, err := comic_center.ListDownloadEpByComicId(comicId)
+ if err != nil {
+ return err
+ }
+ jsonComic := JsonComicDownload{}
+ jsonComic.ComicDownload = *comic
+ jsonComic.EpList = make([]JsonComicDownloadEp, 0)
+ for _, ep := range epList {
+ jsonEp := JsonComicDownloadEp{}
+ jsonEp.ComicDownloadEp = ep
+ jsonEp.PictureList = make([]JsonComicDownloadPicture, 0)
+ pictures, err := comic_center.ListDownloadPictureByEpId(ep.ID)
+ if err != nil {
+ return err
+ }
+ for _, picture := range pictures {
+ jsonPicture := JsonComicDownloadPicture{}
+ jsonPicture.ComicDownloadPicture = picture
+ jsonPicture.SrcPath = fmt.Sprintf("pictures/%04d_%04d.%s", ep.EpOrder, picture.RankInEp, picture.Format)
+ notifyExport(fmt.Sprintf("正在导出 EP:%d PIC:%d", ep.EpOrder, picture.RankInEp))
+ entryWriter, err := os.Create(path.Join(dirPath, jsonPicture.SrcPath))
+ if err != nil {
+ return err
+ }
+ err = func() error {
+ defer entryWriter.Close()
+ source, err := os.Open(downloadPath(picture.LocalPath))
+ if err != nil {
+ return err
+ }
+ _, err = func() (int64, error) {
+ defer source.Close()
+ return io.Copy(entryWriter, source)
+ }()
+ return err
+ }()
+ jsonEp.PictureList = append(jsonEp.PictureList, jsonPicture)
+ }
+ jsonComic.EpList = append(jsonComic.EpList, jsonEp)
+ }
+ if comic.ThumbLocalPath != "" {
+ logoBuff, err := ioutil.ReadFile(downloadPath(comic.ThumbLocalPath))
+ if err == nil {
+ entryWriter, err := os.Create(path.Join(dirPath, "logo"))
+ if err != nil {
+ return err
+ }
+ defer entryWriter.Close()
+ if err != nil {
+ return err
+ }
+ _, err = entryWriter.Write(logoBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // JS
+ {
+ buff, err := json.Marshal(&jsonComic)
+ if err != nil {
+ return err
+ }
+ logoBuff := append([]byte("data = "), buff...)
+ if err == nil {
+
+ entryWriter, err := os.Create(path.Join(dirPath, "data.js"))
+ if err != nil {
+ return err
+ }
+ defer entryWriter.Close()
+ _, err = entryWriter.Write(logoBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // HTML
+ {
+ var htmlBuff = []byte(indexHtml)
+ if err == nil {
+ entryWriter, err := os.Create(path.Join(dirPath, "index.html"))
+ if err != nil {
+ return err
+ }
+ defer entryWriter.Close()
+ _, err = entryWriter.Write(htmlBuff)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ println("OK")
+ return nil
+}
diff --git a/go/pikapi/controller/game.go b/go/pikapi/controller/game.go
new file mode 100644
index 0000000..6243298
--- /dev/null
+++ b/go/pikapi/controller/game.go
@@ -0,0 +1,40 @@
+package controller
+
+import (
+ "errors"
+ "fmt"
+ "github.com/PuerkitoBio/goquery"
+ "net/http"
+ "regexp"
+ "time"
+)
+
+var downloadGameUrlPattern, _ = regexp.Compile("^https://game\\.eroge\\.xyz/hhh\\.php\\?id=\\d+$")
+
+func downloadGame(url string) (string, error) {
+ if downloadGameUrlPattern.MatchString(url) {
+ return cacheable(fmt.Sprintf("GAME_PAGE$%s", url), time.Hour*1000, func() (interface{}, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36")
+ rsp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer rsp.Body.Close()
+ doc, err := goquery.NewDocumentFromReader(rsp.Body)
+ if err != nil {
+ return nil, err
+ }
+ find := doc.Find("a.layui-btn")
+ list := make([]string, find.Size())
+ find.Each(func(i int, selection *goquery.Selection) {
+ list[i] = selection.AttrOr("href", "")
+ })
+ return list, nil
+ })
+ }
+ return "", errors.New("not support url")
+}
diff --git a/go/pikapi/controller/image.go b/go/pikapi/controller/image.go
new file mode 100644
index 0000000..f0e37b6
--- /dev/null
+++ b/go/pikapi/controller/image.go
@@ -0,0 +1,96 @@
+package controller
+
+import (
+ "bytes"
+ "errors"
+ _ "golang.org/x/image/webp"
+ "image"
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+ "io/ioutil"
+ "net/http"
+ "pgo/pikapi/database/comic_center"
+ "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]
+}
+
+func decodeInfoFromBuff(buff []byte) (image.Image, string, error) {
+ buffer := bytes.NewBuffer(buff)
+ return image.Decode(buffer)
+}
+
+func decodeFromFile(path string) ([]byte, image.Image, string, error) {
+ b, e := ioutil.ReadFile(path)
+ if e != nil {
+ return nil, nil, "", e
+ }
+ i, f, e := decodeInfoFromBuff(b)
+ if e != nil {
+ return nil, nil, "", e
+ }
+ return b, i, f, e
+}
+
+// 下载图片并decode
+func decodeFromUrl(fileServer string, path string) ([]byte, image.Image, string, error) {
+ m := takeMutex()
+ m.Lock()
+ defer m.Unlock()
+ request, err := http.NewRequest("GET", fileServer+"/static/"+path, nil)
+ if err != nil {
+ return nil, nil, "", err
+ }
+ response, err := client.Do(request)
+ if err != nil {
+ return nil, nil, "", err
+ }
+ defer response.Body.Close()
+ if response.StatusCode != 200 {
+ return nil, nil, "", errors.New("code is not 200")
+ }
+ buff, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ return nil, nil, "", err
+ }
+ img, format, err := decodeInfoFromBuff(buff)
+ if err != nil {
+ return nil, nil, "", err
+ }
+ return buff, img, format, err
+}
+
+// decodeFromCache 仅下载使用
+func decodeFromCache(fileServer string, path string) ([]byte, image.Image, string, error) {
+ cache := comic_center.FindRemoteImage(fileServer, path)
+ if cache != nil {
+ buff, err := ioutil.ReadFile(remotePath(cache.LocalPath))
+ if err != nil {
+ return nil, nil, "", err
+ }
+ img, format, err := decodeInfoFromBuff(buff)
+ if err != nil {
+ return nil, nil, "", err
+ }
+ return buff, img, format, err
+ }
+ return nil, nil, "", errors.New("not found")
+}
diff --git a/go/pikapi/controller/import.go b/go/pikapi/controller/import.go
new file mode 100644
index 0000000..bbc2f2a
--- /dev/null
+++ b/go/pikapi/controller/import.go
@@ -0,0 +1,197 @@
+package controller
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/gzip"
+ "encoding/json"
+ "gorm.io/gorm"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ path2 "path"
+ "pgo/pikapi/const_value"
+ "pgo/pikapi/database/comic_center"
+ "pgo/pikapi/utils"
+ "strconv"
+ "strings"
+)
+
+func importComicDownloadUsingSocket(addr string) error {
+ //
+ conn, err := net.Dial("tcp", addr)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ gr, err := gzip.NewReader(conn)
+ if err != nil {
+ return err
+ }
+ tr := tar.NewReader(gr)
+ //
+ zipPath := path2.Join(tmpDir, "tmp.zip")
+ closed := false
+ zipFile, err := os.Create(zipPath)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if !closed {
+ zipFile.Close()
+ }
+ os.Remove(zipPath)
+ }()
+ zipWriter := zip.NewWriter(zipFile)
+ defer func() {
+ if !closed {
+ zipWriter.Close()
+ }
+ }()
+ //
+ for {
+ header, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ if header.Typeflag != tar.TypeReg {
+ continue
+ }
+ writer, err := zipWriter.Create(header.Name)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(writer, tr)
+ if err != nil {
+ return err
+ }
+ }
+ err = zipWriter.Close()
+ zipFile.Close()
+ closed = true
+ return importComicDownload(zipPath)
+}
+
+func importComicDownload(zipPath string) error {
+ zip, err := zip.OpenReader(zipPath)
+ if err != nil {
+ return err
+ }
+ defer zip.Close()
+ dataJs, err := zip.Open("data.js")
+ if err != nil {
+ return err
+ }
+ defer dataJs.Close()
+ dataBuff, err := ioutil.ReadAll(dataJs)
+ if err != nil {
+ return err
+ }
+ data := strings.TrimLeft(string(dataBuff), "data = ")
+ var jsonComicDownload JsonComicDownload
+ err = json.Unmarshal([]byte(data), &jsonComicDownload)
+ if err != nil {
+ return err
+ }
+ return comic_center.Transaction(func(tx *gorm.DB) error {
+ // 删除
+ err := tx.Unscoped().Delete(&comic_center.ComicDownload{}, "id = ?", jsonComicDownload.ID).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Unscoped().Delete(&comic_center.ComicDownloadEp{}, "comic_id = ?", jsonComicDownload.ID).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Unscoped().Delete(&comic_center.ComicDownloadPicture{}, "comic_id = ?", jsonComicDownload.ID).Error
+ if err != nil {
+ return err
+ }
+ // 插入
+ err = tx.Save(&jsonComicDownload.ComicDownload).Error
+ if err != nil {
+ return err
+ }
+ for _, ep := range jsonComicDownload.EpList {
+ err = tx.Save(&ep.ComicDownloadEp).Error
+ if err != nil {
+ return err
+ }
+ for _, picture := range ep.PictureList {
+ notifyExport("事务 : " + picture.LocalPath)
+ err = tx.Save(&picture.ComicDownloadPicture).Error
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // VIEW日志
+ view := comic_center.ComicView{}
+ view.ID = jsonComicDownload.ID
+ view.CreatedAt = jsonComicDownload.CreatedAt
+ view.UpdatedAt = jsonComicDownload.UpdatedAt
+ view.Title = jsonComicDownload.Title
+ view.Author = jsonComicDownload.Author
+ view.PagesCount = jsonComicDownload.PagesCount
+ view.EpsCount = jsonComicDownload.EpsCount
+ view.Finished = jsonComicDownload.Finished
+ c, _ := json.Marshal(jsonComicDownload.Categories)
+ view.Categories = string(c)
+ view.ThumbOriginalName = jsonComicDownload.ThumbOriginalName
+ view.ThumbFileServer = jsonComicDownload.ThumbFileServer
+ view.ThumbPath = jsonComicDownload.ThumbPath
+ view.LikesCount = 0
+ view.Description = jsonComicDownload.Description
+ view.ChineseTeam = jsonComicDownload.ChineseTeam
+ t, _ := json.Marshal(jsonComicDownload.Tags)
+ view.Tags = string(t)
+ view.AllowDownload = true
+ view.ViewsCount = 0
+ view.IsFavourite = false
+ view.IsLiked = false
+ view.CommentsCount = 0
+ err = comic_center.NoLockActionViewComicUpdateInfoDB(&view, tx)
+ if err != nil {
+ return err
+ }
+ // 覆盖文件
+ comicDirPath := downloadPath(jsonComicDownload.ID)
+ utils.Mkdir(comicDirPath)
+ logoReader, err := zip.Open("logo")
+ if err == nil {
+ defer logoReader.Close()
+ logoBuff, err := ioutil.ReadAll(logoReader)
+ if err != nil {
+ return err
+ }
+ ioutil.WriteFile(path2.Join(comicDirPath, "logo"), logoBuff, const_value.CreateFileMode)
+ }
+ for _, ep := range jsonComicDownload.EpList {
+ utils.Mkdir(path2.Join(comicDirPath, strconv.Itoa(int(ep.EpOrder))))
+ for _, picture := range ep.PictureList {
+ notifyExport("写入 : " + picture.LocalPath)
+ zipEntry, err := zip.Open(picture.SrcPath)
+ if err != nil {
+ return err
+ }
+ err = func() error {
+ defer zipEntry.Close()
+ entryBuff, err := ioutil.ReadAll(zipEntry)
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(downloadPath(picture.LocalPath), entryBuff, const_value.CreateFileMode)
+ }()
+ if err != nil {
+ return err
+ }
+ }
+ }
+ // 结束
+ return nil
+ })
+}
diff --git a/go/pikapi/controller/modles.go b/go/pikapi/controller/modles.go
new file mode 100644
index 0000000..e31273c
--- /dev/null
+++ b/go/pikapi/controller/modles.go
@@ -0,0 +1,31 @@
+package controller
+
+import "pgo/pikapi/database/comic_center"
+
+type DisplayImageData struct {
+ FileSize int64 `json:"fileSize"`
+ Format string `json:"format"`
+ Width int32 `json:"width"`
+ Height int32 `json:"height"`
+ FinalPath string `json:"finalPath"`
+}
+
+type ComicDownloadPictureWithFinalPath struct {
+ comic_center.ComicDownloadPicture
+ FinalPath string `json:"finalPath"`
+}
+
+type JsonComicDownload struct {
+ comic_center.ComicDownload
+ EpList []JsonComicDownloadEp `json:"epList"`
+}
+
+type JsonComicDownloadEp struct {
+ comic_center.ComicDownloadEp
+ PictureList []JsonComicDownloadPicture `json:"pictureList"`
+}
+
+type JsonComicDownloadPicture struct {
+ comic_center.ComicDownloadPicture
+ SrcPath string `json:"srcPath"`
+}
diff --git a/go/pikapi/controller/network.go b/go/pikapi/controller/network.go
new file mode 100644
index 0000000..9be445e
--- /dev/null
+++ b/go/pikapi/controller/network.go
@@ -0,0 +1,23 @@
+package controller
+
+import (
+ "net"
+ "strings"
+)
+
+func clientIpSet() (string, error) {
+ address, err := net.InterfaceAddrs()
+ if err != nil {
+ return "", err
+ }
+ ipSet := make([]string, 0)
+ for _, address := range address {
+ // 检查ip地址判断是否回环地址
+ if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
+ if ipNet.IP.To4() != nil {
+ ipSet = append(ipSet, ipNet.IP.To4().String())
+ }
+ }
+ }
+ return strings.Join(ipSet, ","), nil
+}
diff --git a/go/pikapi/controller/pikapi.go b/go/pikapi/controller/pikapi.go
new file mode 100644
index 0000000..e2c3894
--- /dev/null
+++ b/go/pikapi/controller/pikapi.go
@@ -0,0 +1,578 @@
+package controller
+
+import (
+ "crypto/md5"
+ "encoding/json"
+ "errors"
+ "fmt"
+ source "github.com/niuhuan/pica-go"
+ "image/jpeg"
+ "io/ioutil"
+ "os"
+ path2 "path"
+ "pgo/pikapi/const_value"
+ "pgo/pikapi/database/comic_center"
+ "pgo/pikapi/database/network_cache"
+ "pgo/pikapi/database/properties"
+ "pgo/pikapi/utils"
+ "strconv"
+ "time"
+)
+
+var (
+ remoteDir string
+ downloadDir string
+ tmpDir string
+)
+
+func InitPlugin(_remoteDir string, _downloadDir string, _tmpDir string) {
+ remoteDir = _remoteDir
+ downloadDir = _downloadDir
+ tmpDir = _tmpDir
+ comic_center.ResetAll()
+ go downloadBackground()
+ downloadRunning = true
+}
+
+func remotePath(path string) string {
+ return path2.Join(remoteDir, path)
+}
+
+func downloadPath(path string) string {
+ return path2.Join(downloadDir, path)
+}
+
+func saveProperty(params 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 setSwitchAddress(nSwitchAddress string) error {
+ err := properties.SaveSwitchAddress(nSwitchAddress)
+ if err != nil {
+ return err
+ }
+ switchAddress = nSwitchAddress
+ return nil
+}
+
+func getSwitchAddress() (string, error) {
+ return switchAddress, nil
+}
+
+func setProxy(value string) error {
+ err := properties.SaveProxy(value)
+ if err != nil {
+ return err
+ }
+ changeProxyUrl(value)
+ return nil
+}
+
+func getProxy() (string, error) {
+ return properties.LoadProxy()
+}
+
+func setUsername(value string) error {
+ return properties.SaveUsername(value)
+}
+
+func getUsername() (string, error) {
+ return properties.LoadUsername()
+}
+
+func setPassword(value string) error {
+ return properties.SavePassword(value)
+}
+
+func getPassword() (string, error) {
+ return properties.LoadPassword()
+}
+
+func preLogin() (string, error) {
+ token, _ := properties.LoadToken()
+ tokenTime, _ := properties.LoadTokenTime()
+ if token != "" && tokenTime > 0 {
+ if utils.Timestamp()-(1000*60*60*24) < tokenTime {
+ client.Token = token
+ return "true", nil
+ }
+ }
+ err := login()
+ if err == nil {
+ return "true", nil
+ }
+ return "false", nil
+}
+
+func login() error {
+ username, _ := properties.LoadUsername()
+ password, _ := properties.LoadPassword()
+ if password == "" || username == "" {
+ return errors.New(" 需要设定用户名和密码 ")
+ }
+ err := client.Login(username, password)
+ if err != nil {
+ return err
+ }
+ properties.SaveToken(client.Token)
+ properties.SaveTokenTime(utils.Timestamp())
+ return nil
+}
+
+func register(params string) error {
+ var dto source.RegisterDto
+ err := json.Unmarshal([]byte(params), &dto)
+ if err != nil {
+ return err
+ }
+ return client.Register(dto)
+}
+
+func clearToken() error {
+ properties.SaveTokenTime(0)
+ properties.SaveToken("")
+ return nil
+}
+
+func userProfile() (string, error) {
+ return serialize(client.UserProfile())
+}
+
+func punchIn() (string, error) {
+ return serialize(client.PunchIn())
+}
+
+func remoteImageData(params string) (string, error) {
+ var paramsStruct struct {
+ FileServer string `json:"fileServer"`
+ Path string `json:"path"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ fileServer := paramsStruct.FileServer
+ path := paramsStruct.Path
+ lock := utils.HashLock(fmt.Sprintf("%s$%s", fileServer, path))
+ lock.Lock()
+ defer lock.Unlock()
+ cache := comic_center.FindRemoteImage(fileServer, path)
+ if cache == nil {
+ buff, img, format, err := decodeFromUrl(fileServer, path)
+ if err != nil {
+ println(fmt.Sprintf("decode error : %s/static/%s %s", fileServer, path, err.Error()))
+ return "", err
+ }
+ local :=
+ fmt.Sprintf("%x",
+ md5.Sum([]byte(fmt.Sprintf("%s$%s", fileServer, path))),
+ )
+ real := remotePath(local)
+ err = ioutil.WriteFile(
+ real,
+ buff, os.FileMode(0600),
+ )
+ if err != nil {
+ return "", err
+ }
+ remote := comic_center.RemoteImage{
+ FileServer: fileServer,
+ Path: path,
+ FileSize: int64(len(buff)),
+ Format: format,
+ Width: int32(img.Bounds().Dx()),
+ Height: int32(img.Bounds().Dy()),
+ LocalPath: local,
+ }
+ err = comic_center.SaveRemoteImage(&remote)
+ if err != nil {
+ return "", err
+ }
+ cache = &remote
+ }
+ display := DisplayImageData{
+ FileSize: cache.FileSize,
+ Format: cache.Format,
+ Width: cache.Width,
+ Height: cache.Height,
+ FinalPath: remotePath(cache.LocalPath),
+ }
+ return serialize(&display, nil)
+}
+
+func downloadImagePath(path string) (string, error) {
+ return downloadPath(path), nil
+}
+
+func createDownload(params string) error {
+ var paramsStruct struct {
+ Comic comic_center.ComicDownload `json:"comic"`
+ EpList []comic_center.ComicDownloadEp `json:"epList"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comic := paramsStruct.Comic
+ epList := paramsStruct.EpList
+ if comic.Title == "" || len(epList) == 0 {
+ return errors.New("params error")
+ }
+ err := comic_center.CreateDownload(&comic, &epList)
+ if err != nil {
+ return err
+ }
+ // 创建文件夹
+ utils.Mkdir(downloadPath(comic.ID))
+ // 复制图标
+ downloadComicLogo(&comic)
+ return nil
+}
+
+func downloadComicLogo(comic *comic_center.ComicDownload) {
+ lock := utils.HashLock(fmt.Sprintf("%s$%s", comic.ThumbFileServer, comic.ThumbPath))
+ lock.Lock()
+ defer lock.Unlock()
+ buff, image, format, err := decodeFromCache(comic.ThumbFileServer, comic.ThumbPath)
+ if err != nil {
+ buff, image, format, err = decodeFromUrl(comic.ThumbFileServer, comic.ThumbPath)
+ }
+ if err == nil {
+ comicLogoPath := path2.Join(comic.ID, "logo")
+ ioutil.WriteFile(downloadPath(comicLogoPath), buff, const_value.CreateFileMode)
+ comic_center.UpdateDownloadLogo(
+ comic.ID,
+ int64(len(buff)),
+ format,
+ int32(image.Bounds().Dx()),
+ int32(image.Bounds().Dy()),
+ comicLogoPath,
+ )
+ comic.ThumbFileSize = int64(len(buff))
+ comic.ThumbFormat = format
+ comic.ThumbWidth = int32(image.Bounds().Dx())
+ comic.ThumbHeight = int32(image.Bounds().Dy())
+ comic.ThumbLocalPath = comicLogoPath
+ }
+ if err != nil {
+ println(err.Error())
+ }
+}
+
+func addDownload(params string) error {
+ var paramsStruct struct {
+ Comic comic_center.ComicDownload `json:"comic"`
+ EpList []comic_center.ComicDownloadEp `json:"epList"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ comic := paramsStruct.Comic
+ epList := paramsStruct.EpList
+ if comic.Title == "" || len(epList) == 0 {
+ return errors.New("params error")
+ }
+ return comic_center.AddDownload(&comic, &epList)
+}
+
+func deleteDownloadComic(comicId string) error {
+ err := comic_center.Deleting(comicId)
+ if err != nil {
+ return err
+ }
+ downloadRestart = true
+ return nil
+}
+
+func loadDownloadComic(comicId string) (string, error) {
+ download, err := comic_center.FindComicDownloadById(comicId)
+ if err != nil {
+ return "", err
+ }
+ if download == nil {
+ return "", nil
+ }
+ comic_center.ViewComic(comicId) // VIEW
+ return serialize(download, err)
+}
+
+func allDownloads() (string, error) {
+ return serialize(comic_center.AllDownloads())
+}
+
+func downloadEpList(comicId string) (string, error) {
+ return serialize(comic_center.ListDownloadEpByComicId(comicId))
+}
+
+func viewLogPage(params string) (string, error) {
+ var paramsStruct struct {
+ Offset int `json:"offset"`
+ Limit int `json:"limit"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ return serialize(comic_center.ViewLogPage(paramsStruct.Offset, paramsStruct.Limit))
+}
+
+func downloadPicturesByEpId(epId string) (string, error) {
+ return serialize(comic_center.ListDownloadPictureByEpId(epId))
+}
+
+func getDownloadRunning() bool {
+ return downloadRunning
+}
+
+func setDownloadRunning(status bool) {
+ downloadRunning = status
+}
+
+func clean() error {
+ var err error
+ notifyExport("清理网络缓存")
+ err = network_cache.RemoveAll()
+ if err != nil {
+ return err
+ }
+ notifyExport("清理图片缓存")
+ err = comic_center.RemoveAllRemoteImage()
+ if err != nil {
+ return err
+ }
+ notifyExport("清理图片文件")
+ os.RemoveAll(remoteDir)
+ utils.Mkdir(remoteDir)
+ notifyExport("清理结束")
+ return nil
+}
+
+func autoClean(expire int64) error {
+ now := time.Now()
+ earliest := now.Add(time.Second * time.Duration(0-expire))
+ err := network_cache.RemoveEarliest(earliest)
+ if err != nil {
+ return err
+ }
+ pageSize := 10
+ for true {
+ images, err := comic_center.EarliestRemoteImage(earliest, pageSize)
+ if err != nil {
+ return err
+ }
+ if len(images) == 0 {
+ return comic_center.VACUUM()
+ }
+ // delete data & remove pic
+ err = comic_center.DeleteRemoteImages(images)
+ if err != nil {
+ return err
+ }
+ for i := 0; i < len(images); i++ {
+ err = os.Remove(remotePath(images[i].LocalPath))
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func storeViewEp(params string) error {
+ var paramsStruct struct {
+ ComicId string `json:"comicId"`
+ EpOrder int `json:"epOrder"`
+ EpTitle string `json:"epTitle"`
+ PictureRank int `json:"pictureRank"`
+ }
+ json.Unmarshal([]byte(params), ¶msStruct)
+ return comic_center.ViewEpAndPicture(
+ paramsStruct.ComicId,
+ paramsStruct.EpOrder,
+ paramsStruct.EpTitle,
+ paramsStruct.PictureRank,
+ )
+}
+
+func loadView(comicId string) (string, error) {
+ view, err := comic_center.LoadViewLog(comicId)
+ if err != nil {
+ return "", nil
+ }
+ if view != nil {
+ b, err := json.Marshal(view)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+ }
+ return "", nil
+}
+
+func convertImageToJPEG100(params string) error {
+ var paramsStruct struct {
+ Path string `json:"path"`
+ Dir string `json:"dir"`
+ }
+ err := json.Unmarshal([]byte(params), ¶msStruct)
+ if err != nil {
+ return err
+ }
+ _, i, _, err := decodeFromFile(paramsStruct.Path)
+ if err != nil {
+ return err
+ }
+ to := path2.Join(paramsStruct.Dir, path2.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})
+}
+
+func FlatInvoke(method string, params string) (string, error) {
+ switch method {
+ case "saveProperty":
+ return "", saveProperty(params)
+ case "loadProperty":
+ return loadProperty(params)
+ case "setSwitchAddress":
+ return "", setSwitchAddress(params)
+ case "getSwitchAddress":
+ return getSwitchAddress()
+ case "setProxy":
+ return "", setProxy(params)
+ case "getProxy":
+ return getProxy()
+ case "setUsername":
+ return "", setUsername(params)
+ case "setPassword":
+ return "", setPassword(params)
+ case "getUsername":
+ return getUsername()
+ case "getPassword":
+ return getPassword()
+ case "preLogin":
+ return preLogin()
+ case "login":
+ return "", login()
+ case "register":
+ return "", register(params)
+ case "clearToken":
+ return "", clearToken()
+ case "userProfile":
+ return userProfile()
+ case "punchIn":
+ return punchIn()
+ case "categories":
+ return categories()
+ case "comics":
+ return comics(params)
+ case "searchComics":
+ return searchComics(params)
+ case "randomComics":
+ return randomComics()
+ case "leaderboard":
+ return leaderboard(params)
+ case "comicInfo":
+ return comicInfo(params)
+ case "comicEpPage":
+ return epPage(params)
+ case "comicPicturePageWithQuality":
+ return comicPicturePageWithQuality(params)
+ case "switchLike":
+ return switchLike(params)
+ case "switchFavourite":
+ return switchFavourite(params)
+ case "favouriteComics":
+ return favouriteComics(params)
+ case "recommendation":
+ return recommendation(params)
+ case "comments":
+ return comments(params)
+ case "commentChildren":
+ return commentChildren(params)
+ case "myComments":
+ return myComments(params)
+ case "postComment":
+ return postComment(params)
+ case "postChildComment":
+ return postChildComment(params)
+ case "game":
+ return game(params)
+ case "games":
+ return games(params)
+ case "viewLogPage":
+ return viewLogPage(params)
+ case "clearAllViewLog":
+ comic_center.ClearAllViewLog()
+ return "", nil
+ case "deleteViewLog":
+ comic_center.DeleteViewLog(params)
+ return "", nil
+ case "clean":
+ return "", clean()
+ case "autoClean":
+ expire, err := strconv.ParseInt(params, 10, 64)
+ if err != nil {
+ return "", err
+ }
+ return "", autoClean(expire)
+ case "storeViewEp":
+ return "", storeViewEp(params)
+ case "loadView":
+ return loadView(params)
+ case "downloadRunning":
+ return strconv.FormatBool(getDownloadRunning()), nil
+ case "setDownloadRunning":
+ b, e := strconv.ParseBool(params)
+ if e != nil {
+ setDownloadRunning(b)
+ }
+ return "", e
+ case "createDownload":
+ return "", createDownload(params)
+ case "addDownload":
+ return "", addDownload(params)
+ case "loadDownloadComic":
+ return loadDownloadComic(params)
+ case "allDownloads":
+ return allDownloads()
+ case "deleteDownloadComic":
+ return "", deleteDownloadComic(params)
+ case "downloadEpList":
+ return downloadEpList(params)
+ case "downloadPicturesByEpId":
+ return downloadPicturesByEpId(params)
+ case "resetAllDownloads":
+ return "", comic_center.ResetAll()
+ case "exportComicDownload":
+ return "", exportComicDownload(params)
+ case "exportComicDownloadToJPG":
+ return "", exportComicDownloadToJPG(params)
+ case "exportComicUsingSocket":
+ i, e := exportComicUsingSocket(params)
+ return fmt.Sprintf("%d", i), e
+ case "exportComicUsingSocketExit":
+ return "", exportComicUsingSocketExit()
+ case "importComicDownload":
+ return "", importComicDownload(params)
+ case "importComicDownloadUsingSocket":
+ return "", importComicDownloadUsingSocket(params)
+ case "remoteImageData":
+ return remoteImageData(params)
+ case "clientIpSet":
+ return clientIpSet()
+ case "downloadImagePath":
+ return downloadImagePath(params)
+ case "downloadGame":
+ return downloadGame(params)
+ case "convertImageToJPEG100":
+ return "", convertImageToJPEG100(params)
+ }
+ return "", errors.New("method not found : " + method)
+}
diff --git a/go/pikapi/database/comic_center/center.go b/go/pikapi/database/comic_center/center.go
new file mode 100644
index 0000000..b89fb29
--- /dev/null
+++ b/go/pikapi/database/comic_center/center.go
@@ -0,0 +1,593 @@
+package comic_center
+
+import (
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "path"
+ "pgo/pikapi/const_value"
+ "sync"
+ "time"
+)
+
+var mutex = sync.Mutex{}
+var db *gorm.DB
+
+func InitDBConnect(databaseDir string) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var err error
+ db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "comic_center.db")), const_value.GormConfig)
+ if err != nil {
+ panic("failed to connect database")
+ }
+ db.AutoMigrate(&Category{})
+ db.AutoMigrate(&ComicView{})
+ db.AutoMigrate(&RemoteImage{})
+ db.AutoMigrate(&ComicDownload{})
+ db.AutoMigrate(&ComicDownloadEp{})
+ db.AutoMigrate(&ComicDownloadPicture{})
+}
+
+func Transaction(t func(tx *gorm.DB) error) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(t)
+}
+
+func UpSetCategories(categories *[]Category) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ var in []string
+ for _, c := range *categories {
+ if c.ID == "" {
+ continue
+ }
+ in = append(in, c.ID)
+ err := tx.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.AssignmentColumns([]string{
+ "updated_at",
+ "title",
+ "description",
+ "is_web",
+ "active",
+ "link",
+ "thumb_original_name",
+ "thumb_file_server",
+ "thumb_path",
+ }),
+ }).Create(&c).Error
+ if err != nil {
+ return err
+ }
+ }
+ err := tx.Unscoped().Model(&Category{}).Where(" id in ?", in).Update("deleted_at", gorm.DeletedAt{
+ Valid: false,
+ }).Error
+ if err != nil {
+ return err
+ }
+ return tx.Unscoped().Model(&Category{}).Where(" id not in ?", in).Update("deleted_at", gorm.DeletedAt{
+ Time: time.Now(),
+ Valid: true,
+ }).Error
+ })
+}
+
+func NoLockActionViewComicUpdateInfoDB(view *ComicView, db *gorm.DB) error {
+ view.LastViewTime = time.Now()
+ return db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "id"}},
+ DoUpdates: clause.AssignmentColumns([]string{
+ "created_at",
+ "updated_at",
+ "title",
+ "author",
+ "pages_count",
+ "eps_count",
+ "finished",
+ "categories",
+ "thumb_original_name",
+ "thumb_file_server",
+ "thumb_path",
+ "likes_count",
+ "description",
+ "chinese_team",
+ "tags",
+ "allow_download",
+ "views_count",
+ "is_favourite",
+ "is_liked",
+ "comments_count",
+ "last_view_time",
+ }),
+ }).Create(view).Error
+}
+
+func ViewComicUpdateInfo(view *ComicView) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return NoLockActionViewComicUpdateInfoDB(view, db)
+}
+
+func ViewComic(comicId string) error {
+ return db.Model(&ComicView{}).Where(
+ "id = ?", comicId,
+ ).Update(
+ "last_view_time",
+ time.Now(),
+ ).Error
+}
+
+func ViewComicUpdateFavourite(comicId string, favourite bool) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicView{}).Where(
+ "id = ?", comicId,
+ ).Update(
+ "is_favourite",
+ favourite,
+ ).Error
+}
+
+func ViewComicUpdateLike(comicId string, like bool) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicView{}).Where(
+ "id = ?", comicId,
+ ).Update(
+ "is_like",
+ like,
+ ).Error
+}
+
+func ViewEpAndPicture(comicId string, epOrder int, epTitle string, pictureRank int) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicView{}).Where("id", comicId).Updates(
+ map[string]interface{}{
+ "last_view_time": time.Now(),
+ "last_view_ep_order": epOrder,
+ "last_view_ep_title": epTitle,
+ "last_view_picture_rank": pictureRank,
+ },
+ ).Error
+}
+
+func LoadViewLog(comicId string) (*ComicView, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var view ComicView
+ err := db.First(&view, "id = ?", comicId).Error
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+ return &view, nil
+}
+
+func FindRemoteImage(fileServer string, path string) *RemoteImage {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var remoteImage RemoteImage
+ err := db.First(&remoteImage, "file_server = ? AND path = ?", fileServer, path).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil
+ } else {
+ panic(err)
+ }
+ }
+ return &remoteImage
+}
+
+func SaveRemoteImage(remote *RemoteImage) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "file_server"}, {Name: "path"}},
+ DoUpdates: clause.AssignmentColumns([]string{
+ "updated_at",
+ "file_size",
+ "format",
+ "width",
+ "height",
+ "local_path",
+ }),
+ }).Create(remote).Error
+}
+
+func CreateDownload(comic *ComicDownload, epList *[]ComicDownloadEp) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ comic.SelectedEpCount = int32(len(*epList))
+ return db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Create(comic).Error
+ if err != nil {
+ return err
+ }
+ for _, ep := range *epList {
+ err := tx.Create(&ep).Error
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func AddDownload(comic *ComicDownload, epList *[]ComicDownloadEp) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(comic).Where("id = ?", comic.ID).Updates(map[string]interface{}{
+ "created_at": comic.CreatedAt,
+ "updated_at": comic.UpdatedAt,
+ "title": comic.Title,
+ "author": comic.Author,
+ "pages_count": comic.PagesCount,
+ "eps_count": comic.EpsCount,
+ "finished": comic.Finished,
+ "categories": comic.Categories,
+ "thumb_original_name": comic.ThumbOriginalName,
+ "thumb_file_server": comic.ThumbFileServer,
+ "thumb_path": comic.ThumbPath,
+ "description": comic.Description,
+ "chinese_team": comic.ChineseTeam,
+ "tags": comic.Tags,
+ "download_finished": false, // restart
+ }).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Exec(
+ "UPDATE comic_downloads SET eps_count = selected_ep_count + ? WHERE id = ?",
+ len(*epList), comic.ID,
+ ).Error
+ if err != nil {
+ return err
+ }
+ for _, ep := range *epList {
+ err := tx.Create(&ep).Error
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func UpdateDownloadLogo(comicId string, fileSize int64, format string, width int32, height int32, localPath string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownload{}).Where("id = ?", comicId).Updates(map[string]interface{}{
+ "thumb_file_size": fileSize,
+ "thumb_format": format,
+ "thumb_width": width,
+ "thumb_height": height,
+ "thumb_local_path": localPath,
+ }).Error
+}
+
+func FindComicDownloadById(comicId string) (*ComicDownload, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var download ComicDownload
+ err := db.First(&download, "id = ?", comicId).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &download, nil
+}
+
+func ListDownloadEpByComicId(comicId string) ([]ComicDownloadEp, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var epList []ComicDownloadEp
+ err := db.Where("comic_id = ?", comicId).Order("ep_order ASC").Find(&epList).Error
+ return epList, err
+}
+
+func ListDownloadPictureByEpId(epId string) ([]ComicDownloadPicture, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var pictureList []ComicDownloadPicture
+ err := db.Where("ep_id = ?", epId).Order("rank_in_ep ASC").Find(&pictureList).Error
+ return pictureList, err
+}
+
+func AllDownloads() (*[]ComicDownload, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var downloads []ComicDownload
+ err := db.Table("comic_downloads").
+ Joins("LEFT JOIN comic_views ON comic_views.id = comic_downloads.id").
+ Select("comic_downloads.*").
+ Order("comic_views.last_view_time DESC").
+ Scan(&downloads).Error
+ // err := db.Find(&downloads).Error
+ return &downloads, err
+}
+
+func LoadFirstNeedDownload() (*ComicDownload, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var download ComicDownload
+ err := db.First(&download, "download_failed = 0 AND pause = 0 AND deleting = 0 AND download_finished = 0").Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &download, nil
+}
+
+func LoadFirstNeedDownloadEp(comicId string) (*ComicDownloadEp, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var ep ComicDownloadEp
+ err := db.First(
+ &ep,
+ " comic_id = ? AND download_failed = 0 AND download_finished = 0 AND fetched_pictures = 1",
+ comicId,
+ ).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &ep, nil
+}
+
+func LoadFirstNeedDownloadPicture(epId string) (*ComicDownloadPicture, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var picture ComicDownloadPicture
+ err := db.First(
+ &picture,
+ "ep_id = ? AND download_failed = 0 AND download_finished = 0",
+ epId,
+ ).Error
+ if err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &picture, nil
+}
+
+func FetchPictures(comicId string, epId string, list *[]ComicDownloadPicture) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ var rankInEp int32
+ for _, picture := range *list {
+ rankInEp = rankInEp + 1
+ picture.RankInEp = rankInEp
+ err := tx.Create(&picture).Error
+ if err != nil {
+ return err
+ }
+ }
+ err := tx.Model(&ComicDownloadEp{}).Where("id = ?", epId).Updates(map[string]interface{}{
+ "fetched_pictures": true,
+ "selected_picture_count": len(*list),
+ }).Error
+ if err != nil {
+ return err
+ }
+ return tx.Exec(
+ "UPDATE comic_downloads SET selected_picture_count = selected_picture_count + ? WHERE id = ?",
+ len(*list), comicId,
+ ).Error
+ })
+}
+
+func DownloadFailed(comicId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownload{}).Where("id = ?", comicId).Update("download_failed", true).Error
+}
+
+func DownloadSuccess(comicId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownload{}).Where("id = ?", comicId).Updates(map[string]interface{}{
+ "download_finished": true,
+ "download_finished_time": time.Now(),
+ }).Error
+}
+
+func EpFailed(epId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownloadEp{}).Where("id = ?", epId).Update("download_failed", true).Error
+}
+
+func EpSuccess(comicId string, epId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(&ComicDownloadEp{}).Where("id = ?", epId).Updates(map[string]interface{}{
+ "download_finished": true,
+ "download_finished_time": time.Now(),
+ }).Error
+ if err != nil {
+ return err
+ }
+ return tx.Exec(
+ "UPDATE comic_downloads SET download_ep_count = download_ep_count + 1 WHERE id = ?",
+ comicId,
+ ).Error
+ })
+}
+
+func PictureFailed(pictureId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownloadPicture{}).Where("id = ?", pictureId).Update("download_failed", true).Error
+}
+
+func PictureSuccess(
+ comicId string, epId string, pictureId string,
+ fileSize int64, format string, width int32, height int32, localPath string,
+) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(&ComicDownloadPicture{}).Where("id = ?", pictureId).Updates(map[string]interface{}{
+ "file_size": fileSize,
+ "format": format,
+ "width": width,
+ "height": height,
+ "local_path": localPath,
+ "download_finished": true,
+ "download_finished_time": time.Now(),
+ }).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Exec(
+ "UPDATE comic_download_eps SET download_picture_count = download_picture_count + 1 WHERE id = ?",
+ epId,
+ ).Error
+ if err != nil {
+ return err
+ }
+ return tx.Exec(
+ "UPDATE comic_downloads SET download_picture_count = download_picture_count + 1 WHERE id = ?",
+ comicId,
+ ).Error
+ })
+}
+
+func ResetAll() error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Model(&ComicDownload{}).Where("1 = 1").
+ Update("download_failed", false).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Model(&ComicDownloadEp{}).Where("1 = 1").
+ Update("download_failed", false).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Model(&ComicDownloadPicture{}).Where("1 = 1").
+ Update("download_failed", false).Error
+ return err
+ })
+}
+
+func ViewLogPage(offset int, limit int) (*[]ComicView, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var list []ComicView
+ err := db.Offset(offset).Limit(limit).Order("last_view_time DESC").Find(&list).Error
+ return &list, err
+}
+
+func ClearAllViewLog() {
+ mutex.Lock()
+ defer mutex.Unlock()
+ db.Unscoped().Where("1 = 1").Delete(&ComicView{})
+}
+
+func DeleteViewLog(id string) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ db.Unscoped().Where("id = ?", id).Delete(&ComicView{})
+}
+
+func DeletingComic() (*ComicDownload, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var download ComicDownload
+ err := db.First(&download, "deleting = 1").Error
+ if err == gorm.ErrRecordNotFound {
+ return nil, nil
+ }
+ return &download, err
+}
+
+func TrueDelete(comicId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ err := db.Transaction(func(tx *gorm.DB) error {
+ err := tx.Unscoped().Delete(&ComicDownload{}, "id = ?", comicId).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Unscoped().Delete(&ComicDownloadEp{}, "comic_id = ?", comicId).Error
+ if err != nil {
+ return err
+ }
+ err = tx.Unscoped().Delete(&ComicDownloadPicture{}, "comic_id = ?", comicId).Error
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return db.Raw("VACUUM").Error
+}
+
+func Deleting(comicId string) error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Model(&ComicDownload{}).Where("id = ?", comicId).Updates(map[string]interface{}{
+ "deleting": true,
+ }).Error
+}
+
+func RemoveAllRemoteImage() error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ err := db.Unscoped().Delete(&RemoteImage{}, "1 = 1").Error
+ if err != nil {
+ return err
+ }
+ return db.Raw("VACUUM").Error
+}
+
+func EarliestRemoteImage(earliest time.Time, pageSize int) ([]RemoteImage, error) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var images []RemoteImage
+ err := db.Where("strftime('%s',updated_at) < strftime('%s',?)", earliest).
+ Order("updated_at").Limit(pageSize).Find(&images).Error
+ return images, err
+}
+
+func DeleteRemoteImages(images []RemoteImage) 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(&RemoteImage{}).Delete("id in ?", ids).Error
+}
+
+func VACUUM() error {
+ mutex.Lock()
+ defer mutex.Unlock()
+ return db.Raw("VACUUM").Error
+}
diff --git a/go/pikapi/database/comic_center/entities.go b/go/pikapi/database/comic_center/entities.go
new file mode 100644
index 0000000..6c34fb4
--- /dev/null
+++ b/go/pikapi/database/comic_center/entities.go
@@ -0,0 +1,122 @@
+package comic_center
+
+import (
+ "gorm.io/gorm"
+ "time"
+)
+
+type Category struct {
+ ID string `gorm:"primarykey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ IsWeb bool `json:"isWeb"`
+ Active bool `json:"active"`
+ Link string `json:"link"`
+ ThumbOriginalName string
+ ThumbFileServer string
+ ThumbPath string
+}
+
+type RemoteImage struct {
+ gorm.Model
+ FileServer string `gorm:"index:uk_fp,unique" json:"fileServer"`
+ Path string `gorm:"index:uk_fp,unique" json:"path"`
+ FileSize int64 `json:"fileSize"`
+ Format string `json:"format"`
+ Width int32 `json:"width"`
+ Height int32 `json:"height"`
+ LocalPath string `json:"localPath"`
+}
+
+type ComicSimple struct {
+ ID string `gorm:"primarykey" json:"id"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+ Title string `json:"title"`
+ Author string `json:"author"`
+ PagesCount int32 `json:"pagesCount"`
+ EpsCount int32 `json:"epsCount"`
+ Finished bool `json:"finished"`
+ Categories string `json:"categories"`
+ ThumbOriginalName string `json:"thumbOriginalName"`
+ ThumbFileServer string `json:"thumbFileServer"`
+ ThumbPath string `json:"thumbPath"`
+}
+
+type ComicInfo struct {
+ ComicSimple
+ LikesCount int32 `json:"likesCount"`
+ Description string `json:"description"`
+ ChineseTeam string `json:"chineseTeam"`
+ Tags string `json:"tags"`
+ AllowDownload bool `json:"allowDownload"`
+ ViewsCount int32 `json:"viewsCount"`
+ IsFavourite bool `json:"isFavourite"`
+ IsLiked bool `json:"isLiked"`
+ CommentsCount int32 `json:"commentsCount"`
+}
+
+type ComicView struct {
+ ComicInfo
+ LastViewTime time.Time `json:"lastViewTime"`
+ LastViewEpOrder int32 `json:"lastViewEpOrder"`
+ LastViewEpTitle string `json:"lastViewEpTitle"`
+ LastViewPictureRank int32 `json:"lastViewPictureRank"`
+}
+
+type ComicDownload struct {
+ ComicSimple
+ Description string `json:"description"`
+ ChineseTeam string `json:"chineseTeam"`
+ Tags string `json:"tags"`
+ SelectedEpCount int32 `json:"selectedEpCount"`
+ SelectedPictureCount int32 `json:"selectedPictureCount"`
+ DownloadEpCount int32 `json:"downloadEpCount"`
+ DownloadPictureCount int32 `json:"downloadPictureCount"`
+ DownloadFinished bool `json:"downloadFinished"`
+ DownloadFinishedTime time.Time `json:"downloadFinishedTime"`
+ DownloadFailed bool `json:"downloadFailed"`
+ Deleting bool `json:"deleting"`
+ ThumbFileSize int64 `json:"thumbFileSize"`
+ ThumbFormat string `json:"thumbFormat"`
+ ThumbWidth int32 `json:"thumbWidth"`
+ ThumbHeight int32 `json:"thumbHeight"`
+ ThumbLocalPath string `json:"thumbLocalPath"`
+ Pause bool `json:"pause"`
+}
+
+type ComicDownloadEp struct {
+ ComicId string `gorm:"index:idx_comic_id" json:"comicId"`
+ ID string `gorm:"primarykey" json:"id"`
+ UpdatedAt time.Time `json:"updated_at"`
+ EpOrder int32 `json:"epOrder"`
+ Title string `json:"title"`
+ FetchedPictures bool `json:"fetchedPictures"`
+ SelectedPictureCount int32 `json:"selectedPictureCount"`
+ DownloadPictureCount int32 `json:"downloadPictureCount"`
+ DownloadFinished bool `json:"downloadFinish"`
+ DownloadFinishedTime time.Time `json:"downloadFinishTime"`
+ DownloadFailed bool `json:"downloadFailed"`
+}
+
+type ComicDownloadPicture struct {
+ ID string `gorm:"primarykey" json:"id"`
+ ComicId string `gorm:"index:idx_comic_id" json:"comicId"`
+ EpId string `gorm:"index:idx_ep_id" json:"epId"`
+ EpOrder int32 `gorm:"index:idx_ep_order" json:"epOrder"`
+ RankInEp int32 `json:"rankInEp"`
+ DownloadFinished bool `json:"downloadFinish"`
+ DownloadFinishedTime time.Time `json:"downloadFinishTime"`
+ DownloadFailed bool `json:"downloadFailed"`
+ OriginalName string
+ FileServer string `gorm:"index:idx_fp,priority:1" json:"fileServer"`
+ Path string `gorm:"index:idx_fp,priority:2" json:"path"`
+ FileSize int64 `json:"fileSize"`
+ Format string `json:"format"`
+ Width int32 `json:"width"`
+ Height int32 `json:"height"`
+ LocalPath string `json:"localPath"`
+}
diff --git a/go/pikapi/database/network_cache/cache.go b/go/pikapi/database/network_cache/cache.go
new file mode 100644
index 0000000..a05076b
--- /dev/null
+++ b/go/pikapi/database/network_cache/cache.go
@@ -0,0 +1,99 @@
+package network_cache
+
+import (
+ "errors"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "path"
+ "pgo/pikapi/const_value"
+ "sync"
+ "time"
+)
+
+var mutex = sync.Mutex{}
+var db *gorm.DB
+
+type NetworkCache struct {
+ gorm.Model
+ K string `gorm:"index:uk_k,unique"`
+ V string
+}
+
+func InitDBConnect(databaseDir string) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var err error
+ db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "network_cache.db")), const_value.GormConfig)
+ if err != nil {
+ panic("failed to connect database")
+ }
+ db.AutoMigrate(&NetworkCache{})
+}
+
+func LoadCache(key string, expire time.Duration) string {
+ 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
+ }
+ if gorm.ErrRecordNotFound == err {
+ return ""
+ }
+ panic(errors.New("?"))
+}
+
+func SaveCache(key string, value string) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ db.Clauses(clause.OnConflict{
+ Columns: []clause.Column{{Name: "k"}},
+ DoUpdates: clause.AssignmentColumns([]string{"created_at", "updated_at", "v"}),
+ }).Create(&NetworkCache{
+ K: key,
+ V: value,
+ })
+}
+
+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 RemoveAll() 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 RemoveEarliest(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
+}
diff --git a/go/pikapi/database/properties/properties.go b/go/pikapi/database/properties/properties.go
new file mode 100644
index 0000000..0272867
--- /dev/null
+++ b/go/pikapi/database/properties/properties.go
@@ -0,0 +1,122 @@
+package properties
+
+import (
+ "errors"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
+ "gorm.io/gorm/clause"
+ "path"
+ "pgo/pikapi/const_value"
+ "strconv"
+ "sync"
+)
+
+var mutex = sync.Mutex{}
+var db *gorm.DB
+
+func InitDBConnect(databaseDir string) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ var err error
+ db, err = gorm.Open(sqlite.Open(path.Join(databaseDir, "properties.db")), const_value.GormConfig)
+ if err != nil {
+ panic("failed to connect database")
+ }
+ 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) {
+ mutex.Lock()
+ defer mutex.Unlock()
+ 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 {
+ 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(&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 SaveBoolProperty(name string, value bool) error {
+ return SaveProperty(name, strconv.FormatBool(value))
+}
+
+func SaveSwitchAddress(value string) error {
+ return SaveProperty("switch_address", value)
+}
+
+func LoadSwitchAddress() (string, error) {
+ return LoadProperty("switch_address", "")
+}
+
+func SaveProxy(value string) error {
+ return SaveProperty("proxy", value)
+}
+
+func LoadProxy() (string, error) {
+ return LoadProperty("proxy", "")
+}
+
+func SaveUsername(value string) error {
+ return SaveProperty("username", value)
+}
+
+func LoadUsername() (string, error) {
+ return LoadProperty("username", "")
+}
+
+func SavePassword(value string) error {
+ return SaveProperty("password", value)
+}
+
+func LoadPassword() (string, error) {
+ return LoadProperty("password", "")
+}
+
+func SaveToken(value string) {
+ SaveProperty("token", value)
+}
+
+func LoadToken() (string, error) {
+ return LoadProperty("token", "")
+}
+
+func SaveTokenTime(value int64) {
+ SaveProperty("token_time", strconv.FormatInt(value, 10))
+}
+
+func LoadTokenTime() (int64, error) {
+ str, err := LoadProperty("token_time", "0")
+ if err != nil {
+ return 0, err
+ }
+ return strconv.ParseInt(str, 10, 64)
+}
diff --git a/go/pikapi/utils/file.go b/go/pikapi/utils/file.go
new file mode 100644
index 0000000..fb8f05f
--- /dev/null
+++ b/go/pikapi/utils/file.go
@@ -0,0 +1,19 @@
+package utils
+
+import (
+ "os"
+ "pgo/pikapi/const_value"
+)
+
+func Mkdir(dir string) {
+ if _, err := os.Stat(dir); err != nil {
+ if os.IsNotExist(err) {
+ err = os.MkdirAll(dir, const_value.CreateDirMode)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ panic(err)
+ }
+ }
+}
diff --git a/go/pikapi/utils/mutex.go b/go/pikapi/utils/mutex.go
new file mode 100644
index 0000000..9f73309
--- /dev/null
+++ b/go/pikapi/utils/mutex.go
@@ -0,0 +1,21 @@
+package utils
+
+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)))]
+}
diff --git a/go/pikapi/utils/time.go b/go/pikapi/utils/time.go
new file mode 100644
index 0000000..6dc3864
--- /dev/null
+++ b/go/pikapi/utils/time.go
@@ -0,0 +1,7 @@
+package utils
+
+import "time"
+
+func Timestamp() int64 {
+ return time.Now().UnixNano() / int64(time.Millisecond)
+}
diff --git a/images/categories_screen.png b/images/categories_screen.png
new file mode 100644
index 0000000..c460124
Binary files /dev/null and b/images/categories_screen.png differ
diff --git a/images/comic_list.png b/images/comic_list.png
new file mode 100644
index 0000000..3fae13c
Binary files /dev/null and b/images/comic_list.png differ
diff --git a/images/download_list_screen.png b/images/download_list_screen.png
new file mode 100644
index 0000000..b57617c
Binary files /dev/null and b/images/download_list_screen.png differ
diff --git a/images/exporting.png b/images/exporting.png
new file mode 100644
index 0000000..c5b8669
Binary files /dev/null and b/images/exporting.png differ
diff --git a/images/exporting2.png b/images/exporting2.png
new file mode 100644
index 0000000..9df3ff5
Binary files /dev/null and b/images/exporting2.png differ
diff --git a/images/game.png b/images/game.png
new file mode 100644
index 0000000..c7a1d5a
Binary files /dev/null and b/images/game.png differ
diff --git a/images/games.png b/images/games.png
new file mode 100644
index 0000000..04113e1
Binary files /dev/null and b/images/games.png differ
diff --git a/images/platforms.png b/images/platforms.png
new file mode 100644
index 0000000..c7d517e
Binary files /dev/null and b/images/platforms.png differ
diff --git a/images/reader.png b/images/reader.png
new file mode 100644
index 0000000..b4ee774
Binary files /dev/null and b/images/reader.png differ
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..151026b
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,33 @@
+*.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
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..9367d48
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 8.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..1e8c3c9
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.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
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 0000000..24b1049
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,28 @@
+PODS:
+ - Flutter (1.0.0)
+ - "permission_handler (5.1.0+2)":
+ - Flutter
+ - url_launcher (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - permission_handler (from `.symlinks/plugins/permission_handler/ios`)
+ - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ permission_handler:
+ :path: ".symlinks/plugins/permission_handler/ios"
+ url_launcher:
+ :path: ".symlinks/plugins/url_launcher/ios"
+
+SPEC CHECKSUMS:
+ Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
+ permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0
+ url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
+
+PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+
+COCOAPODS: 1.10.1
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..36c179e
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,555 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 50;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 605DB0C59210B25A843453FD /* Pods_Runner.framework */; };
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 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 */; };
+ DDEFBAAB26AAE3AA00159A13 /* Pikapi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDEFBAAA26AAE3AA00159A13 /* Pikapi.framework */; };
+/* 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 */
+ 1001C50AAB0DFA884ACAD48C /* 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 = ""; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3742BDBA4B7EA3162E2CDC75 /* 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 = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 605DB0C59210B25A843453FD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 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 = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ CA7EB5DA1FDE22BAC5B01D77 /* 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 = ""; };
+ DDEFBAAA26AAE3AA00159A13 /* Pikapi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pikapi.framework; path = ../go/mobile/lib/Pikapi.framework; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DDEFBAAB26AAE3AA00159A13 /* Pikapi.framework in Frameworks */,
+ 0E44DEFD92B805627806403C /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 6CBB90743F7578DFC9C6BF75 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 3742BDBA4B7EA3162E2CDC75 /* Pods-Runner.debug.xcconfig */,
+ 1001C50AAB0DFA884ACAD48C /* Pods-Runner.release.xcconfig */,
+ CA7EB5DA1FDE22BAC5B01D77 /* Pods-Runner.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ DDEFBAAA26AAE3AA00159A13 /* Pikapi.framework */,
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 6CBB90743F7578DFC9C6BF75 /* Pods */,
+ F6DB48AA376F5D49016BEA7A /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 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 = "";
+ };
+ F6DB48AA376F5D49016BEA7A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 605DB0C59210B25A843453FD /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 6D683F8ECDB7CFFB7E7E554B /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ A66AE356A44E049B8DF0FD4F /* [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 = 1020;
+ 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";
+ };
+ 6D683F8ECDB7CFFB7E7E554B /* [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;
+ };
+ 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";
+ };
+ A66AE356A44E049B8DF0FD4F /* [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;
+ };
+/* 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 = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* 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 = 10.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;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = niuhuan.pikapi;
+ 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 = 10.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 = 10.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;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = niuhuan.pikapi;
+ 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;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = niuhuan.pikapi;
+ 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 */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..a28140c
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..3fd07f4
--- /dev/null
+++ b/ios/Runner/AppDelegate.swift
@@ -0,0 +1,104 @@
+import UIKit
+import Flutter
+import Pikapi
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+
+ let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
+
+ MobileInitApplication(documentsPath)
+
+ let controller = self.window.rootViewController as! FlutterViewController
+ let channel = FlutterMethodChannel.init(name: "method", binaryMessenger: controller as! FlutterBinaryMessenger)
+
+ channel.setMethodCallHandler { (call, result) in
+ Thread {
+ if call.method == "flatInvoke" {
+ if let args = call.arguments as? Dictionary,
+ 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 == "iosSaveFileToImage"{
+ if let args = call.arguments as? Dictionary,
+ 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()
+ }
+
+ //
+ let eventChannel = FlutterEventChannel.init(name: "flatEvent", binaryMessenger: controller as! FlutterBinaryMessenger)
+
+ class EventChannelHandler:NSObject, FlutterStreamHandler {
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ objc_sync_enter(mutex)
+ sink = events
+ objc_sync_exit(mutex)
+ return nil
+ }
+
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
+ objc_sync_enter(mutex)
+ sink = nil
+ objc_sync_exit(mutex)
+ return nil
+ }
+ }
+ class EventNotifyHandler:NSObject, MobileEventNotifyHandlerProtocol {
+ func onNotify(_ message: String?) {
+ objc_sync_enter(mutex)
+ if sink != nil {
+ sink?(message)
+ }
+ objc_sync_exit(mutex)
+ }
+ }
+ eventChannel.setStreamHandler(EventChannelHandler.init())
+ MobileEventNotify(EventNotifyHandler.init())
+
+ //
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
+
+
+var sink : FlutterEventSink?
+let mutex = NSObject.init()
+
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "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"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..ea51e74
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..9b01d66
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..7521e9c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..3d276e7
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..af7aea3
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..4bd9511
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..9584d06
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..7521e9c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..927ef0f
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..3facad9
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..3facad9
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..9db0248
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..d4d448b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..ad6423c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..c920d8e
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "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"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# 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.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..7acbd3b
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ NSPhotoLibraryUsageDescription
+ Save images
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ pikapi
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/lib/assets/android.svg b/lib/assets/android.svg
new file mode 100644
index 0000000..8a843bd
--- /dev/null
+++ b/lib/assets/android.svg
@@ -0,0 +1,64 @@
+
+
+
+
diff --git a/lib/assets/apple.svg b/lib/assets/apple.svg
new file mode 100644
index 0000000..73630c9
--- /dev/null
+++ b/lib/assets/apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/assets/books.svg b/lib/assets/books.svg
new file mode 100644
index 0000000..98b1894
--- /dev/null
+++ b/lib/assets/books.svg
@@ -0,0 +1,33 @@
+
\ No newline at end of file
diff --git a/lib/assets/error.png b/lib/assets/error.png
new file mode 100644
index 0000000..690d3fa
Binary files /dev/null and b/lib/assets/error.png differ
diff --git a/lib/assets/gamepad.svg b/lib/assets/gamepad.svg
new file mode 100644
index 0000000..11d887a
--- /dev/null
+++ b/lib/assets/gamepad.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/assets/github.svg b/lib/assets/github.svg
new file mode 100644
index 0000000..aa05db9
--- /dev/null
+++ b/lib/assets/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/assets/init.jpg b/lib/assets/init.jpg
new file mode 100644
index 0000000..fc8e849
Binary files /dev/null and b/lib/assets/init.jpg differ
diff --git a/lib/assets/random.svg b/lib/assets/random.svg
new file mode 100644
index 0000000..3b14e3b
--- /dev/null
+++ b/lib/assets/random.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/assets/rankings.svg b/lib/assets/rankings.svg
new file mode 100644
index 0000000..e7f4836
--- /dev/null
+++ b/lib/assets/rankings.svg
@@ -0,0 +1,78 @@
+
+
+
diff --git a/lib/assets/unknown.svg b/lib/assets/unknown.svg
new file mode 100644
index 0000000..eab3a80
--- /dev/null
+++ b/lib/assets/unknown.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/lib/basic/Channels.dart b/lib/basic/Channels.dart
new file mode 100644
index 0000000..229e017
--- /dev/null
+++ b/lib/basic/Channels.dart
@@ -0,0 +1,49 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/services.dart';
+
+var _eventChannel = EventChannel("flatEvent");
+StreamSubscription? _eventChannelListen;
+
+Map _eventMap = {};
+
+void registerEvent(void Function(String args) eventHandler, String eventName) {
+ if (_eventMap.containsKey(eventHandler)) {
+ throw 'once register';
+ }
+ _eventMap[eventHandler] = eventName;
+ if (_eventMap.length == 1) {
+ _eventChannelListen =
+ _eventChannel.receiveBroadcastStream().listen(_onFlatEvent);
+ }
+}
+
+void unregisterEvent(void Function(String args) eventHandler) {
+ if (!_eventMap.containsKey(eventHandler)) {
+ throw 'no register';
+ }
+ _eventMap.remove(eventHandler);
+ if (_eventMap.length == 0) {
+ _eventChannelListen?.cancel();
+ }
+}
+
+void _onFlatEvent(dynamic t) {
+ _FlatEvent e = _FlatEvent.fromJson(jsonDecode(t));
+ _eventMap.forEach((key, value) {
+ if (value == e.function) {
+ key(e.content);
+ }
+ });
+}
+
+class _FlatEvent {
+ late String function;
+ late String content;
+
+ _FlatEvent.fromJson(Map json) {
+ this.function = json["function"];
+ this.content = json["content"];
+ }
+}
diff --git a/lib/basic/Common.dart b/lib/basic/Common.dart
new file mode 100644
index 0000000..a6bcd75
--- /dev/null
+++ b/lib/basic/Common.dart
@@ -0,0 +1,274 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+
+double coverWidth = 210;
+double coverHeight = 315;
+
+String categoryTitle(String? categoryTitle) {
+ return categoryTitle ?? "全分类";
+}
+
+/// 显示一个toast
+void defaultToast(BuildContext context, String title) {
+ showToast(
+ title,
+ context: context,
+ position: StyledToastPosition.center,
+ animation: StyledToastAnimation.scale,
+ reverseAnimation: StyledToastAnimation.fade,
+ duration: Duration(seconds: 4),
+ animDuration: Duration(seconds: 1),
+ curve: Curves.elasticOut,
+ reverseCurve: Curves.linear,
+ );
+}
+
+/// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true
+Future confirmDialog(
+ BuildContext context, String title, String content) async {
+ return await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(title),
+ content: new SingleChildScrollView(
+ child: new ListBody(
+ children: [
+ new Text(content),
+ ],
+ ),
+ ),
+ actions: [
+ new MaterialButton(
+ child: new Text('取消'),
+ onPressed: () {
+ Navigator.of(context).pop(false);
+ },
+ ),
+ new MaterialButton(
+ child: new Text('确定'),
+ 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: new SingleChildScrollView(
+ child: new ListBody(
+ children: [
+ new Text(content),
+ ],
+ ),
+ ),
+ actions: [
+ new MaterialButton(
+ child: new Text('确定'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ ));
+}
+
+/// stream-filter的替代方法
+List filteredList(List list, bool Function(T) filter) {
+ List result = [];
+ list.forEach((element) {
+ if (filter(element)) {
+ result.add(element);
+ }
+ });
+ return result;
+}
+
+/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容
+Future chooseListDialog(
+ BuildContext context,
+ String title,
+ List items,
+) async {
+ return showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return SimpleDialog(
+ title: Text(title),
+ children: items
+ .map((e) => SimpleDialogOption(
+ onPressed: () {
+ Navigator.of(context).pop(e);
+ },
+ child: Text('$e'),
+ ))
+ .toList(),
+ );
+ },
+ );
+}
+
+/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value)
+Future chooseMapDialog(
+ BuildContext buildContext, Map values, String title) async {
+ return await showDialog(
+ 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 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: EdgeInsets.only(top: 20, bottom: 10),
+ child: Text(
+ desc,
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context)
+ .textTheme
+ .bodyText1
+ ?.color
+ ?.withOpacity(.5)),
+ ),
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ MaterialButton(
+ child: Text('取消'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ MaterialButton(
+ child: 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);
+ 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 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: [
+ MaterialButton(
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ child: Text('取消'),
+ ),
+ MaterialButton(
+ onPressed: () {
+ Navigator.pop(context, _textEditController.text);
+ },
+ child: Text('确定'),
+ ),
+ ],
+ );
+ },
+ );
+}
diff --git a/lib/basic/Cross.dart b/lib/basic/Cross.dart
new file mode 100644
index 0000000..9db159c
--- /dev/null
+++ b/lib/basic/Cross.dart
@@ -0,0 +1,98 @@
+/// 与平台交互的操作
+
+import 'dart:io';
+import 'package:clipboard/clipboard.dart';
+import 'package:filesystem_picker/filesystem_picker.dart';
+import 'package:flutter/material.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:pikapi/basic/Common.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'Method.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 openUrl(String url) async {
+ if (await canLaunch(url)) {
+ await launch(
+ url,
+ forceSafariVC: false,
+ );
+ }
+}
+
+/// 保存图片
+Future saveImage(String path, BuildContext context) async {
+ Future? future;
+ if (Platform.isIOS) {
+ future = method.iosSaveFileToImage(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 = method.convertImageToJPEG100(path, folder);
+ }
+ }
+ if (future != null) {
+ try {
+ await future;
+ defaultToast(context, '保存成功');
+ } catch (e, s) {
+ print("$e\n$s");
+ defaultToast(context, '保存失败');
+ }
+ } else {
+ defaultToast(context, '暂不支持该平台');
+ }
+}
+
+Future _saveImageAndroid(String path, BuildContext context) async {
+ var p = await Permission.storage.request();
+ if (!p.isGranted) {
+ return;
+ }
+ return method.androidSaveFileToImage(path);
+}
+
+/// 选择一个文件夹用于保存文件
+Future chooseFolder(BuildContext context) async {
+ late String root;
+ if (Platform.isWindows) {
+ root = '/';
+ } else if (Platform.isMacOS) {
+ root = '/Users';
+ } else if (Platform.isLinux) {
+ root = '/';
+ } else if (Platform.isAndroid) {
+ var p = await Permission.storage.request();
+ if (!p.isGranted) {
+ return null;
+ }
+ root = '/storage/emulated/0';
+ } else {
+ throw 'error';
+ }
+ return FilesystemPicker.open(
+ title: '选择一个文件夹',
+ pickText: '将文件保存到这里',
+ context: context,
+ fsType: FilesystemType.folder,
+ rootDirectory: Directory(root),
+ );
+}
+
+void confirmCopy(BuildContext context, String content) async {
+ if (await confirmDialog(context, "复制", content)) {
+ copyToClipBoard(context, content);
+ }
+}
diff --git a/lib/basic/Entities.dart b/lib/basic/Entities.dart
new file mode 100644
index 0000000..375d6a3
--- /dev/null
+++ b/lib/basic/Entities.dart
@@ -0,0 +1,582 @@
+class PicaImage {
+ late String originalName;
+ late String path;
+ late String fileServer;
+
+ PicaImage.fromJson(Map json) {
+ this.originalName = json["originalName"];
+ this.path = json["path"];
+ this.fileServer = json["fileServer"];
+ }
+}
+
+class BasicUser {
+ late String id;
+ late String gender;
+ late String name;
+ late String title;
+ late bool verified;
+ late int exp;
+ late int level;
+ late List characters;
+ late PicaImage avatar;
+
+ BasicUser.fromJson(Map json) {
+ this.id = json["_id"];
+ this.gender = json["gender"];
+ this.name = json["name"];
+ this.title = json["title"];
+ this.verified = json["verified"];
+ this.exp = json["exp"];
+ this.level = json["level"];
+ this.characters = List.of(json["characters"]).map((e) => "$e").toList();
+ this.avatar = PicaImage.fromJson(Map.of(json["avatar"]));
+ }
+}
+
+class UserProfile extends BasicUser {
+ late String birthday;
+ late String email;
+ late String createdAt;
+ late bool isPunched;
+
+ UserProfile.fromJson(Map json) : super.fromJson(json) {
+ this.birthday = json["birthday"];
+ this.email = json["email"];
+ this.createdAt = json["created_at"];
+ this.isPunched = json["isPunched"];
+ }
+}
+
+class Page {
+ late int total;
+ late int limit;
+ late int page;
+ late int pages;
+
+ Page.fromJson(Map json) {
+ this.total = json["total"];
+ this.limit = json["limit"];
+ this.page = json["page"];
+ this.pages = json["pages"];
+ }
+}
+
+class Category {
+ late String id;
+ late String title;
+ late String description;
+ late PicaImage thumb;
+ late bool isWeb;
+ late bool active;
+ late String link;
+
+ Category.fromJson(Map json) {
+ this.id = json["_id"];
+ this.title = json["title"];
+ this.description = json["description"];
+ this.thumb = PicaImage.fromJson(json["thumb"]);
+ this.isWeb = json["isWeb"];
+ this.active = json["active"];
+ this.link = json["link"];
+ }
+}
+
+class ComicsPage extends Page {
+ late List docs;
+
+ ComicsPage.fromJson(Map json) : super.fromJson(json) {
+ this.docs = List.from(json["docs"])
+ .map((e) => Map.from(e))
+ .map((e) => ComicSimple.fromJson(e))
+ .toList();
+ }
+}
+
+class ComicSimple {
+ late String id;
+ late String title;
+ late String author;
+ late int pagesCount;
+ late int epsCount;
+ late bool finished;
+ late List categories;
+ late PicaImage thumb;
+ late int likesCount;
+
+ ComicSimple.fromJson(Map json) {
+ this.id = json["_id"];
+ this.title = json["title"];
+ this.author = json["author"];
+ this.pagesCount = json["pagesCount"];
+ this.epsCount = json["epsCount"];
+ this.finished = json["finished"];
+ this.categories = List.from(json["categories"]);
+ this.thumb = PicaImage.fromJson(json["thumb"]);
+ this.likesCount = json["likesCount"];
+ }
+}
+
+class ComicInfo extends ComicSimple {
+ late String description;
+ late String chineseTeam;
+ late List tags;
+ late String updatedAt;
+ late String createdAt;
+ late bool allowDownload;
+ late int viewsCount;
+ late bool isFavourite;
+ late bool isLiked;
+ late int commentsCount;
+ late Creator creator;
+
+ ComicInfo.fromJson(Map json) : super.fromJson(json) {
+ this.description = json["description"];
+ this.chineseTeam = json["chineseTeam"];
+ this.tags = List.from(json["tags"]);
+ this.updatedAt = (json["updated_at"]);
+ this.createdAt = (json["created_at"]);
+ this.allowDownload = json["allowDownload"];
+ this.viewsCount = json["viewsCount"];
+ this.isFavourite = json["isFavourite"];
+ this.isLiked = json["isLiked"];
+ this.commentsCount = json["commentsCount"];
+ this.creator = Creator.fromJson(Map.of(json["_creator"]));
+ }
+}
+
+class Creator extends BasicUser {
+ late String slogan;
+ late String role;
+ late String character;
+
+ Creator.fromJson(Map json) : super.fromJson(json) {
+ this.slogan = json["slogan"];
+ this.role = json["role"];
+ this.character = json["character"];
+ }
+}
+
+class Ep {
+ late String id;
+ late String title;
+ late int order;
+ late String updatedAt;
+
+ Ep.fromJson(Map json) {
+ this.id = json["_id"];
+ this.title = json["title"];
+ this.order = json["order"];
+ this.updatedAt = (json["updated_at"]);
+ }
+}
+
+class EpPage extends Page {
+ late List docs;
+
+ EpPage.fromJson(Map json) : super.fromJson(json) {
+ this.docs = List.from(json["docs"])
+ .map((e) => Map.from(e))
+ .map((e) => Ep.fromJson(e))
+ .toList();
+ }
+}
+
+class PicturePage extends Page {
+ late List