shadow categories action button

This commit is contained in:
niuhuan 2021-09-30 07:57:09 +08:00
parent b19f9627bc
commit fd175feb80
261 changed files with 20317 additions and 0 deletions

52
.gitignore vendored Normal file
View File

@ -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/

10
.metadata Normal file
View File

@ -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

20
LICENSE Normal file
View File

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

148
README.md
View File

@ -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
```
## 请您遵守使用规则
本软件或本软件的拓展, 个人或企业不可用于商业用途, 不可上架任何商店
拓展包括但是不限于以下内容
- 使用本软件进行继续开发形成的软件。
- 引入本软件部分内容为依赖/参考本软件/使用本软件内代码的同时, 包含本软件内一致内容或功能的软件。
- 直接对本软件进行打包发布

11
android/.gitignore vendored Normal file
View File

@ -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

64
android/app/build.gradle Normal file
View File

@ -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"])
}

View File

@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="niuhuan.pikapi">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

View File

@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="niuhuan.pikapi">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="pikapi"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
</application>
</manifest>

View File

@ -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<String> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mixDisplay()?.let { display ->
return display.supportedModes.map { mode ->
mode.toString()
}
}
}
return ArrayList()
}
private fun setMode(string: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mixDisplay()?.let { display ->
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"
}
}
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

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

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="niuhuan.pikapi">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

29
android/build.gradle Normal file
View File

@ -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
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@ -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

11
android/settings.gradle Normal file
View File

@ -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"

3
go/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build
.last_goflutter_check
.last_go-flutter_check

BIN
go/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

57
go/cmd/init.go Normal file
View File

@ -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)
}

66
go/cmd/main.go Normal file
View File

@ -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
}

14
go/cmd/options.go Normal file
View File

@ -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{}),
}

View File

@ -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
}

17
go/go.mod Normal file
View File

@ -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
)

81
go/go.sum Normal file
View File

@ -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=

9
go/hover.yaml Normal file
View File

@ -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

View File

@ -0,0 +1 @@
gomobile bind -target=android/arm,android/arm64,android/386 -o lib/Pikapi.aar ./

View File

@ -0,0 +1 @@
gomobile bind -target=android/arm -o lib/Pikapi.aar ./

1
go/mobile/bind-ios.sh Normal file
View File

@ -0,0 +1 @@
gomobile bind -target=ios -o lib/Pikapi.framework ./

0
go/mobile/lib/.keep Normal file
View File

22
go/mobile/mobile.go Normal file
View File

@ -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)
}

View File

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

View File

@ -0,0 +1,3 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./build/{{.executableName}}

View File

@ -0,0 +1,9 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Categories=
Comment={{.description}}
Name={{.applicationName}}
Icon={{.iconPath}}
Exec={{.executablePath}}

View File

@ -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)
}

View File

@ -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),
}
)

View File

@ -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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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)
},
)
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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), &paramsStruct)
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 = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
}
html {
color: white;
}
html, body {
height: 100%;
}
#leftNav {
position: fixed;
width: 350px;
height: 100%;
}
#leftNav > * {
display: inline-block;
vertical-align: top;
}
#leftNav > ul {
background: #333;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
width: 300px;
}
#leftNav > #slider {
margin-top: 1em;
float: right;
width: 40px;
border: none;
background: rgba(0, 0, 0, .4);
color: white;
}
#title > img {
display: block;
width: 80px;
margin: 30px auto;
}
#title > p {
text-align: center;
}
#title {
margin-bottom: 30px;
}
#leftNav > ul a {
margin: auto;
display: block;
color: white;
height: 40px;
line-height: 40px;
text-align: center;
width: 280px;
border-top: #666 solid 1px;
text-decoration: none;
}
#leftNav > ul a:hover,#leftNav > ul a.active {
background: rgba(255, 255, 255, .1);
}
#content {
width: 100%;
height: 100%;
background: black;
}
#content img {
width: 100%;
}
</style>
<script src="data.js"></script>
<script>
function changeLeftNav() {
var doc = document.getElementById("leftNav")
if (doc.style.left) {
doc.style.left = ""
} else {
doc.style.left = "-300px"
}
}
function changeEp(epIndex) {
var ps = data.epList[epIndex].pictureList;
document.getElementById('content').innerHTML = "";
var d = document.createElement('div');
d.id = 'd';
document.getElementById('content').append(d);
for (var i = 0; i < ps.length; i++) {
var img = document.createElement('img');
img.src = ps[i].srcPath;
document.getElementById('content').append(img);
}
document.getElementById('d').scrollIntoView();
changeLeftNav();
var as = document.getElementById('leftNav').getElementsByTagName('a');
for (var i = 0; i < ps.length; i++) {
if(epIndex == i){
as[i].classList = ["active"];
}else{
as[i].className = "";
}
}
}
</script>
</head>
<body>
<div id="leftNav">
<ul>
<li id="title">
<script>
document.write('<img src="logo" /> <br/>')
document.write('<p>' + data.title + '</p>');
</script>
</li>
<script>
for (var i = 0; i < data.epList.length; i++) {
document.write('<li><a href="javascript:changeEp(' + i + ')">' + data.epList[i].title + '</a></li>');
}
</script>
</ul>
<button id="slider" onclick="changeLeftNav();">切换</button>
</div>
<div id="content">
</div>
</body>
</html>
`
func exportComicDownloadToJPG(params string) error {
var paramsStruct struct {
ComicId string `json:"comicId"`
Dir string `json:"dir"`
}
json.Unmarshal([]byte(params), &paramsStruct)
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
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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
})
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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), &paramsStruct)
return properties.SaveProperty(paramsStruct.Name, paramsStruct.Value)
}
func loadProperty(params string) (string, error) {
var paramsStruct struct {
Name string `json:"name"`
DefaultValue string `json:"defaultValue"`
}
json.Unmarshal([]byte(params), &paramsStruct)
return properties.LoadProperty(paramsStruct.Name, paramsStruct.DefaultValue)
}
func 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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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), &paramsStruct)
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)
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}

