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
|
||||
*.jar binary
|
||||
|
||||
# Images
|
||||
# Binary files types
|
||||
*.webp binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
@@ -18,7 +18,7 @@ git config --global user.name "github-actions[bot]"
|
||||
git status
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git add .
|
||||
git commit -m "Update preview repository"
|
||||
git commit -m "Updated to $latest"
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build pull request
|
||||
needs: check_wrapper
|
||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
build:
|
||||
name: Build FatJar
|
||||
name: Build artifacts and release
|
||||
needs: check_wrapper
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -28,10 +28,10 @@ jobs:
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Checkout master branch
|
||||
- name: Checkout ${{ github.ref }}
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ github.ref }}
|
||||
path: master
|
||||
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
|
||||
* 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.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
@@ -41,21 +42,34 @@ open class ConfigManager {
|
||||
*/
|
||||
fun loadConfigs(): Config {
|
||||
//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 baseConfig =
|
||||
ConfigFactory.parseMap(
|
||||
mapOf(
|
||||
"ts.server.rootDir" to ApplicationRootDir
|
||||
)
|
||||
)
|
||||
|
||||
//Load user config
|
||||
val userConfig =
|
||||
File(ApplicationRootDir, "server.conf").let {
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
File(ApplicationRootDir, "server.conf").let {
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
|
||||
|
||||
val config = ConfigFactory.empty()
|
||||
.withFallback(baseConfig)
|
||||
.withFallback(userConfig)
|
||||
.withFallback(compatConfig)
|
||||
.withFallback(serverConfig)
|
||||
.resolve()
|
||||
|
||||
// set log level early
|
||||
if (debugLogsEnabled(config)) {
|
||||
setLogLevel(Level.DEBUG)
|
||||
}
|
||||
|
||||
logger.debug {
|
||||
"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 |
|
||||
|-------|----------|---------|---------|
|
||||
|  | [](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
|
||||
<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("io.github.microutils:kotlin-logging:2.0.6")
|
||||
|
||||
// RxJava
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("io.reactivex:rxkotlin:1.0.0")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
|
||||
// JSoup
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.1")
|
||||
implementation("io.github.config4k:config4k:0.4.2")
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
# 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`
|
||||
|
||||
+26
-36
@@ -12,48 +12,27 @@ plugins {
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 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")
|
||||
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
|
||||
val okhttpVersion = "4.10.0-RC1"
|
||||
// okhttp
|
||||
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
|
||||
|
||||
// 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
|
||||
// Javalin api
|
||||
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
|
||||
val exposedVersion = "0.31.1"
|
||||
@@ -61,7 +40,6 @@ dependencies {
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||
|
||||
// current database driver
|
||||
implementation("com.h2database:h2:1.4.200")
|
||||
|
||||
@@ -69,7 +47,19 @@ dependencies {
|
||||
implementation("com.dorkbox:SystemTray:4.1")
|
||||
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
|
||||
implementation(project(":AndroidCompat"))
|
||||
@@ -96,12 +86,12 @@ sourceSets {
|
||||
}
|
||||
|
||||
// should be bumped with each stable release
|
||||
val tachideskVersion = "v0.3.7"
|
||||
val tachideskVersion = "v0.3.8"
|
||||
|
||||
// counts commit count on master
|
||||
val tachideskRevision = Runtime
|
||||
.getRuntime()
|
||||
.exec("git rev-list master --count")
|
||||
.exec("git rev-list HEAD --count")
|
||||
.let { process ->
|
||||
process.waitFor()
|
||||
val output = process.inputStream.use {
|
||||
@@ -171,10 +161,10 @@ tasks {
|
||||
}
|
||||
|
||||
withType<LintTask> {
|
||||
source(files("src"))
|
||||
source(files("src/kotlin"))
|
||||
}
|
||||
|
||||
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 {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
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`
|
||||
*/
|
||||
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 ir.armor.tachidesk.impl.Manga.getManga
|
||||
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.MangaTable
|
||||
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.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.toDataClass
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
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...
|
||||
// ..also Consider to rename it to `isDefault`
|
||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
transaction {
|
||||
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
|
||||
|
||||
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 ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||
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.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.MangaTable
|
||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||
|
||||
@@ -9,7 +9,7 @@ package ir.armor.tachidesk.impl
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
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.MangaTable
|
||||
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.online.HttpSource
|
||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||
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.MangaTable
|
||||
import ir.armor.tachidesk.model.database.table.PageTable
|
||||
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||
import ir.armor.tachidesk.server.ApplicationDirs
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
@@ -28,7 +28,7 @@ import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Page {
|
||||
/**
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -68,33 +68,28 @@ object Page {
|
||||
|
||||
val saveDir = getChapterDir(mangaId, chapterId)
|
||||
File(saveDir).mkdirs()
|
||||
val fileName = index.toString()
|
||||
val fileName = String.format("%03d", index) // e.g. 001.jpeg
|
||||
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
source.fetchImage(tachiPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rewrite this to match tachiyomi
|
||||
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 sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||
|
||||
val chapterDir = when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
val sourceDir = source.toString()
|
||||
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||
val chapterDir = SafePath.buildValidFilename(
|
||||
when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
)
|
||||
|
||||
val mangaTitle = mangaEntry[MangaTable.title]
|
||||
val sourceName = source.toString()
|
||||
|
||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||
// make sure dirs exist
|
||||
File(mangaDir).mkdirs()
|
||||
return mangaDir
|
||||
return "${applicationDirs.mangaRoot}/$sourceDir/$mangaDir/$chapterDir"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ package ir.armor.tachidesk.impl
|
||||
|
||||
import ir.armor.tachidesk.impl.MangaList.processEntries
|
||||
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
|
||||
|
||||
object Search {
|
||||
// TODO
|
||||
// TODO
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
@@ -34,7 +34,7 @@ object Search {
|
||||
val filter: Any
|
||||
)
|
||||
|
||||
/**
|
||||
/**
|
||||
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
|
||||
*/
|
||||
// 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
|
||||
* 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.model.database.table.ExtensionTable
|
||||
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.TrackImpl
|
||||
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 mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.and
|
||||
@@ -64,11 +64,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
||||
logger.info {
|
||||
"""
|
||||
Restore Errors:
|
||||
${
|
||||
errors.map {
|
||||
"${it.first} - ${it.second}"
|
||||
}.joinToString("\n")
|
||||
}
|
||||
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
|
||||
Restore Summary:
|
||||
- Missing Sources:
|
||||
${validationResult.missingSources.joinToString("\n")}
|
||||
@@ -119,6 +115,8 @@ object LegacyBackupImport : LegacyBackupBase() {
|
||||
getHttpSource(manga.source)
|
||||
} catch (e: NullPointerException) {
|
||||
null
|
||||
} catch (e: NoSuchElementException) {
|
||||
null
|
||||
}
|
||||
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
|
||||
@@ -8,14 +8,13 @@ package ir.armor.tachidesk.impl
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
|
||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||
import ir.armor.tachidesk.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
|
||||
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_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.trustedSignatures
|
||||
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.SourceTable
|
||||
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
|
||||
@@ -7,9 +7,9 @@ package ir.armor.tachidesk.impl
|
||||
* 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.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
||||
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
|
||||
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
|
||||
import ir.armor.tachidesk.impl.extension.github.OnlineExtension
|
||||
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||
import mu.KotlinLogging
|
||||
@@ -25,7 +25,7 @@ object ExtensionsList {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
var lastUpdateCheck: Long = 0
|
||||
var updateMap = ConcurrentHashMap<String, Extension.Available>()
|
||||
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||
|
||||
/** 60,000 milliseconds = 60 seconds */
|
||||
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 {
|
||||
foundExtensions.forEach { foundExtension ->
|
||||
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
|
||||
+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
|
||||
@@ -8,13 +8,9 @@ package ir.armor.tachidesk.impl.util
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import okhttp3.Response
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
object CachedImageResponse {
|
||||
private fun pathToInputStream(path: String): InputStream {
|
||||
@@ -45,18 +41,19 @@ object CachedImageResponse {
|
||||
val response = fetcher()
|
||||
|
||||
if (response.code == 200) {
|
||||
val contentType = response.headers["content-type"]!!
|
||||
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||
val fullPath = "$filePath.tmp"
|
||||
val saveFile = File(fullPath)
|
||||
response.body!!.source().saveTo(saveFile)
|
||||
|
||||
Files.newOutputStream(Paths.get(fullPath)).use { output ->
|
||||
response.body!!.source().use { input ->
|
||||
output.sink().buffer().use {
|
||||
it.writeAll(input)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
return pathToInputStream(fullPath) to contentType
|
||||
// find image type
|
||||
val imageType = response.headers["content-type"]
|
||||
?: ImageUtil.findImageType { saveFile.inputStream() }?.mime
|
||||
?: "image/jpeg"
|
||||
.substringAfter("image/")
|
||||
|
||||
saveFile.renameTo(File("$filePath.$imageType"))
|
||||
|
||||
return pathToInputStream(fullPath) to imageType
|
||||
} else {
|
||||
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.
|
||||
// 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 org.jetbrains.exposed.dao.id.EntityID
|
||||
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.transactions.TransactionManager
|
||||
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.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 {}
|
||||
|
||||
@@ -54,13 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
||||
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")
|
||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
||||
.getTopLevelClasses(classPath)
|
||||
fun loadMigrationsFrom(packageName: String): List<Migration> {
|
||||
return getTopLevelClasses(packageName)
|
||||
.map {
|
||||
logger.debug("found Migration class ${it.name}")
|
||||
val clazz = it.load().getDeclaredConstructor().newInstance()
|
||||
val clazz = it.getDeclaredConstructor().newInstance()
|
||||
if (clazz is Migration)
|
||||
clazz
|
||||
else
|
||||
|
||||
@@ -13,13 +13,13 @@ import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
object CategoryTable : IntIdTable() {
|
||||
val name = varchar("name", 64)
|
||||
val isLanding = bool("is_landing").default(false)
|
||||
val order = integer("order").default(0)
|
||||
val isDefault = bool("is_default").default(false)
|
||||
}
|
||||
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||
categoryEntry[this.id].value,
|
||||
categoryEntry[this.order],
|
||||
categoryEntry[this.name],
|
||||
categoryEntry[this.isLanding],
|
||||
categoryEntry[this.isDefault],
|
||||
)
|
||||
|
||||
@@ -11,5 +11,5 @@ data class CategoryDataClass(
|
||||
val id: Int,
|
||||
val order: Int,
|
||||
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.getChapterList
|
||||
import ir.armor.tachidesk.impl.Chapter.modifyChapter
|
||||
import ir.armor.tachidesk.impl.Extension.getExtensionIcon
|
||||
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
|
||||
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.getMangaThumbnail
|
||||
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.legacy.LegacyBackupExport.createLegacyBackup
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
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
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
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
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
@@ -322,6 +313,24 @@ object JavalinSetup {
|
||||
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
|
||||
app.get("/api/v1/library/") { ctx ->
|
||||
ctx.json(getLibraryMangas())
|
||||
@@ -348,8 +357,8 @@ object JavalinSetup {
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||
updateCategory(categoryId, name, isLanding)
|
||||
val isDefault = ctx.formParam("default")?.toBoolean()
|
||||
updateCategory(categoryId, name, isDefault)
|
||||
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 io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import xyz.nulldev.ts.config.debugLogsEnabled
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip: String by config
|
||||
@@ -21,7 +23,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val socksProxyPort: String by config
|
||||
|
||||
// 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 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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import eu.kanade.tachiyomi.App
|
||||
import ir.armor.tachidesk.model.database.databaseUp
|
||||
import ir.armor.tachidesk.server.util.AppMutex.handleAppMutex
|
||||
import ir.armor.tachidesk.server.util.systemTray
|
||||
import mu.KotlinLogging
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.bind
|
||||
import org.kodein.di.conf.global
|
||||
import org.kodein.di.singleton
|
||||
import org.slf4j.Logger
|
||||
import xyz.nulldev.androidcompat.AndroidCompat
|
||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||
import xyz.nulldev.ts.config.ApplicationRootDir
|
||||
@@ -36,7 +35,7 @@ class ApplicationDirs(
|
||||
|
||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||
|
||||
val systemTray by lazy { systemTray() }
|
||||
val systemTrayInstance by lazy { systemTray() }
|
||||
|
||||
val androidCompat by lazy { AndroidCompat() }
|
||||
|
||||
@@ -66,6 +65,9 @@ fun applicationSetup() {
|
||||
ServerConfig.register(GlobalConfigManager.config)
|
||||
)
|
||||
|
||||
// Make sure only one instance of the app is running
|
||||
handleAppMutex()
|
||||
|
||||
// Load config API
|
||||
DI.global.addImport(ConfigKodeinModule().create())
|
||||
// Load Android compatibility dependencies
|
||||
@@ -73,11 +75,6 @@ fun applicationSetup() {
|
||||
// start 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
|
||||
try {
|
||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||
@@ -97,7 +94,7 @@ fun applicationSetup() {
|
||||
// create system tray
|
||||
if (serverConfig.systemTrayEnabled) {
|
||||
try {
|
||||
systemTray
|
||||
systemTrayInstance
|
||||
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||
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
|
||||
@@ -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.SystemTray
|
||||
import dorkbox.systemTray.SystemTray.TrayType
|
||||
import dorkbox.util.CacheUtil
|
||||
import dorkbox.util.Desktop
|
||||
import ir.armor.tachidesk.server.BuildConfig
|
||||
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() {
|
||||
val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
|
||||
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
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -29,8 +29,6 @@ fun systemTray(): SystemTray? {
|
||||
try {
|
||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||
if (System.getProperty("os.name").startsWith("Windows"))
|
||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||
|
||||
CacheUtil.clear(BuildConfig.name)
|
||||
|
||||
@@ -53,8 +51,7 @@ fun systemTray(): SystemTray? {
|
||||
|
||||
mainMenu.add(
|
||||
MenuItem("Quit") {
|
||||
systemTray.shutdown()
|
||||
exitProcess(0)
|
||||
shutdownApp(Success)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ package ir.armor.tachidesk
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
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.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.awaitSingle
|
||||
import ir.armor.tachidesk.impl.util.lang.awaitSingle
|
||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||
import ir.armor.tachidesk.server.applicationSetup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
+12
-11
@@ -2,19 +2,20 @@ plugins {
|
||||
id("com.github.node-gradle.node") version "3.0.1"
|
||||
}
|
||||
|
||||
val nodeRoot = "${project.projectDir}/react"
|
||||
node {
|
||||
nodeProjectDir.set(file("${project.projectDir}/react/"))
|
||||
nodeProjectDir.set(file(nodeRoot))
|
||||
}
|
||||
|
||||
tasks.named("yarn_build") {
|
||||
dependsOn("yarn") // install node_modules
|
||||
}
|
||||
tasks {
|
||||
register<Copy>("copyBuild") {
|
||||
from(file("$nodeRoot/build"))
|
||||
into(file("$rootDir/server/src/main/resources/react"))
|
||||
|
||||
tasks.register<Copy>("copyBuild") {
|
||||
from(file("$rootDir/webUI/react/build"))
|
||||
into(file("$rootDir/server/src/main/resources/react"))
|
||||
}
|
||||
dependsOn("yarn_build")
|
||||
}
|
||||
|
||||
tasks.named("copyBuild") {
|
||||
dependsOn("yarn_build")
|
||||
}
|
||||
named("yarn_build") {
|
||||
dependsOn("yarn") // install node_modules
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
read: {
|
||||
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
|
||||
},
|
||||
bullet: {
|
||||
display: 'inline-block',
|
||||
margin: '0 2px',
|
||||
@@ -80,16 +77,17 @@ export default function ChapterCard(props: IProps) {
|
||||
.then(() => triggerChaptersUpdate());
|
||||
};
|
||||
|
||||
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<Card>
|
||||
<CardContent className={`${classes.root} ${chapter.read && classes.read}`}>
|
||||
<CardContent className={classes.root}>
|
||||
<Link
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: theme.palette.text.primary,
|
||||
color: chapter.read ? readChapterColor : theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex' }}>
|
||||
|
||||
@@ -11,8 +11,11 @@ import Drawer from '@material-ui/core/Drawer';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
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 InboxIcon from '@material-ui/icons/MoveToInbox';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
@@ -47,7 +50,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Library">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<CollectionsBookmarkIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Library" />
|
||||
</ListItem>
|
||||
@@ -55,7 +58,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Extensions">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<ExtensionIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Extensions" />
|
||||
</ListItem>
|
||||
@@ -63,7 +66,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Sources">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<ExploreIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
@@ -71,7 +74,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="settings">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function Library() {
|
||||
const defaultCategoryTab = {
|
||||
category: {
|
||||
name: 'Default',
|
||||
isLanding: true,
|
||||
default: true,
|
||||
order: 0,
|
||||
id: -1,
|
||||
},
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
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 DnsIcon from '@material-ui/icons/Dns';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
@@ -50,13 +51,13 @@ export default function Settings() {
|
||||
<List style={{ padding: 0 }}>
|
||||
<ListItemLink href="/settings/categories">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<ListAltIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Categories" />
|
||||
</ListItemLink>
|
||||
<ListItemLink href="/settings/backup">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
<BackupIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Backup" />
|
||||
</ListItemLink>
|
||||
|
||||
@@ -28,8 +28,9 @@ import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
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 client from '../../util/client';
|
||||
|
||||
@@ -49,7 +50,8 @@ export default function Categories() {
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogValue, setDialogValue] = useState('');
|
||||
const [dialogName, setDialogName] = useState('');
|
||||
const [dialogDefault, setDialogDefault] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
@@ -93,7 +95,8 @@ export default function Categories() {
|
||||
};
|
||||
|
||||
const resetDialog = () => {
|
||||
setDialogValue('');
|
||||
setDialogName('');
|
||||
setDialogDefault(false);
|
||||
setCategoryToEdit(-1);
|
||||
};
|
||||
|
||||
@@ -102,6 +105,13 @@ export default function Categories() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (index) => {
|
||||
setDialogName(categories[index].name);
|
||||
setDialogDefault(categories[index].default);
|
||||
setCategoryToEdit(index);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDialogCancel = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
@@ -110,7 +120,8 @@ export default function Categories() {
|
||||
setDialogOpen(false);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', dialogValue);
|
||||
formData.append('name', dialogName);
|
||||
formData.append('default', dialogDefault);
|
||||
|
||||
if (categoryToEdit === -1) {
|
||||
client.post('/api/v1/category/', formData)
|
||||
@@ -161,8 +172,7 @@ export default function Categories() {
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleDialogOpen();
|
||||
setCategoryToEdit(index);
|
||||
handleEditDialogOpen(index);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
@@ -197,12 +207,9 @@ export default function Categories() {
|
||||
</Fab>
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
|
||||
{categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Enter new category name.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
@@ -210,8 +217,18 @@ export default function Categories() {
|
||||
label="Category Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogValue}
|
||||
onChange={(e) => setDialogValue(e.target.value)}
|
||||
value={dialogName}
|
||||
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>
|
||||
<DialogActions>
|
||||
|
||||
Vendored
+1
-1
@@ -80,7 +80,7 @@ interface ICategory {
|
||||
id: number
|
||||
order: number
|
||||
name: String
|
||||
isLanding: boolean
|
||||
default: boolean
|
||||
}
|
||||
|
||||
interface INavbarOverride {
|
||||
|
||||
Reference in New Issue
Block a user