Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ddb5db57b | |||
| 4f70cc9283 | |||
| 23b643d637 | |||
| fdfc256c4d | |||
| fba56c1b75 | |||
| 4743bfacf7 | |||
| 2356537f7c | |||
| fa071aee84 | |||
| c00ca23a8b | |||
| 733b017936 | |||
| 4147f2e368 | |||
| 154b9992eb | |||
| 88b881b043 | |||
| 5d1491fb8c | |||
| 3a33196cf1 | |||
| fa8e0478da | |||
| 7e7e069244 | |||
| 18e0d34af0 | |||
| 3fe3f35483 | |||
| cf8e274883 | |||
| 10dee8b345 | |||
| ae8d30593f | |||
| 9cde46b5da | |||
| 8e61632155 | |||
| e2c4b4cb57 | |||
| 326da504ea | |||
| c5874a3f10 | |||
| 02802fab97 | |||
| 29dea10be2 | |||
| 6bc36193dc | |||
| 81e123388e | |||
| 8ebd7869a5 | |||
| 7a2f5f13f1 | |||
| 25d7dad39f | |||
| c8f8795920 | |||
| 6fd8b36dca |
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
# Gradle wrapper
|
# Gradle wrapper
|
||||||
*.jar binary
|
*.jar binary
|
||||||
|
|
||||||
# Images
|
# Binary files types
|
||||||
*.webp binary
|
*.webp binary
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ git config --global user.name "github-actions[bot]"
|
|||||||
git status
|
git status
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Update preview repository"
|
git commit -m "Updated to $latest"
|
||||||
git push
|
git push
|
||||||
else
|
else
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build pull request
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout pull request
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build artifacts and release
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -28,10 +28,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
- name: Checkout master branch
|
- name: Checkout ${{ github.ref }}
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: ${{ github.ref }}
|
||||||
path: master
|
path: master
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package xyz.nulldev.ts.config
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
@@ -41,21 +42,34 @@ open class ConfigManager {
|
|||||||
*/
|
*/
|
||||||
fun loadConfigs(): Config {
|
fun loadConfigs(): Config {
|
||||||
//Load reference configs
|
//Load reference configs
|
||||||
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||||
|
val baseConfig =
|
||||||
|
ConfigFactory.parseMap(
|
||||||
|
mapOf(
|
||||||
|
"ts.server.rootDir" to ApplicationRootDir
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
//Load user config
|
//Load user config
|
||||||
val userConfig =
|
val userConfig =
|
||||||
File(ApplicationRootDir, "server.conf").let {
|
File(ApplicationRootDir, "server.conf").let {
|
||||||
ConfigFactory.parseFile(it)
|
ConfigFactory.parseFile(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val config = ConfigFactory.empty()
|
val config = ConfigFactory.empty()
|
||||||
|
.withFallback(baseConfig)
|
||||||
.withFallback(userConfig)
|
.withFallback(userConfig)
|
||||||
.withFallback(compatConfig)
|
.withFallback(compatConfig)
|
||||||
.withFallback(serverConfig)
|
.withFallback(serverConfig)
|
||||||
.resolve()
|
.resolve()
|
||||||
|
|
||||||
|
// set log level early
|
||||||
|
if (debugLogsEnabled(config)) {
|
||||||
|
setLogLevel(Level.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug {
|
logger.debug {
|
||||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xyz.nulldev.ts.config
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.slf4j.Logger
|
||||||
|
|
||||||
|
fun setLogLevel(level: Level) {
|
||||||
|
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
|
||||||
|
}
|
||||||
|
|
||||||
|
fun debugLogsEnabled(config: Config)
|
||||||
|
= System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
| Build | Stable | Preview | Support Server |
|
| Build | Stable | Preview | Support Server |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk-preview/tree/main/) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
|
|||||||
+2
-2
@@ -59,14 +59,14 @@ configure(projects) {
|
|||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||||
|
|
||||||
// RxJava
|
// ReactiveX
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||||
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
|
|
||||||
// JSoup
|
// JSoup
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
|
||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.1")
|
implementation("com.typesafe:config:1.4.1")
|
||||||
implementation("io.github.config4k:config4k:0.4.2")
|
implementation("io.github.config4k:config4k:0.4.2")
|
||||||
|
|||||||
Binary file not shown.
@@ -1,3 +1,3 @@
|
|||||||
# Building `Tachidesk Launcher.exe`
|
# Building `Tachidesk Launcher.exe`
|
||||||
1. compile `Tachidesk Launcher.c` statically using GCC MinGW: `gcc -o "Tachidesk Launcher.exe" "Tachidesk Launcher.c"`
|
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
|
||||||
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
||||||
|
|||||||
+26
-36
@@ -12,48 +12,27 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
maven {
|
||||||
|
url = uri("https://repo1.maven.org/maven2/")
|
||||||
|
}
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// okhttp
|
||||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
|
||||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
|
||||||
|
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
|
||||||
|
|
||||||
val okhttpVersion = "4.10.0-RC1"
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
|
// Javalin api
|
||||||
// Retrofit
|
|
||||||
val retrofitVersion = "2.9.0"
|
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
|
||||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
|
||||||
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
|
|
||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
|
||||||
|
|
||||||
|
|
||||||
// Reactivex
|
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
|
||||||
|
|
||||||
|
|
||||||
// api
|
|
||||||
implementation("io.javalin:javalin:3.13.6")
|
implementation("io.javalin:javalin:3.13.6")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
|
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposedVersion = "0.31.1"
|
val exposedVersion = "0.31.1"
|
||||||
@@ -61,7 +40,6 @@ dependencies {
|
|||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||||
|
|
||||||
// current database driver
|
// current database driver
|
||||||
implementation("com.h2database:h2:1.4.200")
|
implementation("com.h2database:h2:1.4.200")
|
||||||
|
|
||||||
@@ -69,7 +47,19 @@ dependencies {
|
|||||||
implementation("com.dorkbox:SystemTray:4.1")
|
implementation("com.dorkbox:SystemTray:4.1")
|
||||||
implementation("com.dorkbox:Utilities:1.9")
|
implementation("com.dorkbox:Utilities:1.9")
|
||||||
|
|
||||||
implementation("com.google.guava:guava:30.1.1-jre")
|
|
||||||
|
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
|
||||||
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.9.1")
|
||||||
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
|
implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
|
|
||||||
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
|
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||||
|
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
@@ -96,12 +86,12 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.7"
|
val tachideskVersion = "v0.3.8"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = Runtime
|
val tachideskRevision = Runtime
|
||||||
.getRuntime()
|
.getRuntime()
|
||||||
.exec("git rev-list master --count")
|
.exec("git rev-list HEAD --count")
|
||||||
.let { process ->
|
.let { process ->
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
val output = process.inputStream.use {
|
val output = process.inputStream.use {
|
||||||
@@ -171,10 +161,10 @@ tasks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withType<LintTask> {
|
withType<LintTask> {
|
||||||
source(files("src"))
|
source(files("src/kotlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
withType<FormatTask> {
|
withType<FormatTask> {
|
||||||
source(files("src"))
|
source(files("src/kotlin"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
|
|
||||||
object ExtensionGithubApi {
|
|
||||||
const val BASE_URL = "https://raw.githubusercontent.com"
|
|
||||||
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
|
||||||
|
|
||||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
|
||||||
return json
|
|
||||||
.filter { element ->
|
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
|
||||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
|
||||||
}
|
|
||||||
.map { element ->
|
|
||||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
|
||||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
|
||||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
|
||||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
|
||||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
|
||||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
|
||||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
|
||||||
|
|
||||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findExtensions(): List<Extension.Available> {
|
|
||||||
val service: ExtensionGithubService = ExtensionGithubService.create()
|
|
||||||
|
|
||||||
val response = service.getRepo()
|
|
||||||
return parseResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
|
||||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to get the extension repo listing from GitHub.
|
|
||||||
*/
|
|
||||||
interface ExtensionGithubService {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val client by lazy {
|
|
||||||
val network: NetworkHelper by injectLazy()
|
|
||||||
network.client.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalResponse = chain.proceed(chain.request())
|
|
||||||
originalResponse.newBuilder()
|
|
||||||
.header("Content-Encoding", "gzip")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalSerializationApi
|
|
||||||
fun create(): ExtensionGithubService {
|
|
||||||
val adapter = Retrofit.Builder()
|
|
||||||
.baseUrl(ExtensionGithubApi.BASE_URL)
|
|
||||||
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return adapter.create(ExtensionGithubService::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
|
|
||||||
suspend fun getRepo(): JsonArray
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
|
|
||||||
sealed class Extension {
|
|
||||||
|
|
||||||
abstract val name: String
|
|
||||||
abstract val pkgName: String
|
|
||||||
abstract val versionName: String
|
|
||||||
abstract val versionCode: Int
|
|
||||||
abstract val lang: String?
|
|
||||||
abstract val isNsfw: Boolean
|
|
||||||
|
|
||||||
data class Installed(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
override val lang: String,
|
|
||||||
override val isNsfw: Boolean,
|
|
||||||
val sources: List<Source>,
|
|
||||||
val hasUpdate: Boolean = false,
|
|
||||||
val isObsolete: Boolean = false,
|
|
||||||
val isUnofficial: Boolean = false
|
|
||||||
) : Extension()
|
|
||||||
|
|
||||||
data class Available(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
override val lang: String,
|
|
||||||
override val isNsfw: Boolean,
|
|
||||||
val apkName: String,
|
|
||||||
val iconUrl: String
|
|
||||||
) : Extension()
|
|
||||||
|
|
||||||
data class Untrusted(
|
|
||||||
override val name: String,
|
|
||||||
override val pkgName: String,
|
|
||||||
override val versionName: String,
|
|
||||||
override val versionCode: Int,
|
|
||||||
val signatureHash: String,
|
|
||||||
override val lang: String? = null,
|
|
||||||
override val isNsfw: Boolean = false
|
|
||||||
) : Extension()
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
enum class InstallStep {
|
|
||||||
Pending, Downloading, Installing, Installed, Error;
|
|
||||||
|
|
||||||
fun isCompleted(): Boolean {
|
|
||||||
return this == Installed || this == Error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.model
|
|
||||||
|
|
||||||
sealed class LoadResult {
|
|
||||||
|
|
||||||
class Success(val extension: Extension.Installed) : LoadResult()
|
|
||||||
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
|
|
||||||
class Error(val message: String? = null) : LoadResult() {
|
|
||||||
constructor(exception: Throwable) : this(exception.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
// import android.annotation.SuppressLint
|
|
||||||
// import android.content.Context
|
|
||||||
// import android.content.pm.PackageInfo
|
|
||||||
// import android.content.pm.PackageManager
|
|
||||||
// import dalvik.system.PathClassLoader
|
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
|
||||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
// import eu.kanade.tachiyomi.util.lang.Hash
|
|
||||||
// import kotlinx.coroutines.async
|
|
||||||
// import kotlinx.coroutines.runBlocking
|
|
||||||
// import timber.log.Timber
|
|
||||||
// import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
|
||||||
*/
|
|
||||||
// @SuppressLint("PackageManagerGetSignatures")
|
|
||||||
internal object ExtensionLoader {
|
|
||||||
|
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
// private val allowNsfwSource by lazy {
|
|
||||||
// preferences.allowNsfwSource().get()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private const val EXTENSION_FEATURE = "tachiyomi.extension"
|
|
||||||
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
|
|
||||||
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
|
||||||
const val LIB_VERSION_MIN = 1.2
|
|
||||||
const val LIB_VERSION_MAX = 1.2
|
|
||||||
|
|
||||||
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
|
|
||||||
|
|
||||||
// inorichi's key
|
|
||||||
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
|
|
||||||
/**
|
|
||||||
* List of the trusted signatures.
|
|
||||||
*/
|
|
||||||
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list of all the installed extensions initialized concurrently.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
*/
|
|
||||||
// fun loadExtensions(context: Context): List<LoadResult> {
|
|
||||||
// val pkgManager = context.packageManager
|
|
||||||
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
|
|
||||||
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
|
|
||||||
//
|
|
||||||
// if (extPkgs.isEmpty()) return emptyList()
|
|
||||||
//
|
|
||||||
// // Load each extension concurrently and wait for completion
|
|
||||||
// return runBlocking {
|
|
||||||
// val deferred = extPkgs.map {
|
|
||||||
// async { loadExtension(context, it.packageName, it) }
|
|
||||||
// }
|
|
||||||
// deferred.map { it.await() }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to load an extension from the given package name. It checks if the extension
|
|
||||||
* contains the required feature flag before trying to load it.
|
|
||||||
*/
|
|
||||||
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
|
|
||||||
// val pkgInfo = try {
|
|
||||||
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
|
|
||||||
// } catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// // Unlikely, but the package may have been uninstalled at this point
|
|
||||||
// return LoadResult.Error(error)
|
|
||||||
// }
|
|
||||||
// if (!isPackageAnExtension(pkgInfo)) {
|
|
||||||
// return LoadResult.Error("Tried to load a package that wasn't a extension")
|
|
||||||
// }
|
|
||||||
// return loadExtension(context, pkgName, pkgInfo)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an extension given its package name.
|
|
||||||
*
|
|
||||||
* @param context The application context.
|
|
||||||
* @param pkgName The package name of the extension to load.
|
|
||||||
* @param pkgInfo The package info of the extension.
|
|
||||||
*/
|
|
||||||
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
|
|
||||||
// val pkgManager = context.packageManager
|
|
||||||
//
|
|
||||||
// val appInfo = try {
|
|
||||||
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
|
||||||
// } catch (error: PackageManager.NameNotFoundException) {
|
|
||||||
// // Unlikely, but the package may have been uninstalled at this point
|
|
||||||
// return LoadResult.Error(error)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
|
|
||||||
// val versionName = pkgInfo.versionName
|
|
||||||
// val versionCode = pkgInfo.versionCode
|
|
||||||
//
|
|
||||||
// if (versionName.isNullOrEmpty()) {
|
|
||||||
// val exception = Exception("Missing versionName for extension $extName")
|
|
||||||
// Timber.w(exception)
|
|
||||||
// return LoadResult.Error(exception)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Validate lib version
|
|
||||||
// val libVersion = versionName.substringBeforeLast('.').toDouble()
|
|
||||||
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
|
|
||||||
// val exception = Exception(
|
|
||||||
// "Lib version is $libVersion, while only versions " +
|
|
||||||
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
|
|
||||||
// )
|
|
||||||
// Timber.w(exception)
|
|
||||||
// return LoadResult.Error(exception)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val signatureHash = getSignatureHash(pkgInfo)
|
|
||||||
//
|
|
||||||
// if (signatureHash == null) {
|
|
||||||
// return LoadResult.Error("Package $pkgName isn't signed")
|
|
||||||
// } else if (signatureHash !in trustedSignatures) {
|
|
||||||
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
|
|
||||||
// Timber.w("Extension $pkgName isn't trusted")
|
|
||||||
// return LoadResult.Untrusted(extension)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
|
|
||||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
|
|
||||||
// return LoadResult.Error("NSFW extension $pkgName not allowed")
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
|
||||||
//
|
|
||||||
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
|
|
||||||
// .split(";")
|
|
||||||
// .map {
|
|
||||||
// val sourceClass = it.trim()
|
|
||||||
// if (sourceClass.startsWith(".")) {
|
|
||||||
// pkgInfo.packageName + sourceClass
|
|
||||||
// } else {
|
|
||||||
// sourceClass
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .flatMap {
|
|
||||||
// try {
|
|
||||||
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
|
|
||||||
// is Source -> listOf(obj)
|
|
||||||
// is SourceFactory -> {
|
|
||||||
// if (isSourceNsfw(obj)) {
|
|
||||||
// emptyList()
|
|
||||||
// } else {
|
|
||||||
// obj.createSources()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
|
|
||||||
// }
|
|
||||||
// } catch (e: Throwable) {
|
|
||||||
// Timber.e(e, "Extension load error: $extName.")
|
|
||||||
// return LoadResult.Error(e)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .filter { !isSourceNsfw(it) }
|
|
||||||
//
|
|
||||||
// val langs = sources.filterIsInstance<CatalogueSource>()
|
|
||||||
// .map { it.lang }
|
|
||||||
// .toSet()
|
|
||||||
// val lang = when (langs.size) {
|
|
||||||
// 0 -> ""
|
|
||||||
// 1 -> langs.first()
|
|
||||||
// else -> "all"
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val extension = Extension.Installed(
|
|
||||||
// extName,
|
|
||||||
// pkgName,
|
|
||||||
// versionName,
|
|
||||||
// versionCode,
|
|
||||||
// lang,
|
|
||||||
// isNsfw,
|
|
||||||
// sources,
|
|
||||||
// isUnofficial = signatureHash != officialSignature
|
|
||||||
// )
|
|
||||||
// return LoadResult.Success(extension)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given package is an extension.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
|
|
||||||
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the signature hash of the package or null if it's not signed.
|
|
||||||
*
|
|
||||||
* @param pkgInfo The package info of the application.
|
|
||||||
*/
|
|
||||||
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
|
|
||||||
// val signatures = pkgInfo.signatures
|
|
||||||
// return if (signatures != null && signatures.isNotEmpty()) {
|
|
||||||
// Hash.sha256(signatures.first().toByteArray())
|
|
||||||
// } else {
|
|
||||||
// null
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
|
|
||||||
*/
|
|
||||||
// private fun isSourceNsfw(clazz: Any): Boolean {
|
|
||||||
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (clazz !is Source && clazz !is SourceFactory) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Annotations are proxied, hence this janky way of checking for them
|
|
||||||
// return clazz.javaClass.annotations
|
|
||||||
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
|
|
||||||
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
@@ -35,16 +35,16 @@ object Category {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
if (name != null) it[CategoryTable.name] = name
|
if (name != null) it[CategoryTable.name] = name
|
||||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the category from position `from` to `to`
|
* Move the category from position `from` to `to`
|
||||||
*/
|
*/
|
||||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.table.PageTable
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
|
|||||||
@@ -9,25 +9,37 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.table.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
object Library {
|
object Library {
|
||||||
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
|
||||||
// ..implement that shit at some time...
|
// ..implement that shit at some time...
|
||||||
// ..also Consider to rename it to `isDefault`
|
// ..also Consider to rename it to `isDefault`
|
||||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
if (!manga.inLibrary) {
|
if (!manga.inLibrary) {
|
||||||
transaction {
|
transaction {
|
||||||
|
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
it[inLibrary] = true
|
it[MangaTable.inLibrary] = true
|
||||||
|
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCategories.forEach { category ->
|
||||||
|
CategoryMangaTable.insert {
|
||||||
|
it[CategoryMangaTable.category] = category[CategoryTable.id].value
|
||||||
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
import ir.armor.tachidesk.impl.Source.getSource
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
|
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.await
|
import ir.armor.tachidesk.impl.util.await
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
|
||||||
|
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.table.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
|
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
|
import ir.armor.tachidesk.impl.util.storage.SafePath
|
||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.table.PageTable
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
@@ -28,7 +28,7 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Page {
|
object Page {
|
||||||
/**
|
/**
|
||||||
* A page might have a imageUrl ready from the get go, or we might need to
|
* A page might have a imageUrl ready from the get go, or we might need to
|
||||||
* go an extra step and call fetchImageUrl to get it.
|
* go an extra step and call fetchImageUrl to get it.
|
||||||
*/
|
*/
|
||||||
@@ -68,33 +68,28 @@ object Page {
|
|||||||
|
|
||||||
val saveDir = getChapterDir(mangaId, chapterId)
|
val saveDir = getChapterDir(mangaId, chapterId)
|
||||||
File(saveDir).mkdirs()
|
File(saveDir).mkdirs()
|
||||||
val fileName = index.toString()
|
val fileName = String.format("%03d", index) // e.g. 001.jpeg
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
source.fetchImage(tachiPage).awaitSingle()
|
source.fetchImage(tachiPage).awaitSingle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rewrite this to match tachiyomi
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
private fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val source = getHttpSource(sourceId)
|
|
||||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() }
|
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||||
|
|
||||||
val chapterDir = when {
|
val sourceDir = source.toString()
|
||||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||||
else -> chapterEntry[ChapterTable.name]
|
val chapterDir = SafePath.buildValidFilename(
|
||||||
}
|
when {
|
||||||
|
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
|
else -> chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val mangaTitle = mangaEntry[MangaTable.title]
|
return "${applicationDirs.mangaRoot}/$sourceDir/$mangaDir/$chapterDir"
|
||||||
val sourceName = source.toString()
|
|
||||||
|
|
||||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
|
||||||
// make sure dirs exist
|
|
||||||
File(mangaDir).mkdirs()
|
|
||||||
return mangaDir
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import ir.armor.tachidesk.impl.MangaList.processEntries
|
import ir.armor.tachidesk.impl.MangaList.processEntries
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
|
|
||||||
object Search {
|
object Search {
|
||||||
// TODO
|
// TODO
|
||||||
fun sourceFilters(sourceId: Long) {
|
fun sourceFilters(sourceId: Long) {
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
// source.getFilterList().toItems()
|
// source.getFilterList().toItems()
|
||||||
@@ -34,7 +34,7 @@ object Search {
|
|||||||
val filter: Any
|
val filter: Any
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
|
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
|
||||||
*/
|
*/
|
||||||
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
|||||||
import ir.armor.tachidesk.impl.backup.models.Track
|
import ir.armor.tachidesk.impl.backup.models.Track
|
||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
@@ -64,11 +64,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
logger.info {
|
logger.info {
|
||||||
"""
|
"""
|
||||||
Restore Errors:
|
Restore Errors:
|
||||||
${
|
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
|
||||||
errors.map {
|
|
||||||
"${it.first} - ${it.second}"
|
|
||||||
}.joinToString("\n")
|
|
||||||
}
|
|
||||||
Restore Summary:
|
Restore Summary:
|
||||||
- Missing Sources:
|
- Missing Sources:
|
||||||
${validationResult.missingSources.joinToString("\n")}
|
${validationResult.missingSources.joinToString("\n")}
|
||||||
@@ -119,6 +115,8 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
getHttpSource(manga.source)
|
getHttpSource(manga.source)
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: NullPointerException) {
|
||||||
null
|
null
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
null
|
||||||
}
|
}
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package ir.armor.tachidesk.impl.download
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
data class Download(
|
||||||
|
val chapter: ResultRow,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val downloadQueue = LinkedBlockingQueue<Download>()
|
||||||
|
|
||||||
|
class Downloader {
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package ir.armor.tachidesk.impl.extension
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,14 +8,13 @@ package ir.armor.tachidesk.impl
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
|
import ir.armor.tachidesk.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
|
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
|
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
|
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
|
||||||
@@ -27,6 +26,7 @@ import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
|
|||||||
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
|
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
|
||||||
import ir.armor.tachidesk.impl.util.await
|
import ir.armor.tachidesk.impl.util.await
|
||||||
|
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
+6
-6
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl
|
package ir.armor.tachidesk.impl.extension
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -7,9 +7,9 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
import ir.armor.tachidesk.impl.extension.github.OnlineExtension
|
||||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@@ -25,7 +25,7 @@ object ExtensionsList {
|
|||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
var lastUpdateCheck: Long = 0
|
var lastUpdateCheck: Long = 0
|
||||||
var updateMap = ConcurrentHashMap<String, Extension.Available>()
|
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||||
|
|
||||||
/** 60,000 milliseconds = 60 seconds */
|
/** 60,000 milliseconds = 60 seconds */
|
||||||
private const val ExtensionUpdateDelayTime = 60 * 1000
|
private const val ExtensionUpdateDelayTime = 60 * 1000
|
||||||
@@ -63,7 +63,7 @@ object ExtensionsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
|
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||||
transaction {
|
transaction {
|
||||||
foundExtensions.forEach { foundExtension ->
|
foundExtensions.forEach { foundExtension ->
|
||||||
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
package ir.armor.tachidesk.impl.extension.github
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Interceptor.Chain
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.http.RealResponseBody
|
||||||
|
import okio.GzipSource
|
||||||
|
import okio.buffer
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
object ExtensionGithubApi {
|
||||||
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
|
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
||||||
|
|
||||||
|
private const val LIB_VERSION_MIN = "1.2"
|
||||||
|
private const val LIB_VERSION_MAX = "1.2"
|
||||||
|
|
||||||
|
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
|
||||||
|
return json
|
||||||
|
.map { it.asJsonObject }
|
||||||
|
.filter { element ->
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val libVersion = versionName.substringBeforeLast('.')
|
||||||
|
libVersion == LIB_VERSION_MAX
|
||||||
|
}
|
||||||
|
.map { element ->
|
||||||
|
val name = element["name"].string.substringAfter("Tachiyomi: ")
|
||||||
|
val pkgName = element["pkg"].string
|
||||||
|
val apkName = element["apk"].string
|
||||||
|
val versionName = element["version"].string
|
||||||
|
val versionCode = element["code"].int
|
||||||
|
val lang = element["lang"].string
|
||||||
|
val nsfw = element["nsfw"].int == 1
|
||||||
|
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||||
|
|
||||||
|
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findExtensions(): List<OnlineExtension> {
|
||||||
|
val response = getRepo()
|
||||||
|
return parseResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||||
|
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val client by lazy {
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
network.client.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalResponse = chain.proceed(chain.request())
|
||||||
|
originalResponse.newBuilder()
|
||||||
|
.header("Content-Encoding", "gzip")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.addInterceptor(UnzippingInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getRepo(): com.google.gson.JsonArray {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url("$REPO_URL_PREFIX/index.json.gz")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
|
||||||
|
return JsonParser.parseString(response).asJsonArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ref: https://stackoverflow.com/questions/51901333/okhttp-3-how-to-decompress-gzip-deflate-response-manually-using-java-android
|
||||||
|
private class UnzippingInterceptor : Interceptor {
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun intercept(chain: Chain): Response {
|
||||||
|
val response: Response = chain.proceed(chain.request())
|
||||||
|
return unzip(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun unzip(response: Response): Response {
|
||||||
|
if (response.body == null) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have gzip response
|
||||||
|
val contentEncoding: String? = response.headers["Content-Encoding"]
|
||||||
|
|
||||||
|
// this is used to decompress gzipped responses
|
||||||
|
return if (contentEncoding != null && contentEncoding == "gzip") {
|
||||||
|
val body = response.body!!
|
||||||
|
val contentLength: Long = body.contentLength()
|
||||||
|
val responseBody = GzipSource(body.source())
|
||||||
|
val strippedHeaders: Headers = response.headers.newBuilder().build()
|
||||||
|
response.newBuilder().headers(strippedHeaders)
|
||||||
|
.body(RealResponseBody(body.contentType().toString(), contentLength, responseBody.buffer()))
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ir.armor.tachidesk.impl.extension.github
|
||||||
|
|
||||||
|
data class OnlineExtension(
|
||||||
|
val name: String,
|
||||||
|
val pkgName: String,
|
||||||
|
val versionName: String,
|
||||||
|
val versionCode: Int,
|
||||||
|
val lang: String,
|
||||||
|
val isNsfw: Boolean,
|
||||||
|
val apkName: String,
|
||||||
|
val iconUrl: String
|
||||||
|
)
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package ir.armor.tachidesk.impl.util
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import java.net.URLEncoder
|
|
||||||
|
|
||||||
// TODO: finish MangaDex support
|
|
||||||
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
|
||||||
|
|
||||||
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
|
||||||
|
|
||||||
private fun clientBuilder(
|
|
||||||
r18Toggle: Int,
|
|
||||||
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
|
||||||
): OkHttpClient = okHttpClient.newBuilder()
|
|
||||||
.addNetworkInterceptor { chain ->
|
|
||||||
val originalCookies = chain.request().header("Cookie") ?: ""
|
|
||||||
val newReq = chain
|
|
||||||
.request()
|
|
||||||
.newBuilder()
|
|
||||||
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
|
||||||
.build()
|
|
||||||
chain.proceed(newReq)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
private fun cookiesHeader(r18Toggle: Int): String {
|
|
||||||
val cookies = mutableMapOf<String, String>()
|
|
||||||
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
|
||||||
return buildCookies(cookies)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildCookies(cookies: Map<String, String>) =
|
|
||||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
|
||||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun isLogged(): Boolean {
|
|
||||||
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
|
||||||
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
|
||||||
val formBody = FormBody.Builder()
|
|
||||||
.add("login_username", username)
|
|
||||||
.add("login_password", password)
|
|
||||||
.add("no_js", "1")
|
|
||||||
.add("remember_me", "1")
|
|
||||||
|
|
||||||
twoFactorCode.let {
|
|
||||||
formBody.add("two_factor", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = clientBuilder().newCall(
|
|
||||||
POST(
|
|
||||||
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
|
||||||
mangaDexSource.headers,
|
|
||||||
formBody.build()
|
|
||||||
)
|
|
||||||
).execute()
|
|
||||||
return response.body!!.string().isEmpty()
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// fun logout(): Boolean {
|
|
||||||
// return withContext(Dispatchers.IO) {
|
|
||||||
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
|
||||||
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
|
||||||
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
|
||||||
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
|
||||||
// val token = cookie?.value
|
|
||||||
// if (token.isNullOrEmpty()) {
|
|
||||||
// return@withContext true
|
|
||||||
// }
|
|
||||||
// val result = clientBuilder().newCall(
|
|
||||||
// POSTWithCookie(
|
|
||||||
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
|
||||||
// REMEMBER_ME,
|
|
||||||
// token,
|
|
||||||
// headers
|
|
||||||
// )
|
|
||||||
// ).execute()
|
|
||||||
// val resultStr = result.body!!.string()
|
|
||||||
// if (resultStr.contains("success", true)) {
|
|
||||||
// network.cookieManager.remove(httpUrl)
|
|
||||||
// return@withContext true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.util
|
package ir.armor.tachidesk.impl.util.lang
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+13
-16
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.impl.util
|
package ir.armor.tachidesk.impl.util.storage
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -8,13 +8,9 @@ package ir.armor.tachidesk.impl.util
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
object CachedImageResponse {
|
object CachedImageResponse {
|
||||||
private fun pathToInputStream(path: String): InputStream {
|
private fun pathToInputStream(path: String): InputStream {
|
||||||
@@ -45,18 +41,19 @@ object CachedImageResponse {
|
|||||||
val response = fetcher()
|
val response = fetcher()
|
||||||
|
|
||||||
if (response.code == 200) {
|
if (response.code == 200) {
|
||||||
val contentType = response.headers["content-type"]!!
|
val fullPath = "$filePath.tmp"
|
||||||
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
val saveFile = File(fullPath)
|
||||||
|
response.body!!.source().saveTo(saveFile)
|
||||||
|
|
||||||
Files.newOutputStream(Paths.get(fullPath)).use { output ->
|
// find image type
|
||||||
response.body!!.source().use { input ->
|
val imageType = response.headers["content-type"]
|
||||||
output.sink().buffer().use {
|
?: ImageUtil.findImageType { saveFile.inputStream() }?.mime
|
||||||
it.writeAll(input)
|
?: "image/jpeg"
|
||||||
it.flush()
|
.substringAfter("image/")
|
||||||
}
|
|
||||||
}
|
saveFile.renameTo(File("$filePath.$imageType"))
|
||||||
}
|
|
||||||
return pathToInputStream(fullPath) to contentType
|
return pathToInputStream(fullPath) to imageType
|
||||||
} else {
|
} else {
|
||||||
throw Exception("request error! ${response.code}")
|
throw Exception("request error! ${response.code}")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package ir.armor.tachidesk.impl.util.storage
|
||||||
|
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
|
||||||
|
object ImageUtil {
|
||||||
|
|
||||||
|
fun findImageType(openStream: () -> InputStream): ImageType? {
|
||||||
|
return openStream().use { findImageType(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findImageType(stream: InputStream): ImageType? {
|
||||||
|
try {
|
||||||
|
val bytes = ByteArray(8)
|
||||||
|
|
||||||
|
val length = if (stream.markSupported()) {
|
||||||
|
stream.mark(bytes.size)
|
||||||
|
stream.read(bytes, 0, bytes.size).also { stream.reset() }
|
||||||
|
} else {
|
||||||
|
stream.read(bytes, 0, bytes.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) {
|
||||||
|
return ImageType.JPG
|
||||||
|
}
|
||||||
|
if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) {
|
||||||
|
return ImageType.PNG
|
||||||
|
}
|
||||||
|
if (bytes.compareWith("GIF8".toByteArray())) {
|
||||||
|
return ImageType.GIF
|
||||||
|
}
|
||||||
|
if (bytes.compareWith("RIFF".toByteArray())) {
|
||||||
|
return ImageType.WEBP
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
|
||||||
|
return magic.indices.none { this[it] != magic[it] }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
|
||||||
|
return ByteArray(bytes.size).apply {
|
||||||
|
for (i in bytes.indices) {
|
||||||
|
set(i, bytes[i].toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ImageType(val mime: String) {
|
||||||
|
JPG("image/jpeg"),
|
||||||
|
PNG("image/png"),
|
||||||
|
GIF("image/gif"),
|
||||||
|
WEBP("image/webp")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ir.armor.tachidesk.impl.util.storage
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt
|
||||||
|
/**
|
||||||
|
* Saves the given source to a file and closes it. Directories will be created if needed.
|
||||||
|
*
|
||||||
|
* @param file the file where the source is copied.
|
||||||
|
*/
|
||||||
|
fun BufferedSource.saveTo(file: File) {
|
||||||
|
try {
|
||||||
|
// Create parent dirs if needed
|
||||||
|
file.parentFile.mkdirs()
|
||||||
|
|
||||||
|
// Copy to destination
|
||||||
|
saveTo(file.outputStream())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
close()
|
||||||
|
file.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given source to an output stream and closes both resources.
|
||||||
|
*
|
||||||
|
* @param stream the stream where the source is copied.
|
||||||
|
*/
|
||||||
|
fun BufferedSource.saveTo(stream: OutputStream) {
|
||||||
|
use { input ->
|
||||||
|
stream.sink().buffer().use {
|
||||||
|
it.writeAll(input)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package ir.armor.tachidesk.impl.util.storage
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
|
||||||
|
object SafePath {
|
||||||
|
/**
|
||||||
|
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||||
|
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
|
||||||
|
* with a dot), but you can manually add it later.
|
||||||
|
*/
|
||||||
|
fun buildValidFilename(origName: String): String {
|
||||||
|
val name = origName.trim('.', ' ')
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
return "(invalid)"
|
||||||
|
}
|
||||||
|
val sb = StringBuilder(name.length)
|
||||||
|
name.forEach { c ->
|
||||||
|
if (isValidFatFilenameChar(c)) {
|
||||||
|
sb.append(c)
|
||||||
|
} else {
|
||||||
|
sb.append('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||||
|
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
|
||||||
|
return sb.toString().take(240)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given character is a valid filename character, false otherwise.
|
||||||
|
*/
|
||||||
|
private fun isValidFatFilenameChar(c: Char): Boolean {
|
||||||
|
if (0x00.toChar() <= c && c <= 0x1f.toChar()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return when (c) {
|
||||||
|
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7f.toChar() -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0003_DefaultCategory : Migration() {
|
||||||
|
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CATEGORY ALTER COLUMN IS_LANDING RENAME TO IS_DEFAULT")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-5
@@ -10,7 +10,7 @@ package ir.armor.tachidesk.model.database.migration.lib
|
|||||||
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
|
||||||
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
|
||||||
|
|
||||||
import com.google.common.reflect.ClassPath
|
import ir.armor.tachidesk.server.ServerConfig
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
@@ -18,8 +18,15 @@ import org.jetbrains.exposed.sql.SchemaUtils.create
|
|||||||
import org.jetbrains.exposed.sql.exists
|
import org.jetbrains.exposed.sql.exists
|
||||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import java.nio.file.FileSystems
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant.now
|
import java.time.Instant.now
|
||||||
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.io.path.name
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@@ -54,13 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
logger.info { "Migrations finished successfully" }
|
logger.info { "Migrations finished successfully" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPathApi::class)
|
||||||
|
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
|
||||||
|
ServerConfig::class.java.getResource("/" + "ir.armor.tachidesk.model.database.migration".replace('.', '/'))
|
||||||
|
val path = "/" + packageName.replace('.', '/')
|
||||||
|
val uri = ServerConfig::class.java.getResource(path).toURI()
|
||||||
|
|
||||||
|
return when (uri.scheme) {
|
||||||
|
"jar" -> {
|
||||||
|
val fileSystem = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
|
||||||
|
fileSystem.getPath(path)
|
||||||
|
}
|
||||||
|
else -> Paths.get(uri)
|
||||||
|
}.let { Files.walk(it, 1) }
|
||||||
|
.toList()
|
||||||
|
.filterNot { it.isDirectory() || it.name.contains('$') } // '$' means it's not a top level class
|
||||||
|
.filter { it.name.endsWith(".class") }
|
||||||
|
.map { Class.forName("$packageName.${it.name.substringBefore(".class")}") }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("UnstableApiUsage")
|
@Suppress("UnstableApiUsage")
|
||||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
fun loadMigrationsFrom(packageName: String): List<Migration> {
|
||||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
return getTopLevelClasses(packageName)
|
||||||
.getTopLevelClasses(classPath)
|
|
||||||
.map {
|
.map {
|
||||||
logger.debug("found Migration class ${it.name}")
|
logger.debug("found Migration class ${it.name}")
|
||||||
val clazz = it.load().getDeclaredConstructor().newInstance()
|
val clazz = it.getDeclaredConstructor().newInstance()
|
||||||
if (clazz is Migration)
|
if (clazz is Migration)
|
||||||
clazz
|
clazz
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import org.jetbrains.exposed.sql.ResultRow
|
|||||||
|
|
||||||
object CategoryTable : IntIdTable() {
|
object CategoryTable : IntIdTable() {
|
||||||
val name = varchar("name", 64)
|
val name = varchar("name", 64)
|
||||||
val isLanding = bool("is_landing").default(false)
|
|
||||||
val order = integer("order").default(0)
|
val order = integer("order").default(0)
|
||||||
|
val isDefault = bool("is_default").default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
categoryEntry[this.id].value,
|
categoryEntry[this.id].value,
|
||||||
categoryEntry[this.order],
|
categoryEntry[this.order],
|
||||||
categoryEntry[this.name],
|
categoryEntry[this.name],
|
||||||
categoryEntry[this.isLanding],
|
categoryEntry[this.isDefault],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ data class CategoryDataClass(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val order: Int,
|
val order: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val isLanding: Boolean
|
val default: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,14 +13,8 @@ import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
|||||||
import ir.armor.tachidesk.impl.Chapter.getChapter
|
import ir.armor.tachidesk.impl.Chapter.getChapter
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
||||||
import ir.armor.tachidesk.impl.Chapter.modifyChapter
|
import ir.armor.tachidesk.impl.Chapter.modifyChapter
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIcon
|
import ir.armor.tachidesk.impl.Library
|
||||||
import ir.armor.tachidesk.impl.Extension.installExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.updateExtension
|
|
||||||
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
|
|
||||||
import ir.armor.tachidesk.impl.Library.addMangaToLibrary
|
|
||||||
import ir.armor.tachidesk.impl.Library.getLibraryMangas
|
import ir.armor.tachidesk.impl.Library.getLibraryMangas
|
||||||
import ir.armor.tachidesk.impl.Library.removeMangaFromLibrary
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
|
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
|
||||||
import ir.armor.tachidesk.impl.MangaList.getMangaList
|
import ir.armor.tachidesk.impl.MangaList.getMangaList
|
||||||
@@ -33,7 +27,12 @@ import ir.armor.tachidesk.impl.Source.getSourceList
|
|||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
import ir.armor.tachidesk.impl.backup.BackupFlags
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
||||||
import ir.armor.tachidesk.server.internal.About.getAbout
|
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIcon
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.installExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
|
||||||
|
import ir.armor.tachidesk.server.impl_internal.About.getAbout
|
||||||
import ir.armor.tachidesk.server.util.openInBrowser
|
import ir.armor.tachidesk.server.util.openInBrowser
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -216,24 +215,6 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// adds the manga to library
|
|
||||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
|
||||||
|
|
||||||
ctx.result(
|
|
||||||
future { addMangaToLibrary(mangaId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// removes the manga from the library
|
|
||||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
|
||||||
|
|
||||||
ctx.result(
|
|
||||||
future { removeMangaFromLibrary(mangaId) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// list manga's categories
|
// list manga's categories
|
||||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
@@ -302,6 +283,16 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// submit a chapter for download
|
||||||
|
app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel a chapter download
|
||||||
|
app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
// global search, Not implemented yet
|
// global search, Not implemented yet
|
||||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
val searchTerm = ctx.pathParam("searchTerm")
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
@@ -322,6 +313,24 @@ object JavalinSetup {
|
|||||||
ctx.json(sourceFilters(sourceId))
|
ctx.json(sourceFilters(sourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adds the manga to library
|
||||||
|
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { Library.addMangaToLibrary(mangaId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the library
|
||||||
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
ctx.result(
|
||||||
|
future { Library.removeMangaFromLibrary(mangaId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// lists mangas that have no category assigned
|
// lists mangas that have no category assigned
|
||||||
app.get("/api/v1/library/") { ctx ->
|
app.get("/api/v1/library/") { ctx ->
|
||||||
ctx.json(getLibraryMangas())
|
ctx.json(getLibraryMangas())
|
||||||
@@ -348,8 +357,8 @@ object JavalinSetup {
|
|||||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
val name = ctx.formParam("name")
|
val name = ctx.formParam("name")
|
||||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||||
updateCategory(categoryId, name, isLanding)
|
updateCategory(categoryId, name, isDefault)
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,5 +441,19 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Download queue stats
|
||||||
|
app.ws("/api/v1/downloads") { ws ->
|
||||||
|
ws.onConnect { ctx ->
|
||||||
|
// TODO: send current stat
|
||||||
|
// TODO: add to downlad subscribers
|
||||||
|
}
|
||||||
|
ws.onMessage {
|
||||||
|
// TODO: send current stat
|
||||||
|
}
|
||||||
|
ws.onClose { ctx ->
|
||||||
|
// TODO: remove from subscribers
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ package ir.armor.tachidesk.server
|
|||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import io.github.config4k.getValue
|
import io.github.config4k.getValue
|
||||||
import xyz.nulldev.ts.config.ConfigModule
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
|
import xyz.nulldev.ts.config.debugLogsEnabled
|
||||||
|
|
||||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||||
val ip: String by config
|
val ip: String by config
|
||||||
@@ -21,7 +23,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
|||||||
val socksProxyPort: String by config
|
val socksProxyPort: String by config
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
val debugLogsEnabled: Boolean = System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("debugLogsEnabled")).toBoolean()
|
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
||||||
val systemTrayEnabled: Boolean by config
|
val systemTrayEnabled: Boolean by config
|
||||||
val initialOpenInBrowserEnabled: Boolean by config
|
val initialOpenInBrowserEnabled: Boolean by config
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ package ir.armor.tachidesk.server
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ch.qos.logback.classic.Level
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.model.database.databaseUp
|
import ir.armor.tachidesk.model.database.databaseUp
|
||||||
|
import ir.armor.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.systemTray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.bind
|
import org.kodein.di.bind
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.singleton
|
import org.kodein.di.singleton
|
||||||
import org.slf4j.Logger
|
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
import xyz.nulldev.ts.config.ApplicationRootDir
|
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||||
@@ -36,7 +35,7 @@ class ApplicationDirs(
|
|||||||
|
|
||||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||||
|
|
||||||
val systemTray by lazy { systemTray() }
|
val systemTrayInstance by lazy { systemTray() }
|
||||||
|
|
||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
@@ -66,6 +65,9 @@ fun applicationSetup() {
|
|||||||
ServerConfig.register(GlobalConfigManager.config)
|
ServerConfig.register(GlobalConfigManager.config)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Make sure only one instance of the app is running
|
||||||
|
handleAppMutex()
|
||||||
|
|
||||||
// Load config API
|
// Load config API
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
// Load Android compatibility dependencies
|
// Load Android compatibility dependencies
|
||||||
@@ -73,11 +75,6 @@ fun applicationSetup() {
|
|||||||
// start app
|
// start app
|
||||||
androidCompat.startApp(App())
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
// set application wide logging level
|
|
||||||
if (serverConfig.debugLogsEnabled) {
|
|
||||||
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
|
||||||
}
|
|
||||||
|
|
||||||
// create conf file if doesn't exist
|
// create conf file if doesn't exist
|
||||||
try {
|
try {
|
||||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||||
@@ -97,7 +94,7 @@ fun applicationSetup() {
|
|||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled) {
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTrayInstance
|
||||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.server.internal
|
package ir.armor.tachidesk.server.impl_internal
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ir.armor.tachidesk.server.util
|
||||||
|
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
enum class ExitCode(val code: Int) {
|
||||||
|
Success(0),
|
||||||
|
MutexCheckFailedTachideskRunning(1),
|
||||||
|
MutexCheckFailedAnotherAppRunning(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shutdownApp(exitCode: ExitCode) {
|
||||||
|
logger.info("Shutting Down Tachidesk. Goodbye!")
|
||||||
|
|
||||||
|
exitProcess(exitCode.code)
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package ir.armor.tachidesk.server.util
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import io.javalin.plugin.json.JavalinJackson
|
||||||
|
import ir.armor.tachidesk.server.impl_internal.AboutDataClass
|
||||||
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
|
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.Clear
|
||||||
|
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
|
||||||
|
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request.Builder
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object AppMutex {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
private enum class AppMutexStat(val stat: Int) {
|
||||||
|
Clear(0),
|
||||||
|
TachideskInstanceRunning(1),
|
||||||
|
OtherApplicationRunning(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
|
|
||||||
|
private fun checkAppMutex(): AppMutexStat {
|
||||||
|
val client = OkHttpClient.Builder()
|
||||||
|
.connectTimeout(200, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = Builder()
|
||||||
|
.url("http://$appIP:${serverConfig.port}/api/v1/about/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = try {
|
||||||
|
client.newCall(request).execute().use { response -> response.body!!.string() }
|
||||||
|
} catch (e: IOException) {
|
||||||
|
return AppMutexStat.Clear
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
JavalinJackson.fromJson(response, AboutDataClass::class.java)
|
||||||
|
AppMutexStat.TachideskInstanceRunning
|
||||||
|
} catch (e: IOException) {
|
||||||
|
AppMutexStat.OtherApplicationRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleAppMutex() {
|
||||||
|
when (checkAppMutex()) {
|
||||||
|
Clear -> {
|
||||||
|
logger.info("Mutex status is clear, Resuming startup.")
|
||||||
|
}
|
||||||
|
TachideskInstanceRunning -> {
|
||||||
|
logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}")
|
||||||
|
|
||||||
|
logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.")
|
||||||
|
openInBrowser()
|
||||||
|
|
||||||
|
logger.info("Aborting startup.")
|
||||||
|
|
||||||
|
shutdownApp(ExitCode.MutexCheckFailedTachideskRunning)
|
||||||
|
}
|
||||||
|
OtherApplicationRunning -> {
|
||||||
|
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
|
||||||
|
shutdownApp(ExitCode.MutexCheckFailedAnotherAppRunning)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,17 +9,17 @@ package ir.armor.tachidesk.server.util
|
|||||||
|
|
||||||
import dorkbox.systemTray.MenuItem
|
import dorkbox.systemTray.MenuItem
|
||||||
import dorkbox.systemTray.SystemTray
|
import dorkbox.systemTray.SystemTray
|
||||||
import dorkbox.systemTray.SystemTray.TrayType
|
|
||||||
import dorkbox.util.CacheUtil
|
import dorkbox.util.CacheUtil
|
||||||
import dorkbox.util.Desktop
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.server.BuildConfig
|
import ir.armor.tachidesk.server.BuildConfig
|
||||||
import ir.armor.tachidesk.server.ServerConfig
|
import ir.armor.tachidesk.server.ServerConfig
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
import kotlin.system.exitProcess
|
import ir.armor.tachidesk.server.util.ExitCode.Success
|
||||||
|
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
|
val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||||
try {
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://$appIP:${serverConfig.port}")
|
||||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,6 @@ fun systemTray(): SystemTray? {
|
|||||||
try {
|
try {
|
||||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
|
||||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
|
||||||
|
|
||||||
CacheUtil.clear(BuildConfig.name)
|
CacheUtil.clear(BuildConfig.name)
|
||||||
|
|
||||||
@@ -53,8 +51,7 @@ fun systemTray(): SystemTray? {
|
|||||||
|
|
||||||
mainMenu.add(
|
mainMenu.add(
|
||||||
MenuItem("Quit") {
|
MenuItem("Quit") {
|
||||||
systemTray.shutdown()
|
shutdownApp(Success)
|
||||||
exitProcess(0)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ package ir.armor.tachidesk
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.impl.Extension.installExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
|
||||||
import ir.armor.tachidesk.impl.Extension.updateExtension
|
|
||||||
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
|
|
||||||
import ir.armor.tachidesk.impl.Source.getSourceList
|
import ir.armor.tachidesk.impl.Source.getSourceList
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.installExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
|
||||||
|
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
import ir.armor.tachidesk.server.applicationSetup
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|||||||
+11
-10
@@ -2,19 +2,20 @@ plugins {
|
|||||||
id("com.github.node-gradle.node") version "3.0.1"
|
id("com.github.node-gradle.node") version "3.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val nodeRoot = "${project.projectDir}/react"
|
||||||
node {
|
node {
|
||||||
nodeProjectDir.set(file("${project.projectDir}/react/"))
|
nodeProjectDir.set(file(nodeRoot))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("yarn_build") {
|
tasks {
|
||||||
dependsOn("yarn") // install node_modules
|
register<Copy>("copyBuild") {
|
||||||
}
|
from(file("$nodeRoot/build"))
|
||||||
|
into(file("$rootDir/server/src/main/resources/react"))
|
||||||
|
|
||||||
tasks.register<Copy>("copyBuild") {
|
dependsOn("yarn_build")
|
||||||
from(file("$rootDir/webUI/react/build"))
|
}
|
||||||
into(file("$rootDir/server/src/main/resources/react"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("copyBuild") {
|
named("yarn_build") {
|
||||||
dependsOn("yarn_build")
|
dependsOn("yarn") // install node_modules
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -26,9 +26,6 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
read: {
|
|
||||||
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
|
|
||||||
},
|
|
||||||
bullet: {
|
bullet: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
margin: '0 2px',
|
margin: '0 2px',
|
||||||
@@ -80,16 +77,17 @@ export default function ChapterCard(props: IProps) {
|
|||||||
.then(() => triggerChaptersUpdate());
|
.then(() => triggerChaptersUpdate());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className={`${classes.root} ${chapter.read && classes.read}`}>
|
<CardContent className={classes.root}>
|
||||||
<Link
|
<Link
|
||||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||||
style={{
|
style={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: theme.palette.text.primary,
|
color: chapter.read ? readChapterColor : theme.palette.text.primary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import Drawer from '@material-ui/core/Drawer';
|
|||||||
import List from '@material-ui/core/List';
|
import List from '@material-ui/core/List';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
|
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
|
||||||
|
import ExploreIcon from '@material-ui/icons/Explore';
|
||||||
|
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import InboxIcon from '@material-ui/icons/MoveToInbox';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -47,7 +50,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="Library">
|
<ListItem button key="Library">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<CollectionsBookmarkIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Library" />
|
<ListItemText primary="Library" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -55,7 +58,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="Extensions">
|
<ListItem button key="Extensions">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<ExtensionIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Extensions" />
|
<ListItemText primary="Extensions" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -63,7 +66,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="Sources">
|
<ListItem button key="Sources">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<ExploreIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Sources" />
|
<ListItemText primary="Sources" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -71,7 +74,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="settings">
|
<ListItem button key="settings">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<SettingsIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Settings" />
|
<ListItemText primary="Settings" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function Library() {
|
|||||||
const defaultCategoryTab = {
|
const defaultCategoryTab = {
|
||||||
category: {
|
category: {
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
isLanding: true,
|
default: true,
|
||||||
order: 0,
|
order: 0,
|
||||||
id: -1,
|
id: -1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import List from '@material-ui/core/List';
|
import List from '@material-ui/core/List';
|
||||||
import InboxIcon from '@material-ui/icons/Inbox';
|
import ListAltIcon from '@material-ui/icons/ListAlt';
|
||||||
|
import BackupIcon from '@material-ui/icons/Backup';
|
||||||
import Brightness6Icon from '@material-ui/icons/Brightness6';
|
import Brightness6Icon from '@material-ui/icons/Brightness6';
|
||||||
import DnsIcon from '@material-ui/icons/Dns';
|
import DnsIcon from '@material-ui/icons/Dns';
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
@@ -50,13 +51,13 @@ export default function Settings() {
|
|||||||
<List style={{ padding: 0 }}>
|
<List style={{ padding: 0 }}>
|
||||||
<ListItemLink href="/settings/categories">
|
<ListItemLink href="/settings/categories">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<ListAltIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Categories" />
|
<ListItemText primary="Categories" />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
<ListItemLink href="/settings/backup">
|
<ListItemLink href="/settings/backup">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<InboxIcon />
|
<BackupIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Backup" />
|
<ListItemText primary="Backup" />
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ import TextField from '@material-ui/core/TextField';
|
|||||||
import Dialog from '@material-ui/core/Dialog';
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
|
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||||
import NavbarContext from '../../context/NavbarContext';
|
import NavbarContext from '../../context/NavbarContext';
|
||||||
import client from '../../util/client';
|
import client from '../../util/client';
|
||||||
|
|
||||||
@@ -49,7 +50,8 @@ export default function Categories() {
|
|||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [dialogValue, setDialogValue] = useState('');
|
const [dialogName, setDialogName] = useState('');
|
||||||
|
const [dialogDefault, setDialogDefault] = useState(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||||
@@ -93,7 +95,8 @@ export default function Categories() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetDialog = () => {
|
const resetDialog = () => {
|
||||||
setDialogValue('');
|
setDialogName('');
|
||||||
|
setDialogDefault(false);
|
||||||
setCategoryToEdit(-1);
|
setCategoryToEdit(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -102,6 +105,13 @@ export default function Categories() {
|
|||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEditDialogOpen = (index) => {
|
||||||
|
setDialogName(categories[index].name);
|
||||||
|
setDialogDefault(categories[index].default);
|
||||||
|
setCategoryToEdit(index);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDialogCancel = () => {
|
const handleDialogCancel = () => {
|
||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
};
|
};
|
||||||
@@ -110,7 +120,8 @@ export default function Categories() {
|
|||||||
setDialogOpen(false);
|
setDialogOpen(false);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('name', dialogValue);
|
formData.append('name', dialogName);
|
||||||
|
formData.append('default', dialogDefault);
|
||||||
|
|
||||||
if (categoryToEdit === -1) {
|
if (categoryToEdit === -1) {
|
||||||
client.post('/api/v1/category/', formData)
|
client.post('/api/v1/category/', formData)
|
||||||
@@ -161,8 +172,7 @@ export default function Categories() {
|
|||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDialogOpen();
|
handleEditDialogOpen(index);
|
||||||
setCategoryToEdit(index);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
@@ -197,12 +207,9 @@ export default function Categories() {
|
|||||||
</Fab>
|
</Fab>
|
||||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
<DialogTitle id="form-dialog-title">
|
<DialogTitle id="form-dialog-title">
|
||||||
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
|
{categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>
|
|
||||||
Enter new category name.
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
margin="dense"
|
margin="dense"
|
||||||
@@ -210,8 +217,18 @@ export default function Categories() {
|
|||||||
label="Category Name"
|
label="Category Name"
|
||||||
type="text"
|
type="text"
|
||||||
fullWidth
|
fullWidth
|
||||||
value={dialogValue}
|
value={dialogName}
|
||||||
onChange={(e) => setDialogValue(e.target.value)}
|
onChange={(e) => setDialogName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={(
|
||||||
|
<Checkbox
|
||||||
|
checked={dialogDefault}
|
||||||
|
onChange={(e) => setDialogDefault(e.target.checked)}
|
||||||
|
color="default"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
label="Default category when adding new manga to library"
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
Vendored
+1
-1
@@ -80,7 +80,7 @@ interface ICategory {
|
|||||||
id: number
|
id: number
|
||||||
order: number
|
order: number
|
||||||
name: String
|
name: String
|
||||||
isLanding: boolean
|
default: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INavbarOverride {
|
interface INavbarOverride {
|
||||||
|
|||||||
Reference in New Issue
Block a user