19
go/pikapi/utils/file.go Normal file
View File

@ -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)
}
}
}

21
go/pikapi/utils/mutex.go Normal file
View File

@ -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)))]
}

7
go/pikapi/utils/time.go Normal file
View File

@ -0,0 +1,7 @@
package utils
import "time"
func Timestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
images/comic_list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
images/exporting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
images/exporting2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
images/game.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

BIN
images/games.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

BIN
images/platforms.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
images/reader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

33
ios/.gitignore vendored Normal file
View File

@ -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

View File

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

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

41
ios/Podfile Normal file
View File

@ -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

28
ios/Podfile.lock Normal file
View File

@ -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

View File

@ -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 = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
DDEFBAAA26AAE3AA00159A13 /* Pikapi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pikapi.framework; path = ../go/mobile/lib/Pikapi.framework; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
DDEFBAAA26AAE3AA00159A13 /* Pikapi.framework */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
6CBB90743F7578DFC9C6BF75 /* Pods */,
F6DB48AA376F5D49016BEA7A /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
F6DB48AA376F5D49016BEA7A /* Frameworks */ = {
isa = PBXGroup;
children = (
605DB0C59210B25A843453FD /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
FRAMEWORK_SEARCH_PATHS = "../go/mobile/lib/**";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -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<String, Any>,
let method = args["method"] as? String,
let params = args["params"] as? String{
var error: NSError?
let data = MobileFlatInvoke(method, params, &error)
if error != nil {
result(FlutterError(code: "", message: error?.localizedDescription, details: ""))
}else{
result(data)
}
}else{
result(FlutterError(code: "", message: "params error", details: ""))
}
}
else if call.method == "iosSaveFileToImage"{
if let args = call.arguments as? Dictionary<String, Any>,
let path = args["path"] as? String{
do {
let fileURL: URL = URL(fileURLWithPath: path)
let imageData = try Data(contentsOf: fileURL)
if let uiImage = UIImage(data: imageData) {
UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil)
result("OK")
}else{
result(FlutterError(code: "", message: "Error loading image ", details: ""))
}
} catch {
result(FlutterError(code: "", message: "Error loading image : \(error)", details: ""))
}
}else{
result(FlutterError(code: "", message: "params error", details: ""))
}
}
else{
result(FlutterMethodNotImplemented)
}
}.start()
}
//
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()

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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"
}
}

Some files were not shown because too many files have changed in this diff Show More