Compare commits

...

35 Commits

Author SHA1 Message Date
Aria Moradi d1cd2cfc8c [RELEASE CI] bump version 2021-01-29 15:26:08 +03:30
Aria Moradi 832c224ed4 uninstalling extensions implemented 2021-01-29 15:23:29 +03:30
Aria Moradi 99316f4bd5 revert react changes 2021-01-29 14:25:18 +03:30
Aria Moradi 9caae5f1e5 thumbnail caching 2021-01-29 14:19:24 +03:30
Aria Moradi 345be95ce9 [RELEASE CI] bump version 2021-01-28 15:13:56 +03:30
Aria Moradi 6fe68841b7 [SKIP CI] add docker thanks to @arbuilder 2021-01-28 15:08:37 +03:30
Aria Moradi eaff2c15a9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-26 23:33:14 +03:30
Aria Moradi 5eb8dc66a8 add license notice to everything 2021-01-26 23:32:12 +03:30
Aria Moradi 49715c81e4 Update README.md 2021-01-26 23:11:20 +03:30
Aria Moradi 3398409555 Update README.md 2021-01-26 23:07:54 +03:30
Aria Moradi f05aa0589a [SKIP CI] add apache 2 license link 2021-01-26 23:02:04 +03:30
Aria Moradi fbc71ce781 fix nav buttons 2021-01-23 02:57:32 +03:30
Aria Moradi ca9c671886 more css hacks: scroll bar 2021-01-23 02:53:00 +03:30
Aria Moradi bd109ba11f some css hacks 2021-01-23 02:32:18 +03:30
Aria Moradi 0ff770a98b fluid manga grid 2021-01-23 01:58:18 +03:30
Aria Moradi ed7bb408a3 fix light theme AppBar 2021-01-23 01:36:56 +03:30
Aria Moradi 84676b9156 remove some wierdness 2021-01-23 01:33:12 +03:30
Aria Moradi dcdd50ffe1 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-23 01:25:49 +03:30
Aria Moradi afb21c59f0 DarkTheme! my eyes can rest now :) 2021-01-23 01:20:16 +03:30
Aria Moradi e219179519 Update README.md 2021-01-23 00:28:32 +03:30
Aria Moradi 15a2115c5a Update README.md 2021-01-23 00:26:43 +03:30
Aria Moradi 94c6f33925 Update README.md 2021-01-23 00:23:40 +03:30
Aria Moradi 202e38871d [RELEASE CI] hotfix 2021-01-22 21:33:54 +03:30
Aria Moradi 3f75b84651 fix button text 2021-01-22 21:33:12 +03:30
Aria Moradi f171b785a0 [RELEASE CI] fix linting error 2021-01-22 21:29:09 +03:30
Aria Moradi 088dd6a856 [RELEASE CI] Milestone: The application is considered usable. 2021-01-22 21:18:14 +03:30
Aria Moradi 6318628ea2 [RELEASE CI] bump version 2021-01-22 21:15:14 +03:30
Aria Moradi 0757ea5d0d implemented infinite scroll 2021-01-22 21:11:00 +03:30
Aria Moradi 2c76ad9b74 [RELEASE CI] Search implemented, other improvements 2021-01-22 18:14:59 +03:30
Aria Moradi 7d1c63e181 single source search done 2021-01-22 18:07:31 +03:30
Aria Moradi 6401b946b6 add page title 2021-01-22 17:00:33 +03:30
Aria Moradi 9a61f58043 add kotlinter 2021-01-22 12:34:03 +03:30
Aria Moradi b854fdeadb fix matching 2021-01-22 11:50:33 +03:30
Aria Moradi 6eb4f1ba88 Update README.md 2021-01-22 11:25:11 +03:30
Aria Moradi ded5e3a73a Update README.md 2021-01-22 11:13:25 +03:30
66 changed files with 1148 additions and 484 deletions
+3 -4
View File
@@ -1,14 +1,13 @@
#!/bin/bash #!/bin/bash
mkdir -p repo/
# Get last commit message # Get last commit message
last_commit_log=$(git log -1 --pretty=format:"%s") last_commit_log=$(git log -1 --pretty=format:"%s")
echo "last commit log: $last_commit_log" echo "last commit log: $last_commit_log"
filter_count=$(echo "$last_commit_log" | grep -c "[RELEASE CI]" ) filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
echo "count is: $filter_count"
if [ "$filter_count" -gt 0 ]; then if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/ cp server/build/Tachidesk-*.jar repo/
fi fi
+23 -7
View File
@@ -1,14 +1,14 @@
# Tachidesk # Tachidesk
A free and open source manga reader than runs extensions built for [Tachiyomi](https://tachiyomi.org/) which runs on desktop operating systems. A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## How do I run the thing? ## How do I run the thing?
#### Prerequisites
You should have java 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
#### Running pre-built jar packages #### Running pre-built jar packages
Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases). Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
@@ -16,6 +16,9 @@ Double click on the jar file or run `java -jar Tachidesk-latest.jar` or `java -j
The server will be running on `http://localhost:4567` open this url in your browser. The server will be running on `http://localhost:4567` open this url in your browser.
#### Running on Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## Building from source ## Building from source
### Get Android stubs jar ### Get Android stubs jar
#### Manual download #### Manual download
@@ -35,13 +38,26 @@ How to do it is described in `webUI/react/README.md` but for short,
and supports HMR and all the other goodies you'll need. and supports HMR and all the other goodies you'll need.
## Is this application usable? Should I test it? ## Is this application usable? Should I test it?
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented. If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Credit ## Credit
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`. The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`. Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
You can obtain a copy of the license from http://www.apache.org/licenses/LICENSE-2.0
## License ## License
Copyright (C) 2020-2021 Aria Moradi and contributors Copyright (C) 2020-2021 Aria Moradi and contributors
+9 -1
View File
@@ -5,9 +5,10 @@ plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21" // id("org.jetbrains.kotlin.jvm") version "1.4.21"
application application
id("com.github.johnrengelman.shadow") version "6.1.0" id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
} }
val TachideskVersion = "v0.0.2" val TachideskVersion = "v0.1.3"
repositories { repositories {
@@ -139,9 +140,16 @@ tasks {
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build") destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
} }
tasks.named("processResources") { tasks.named("processResources") {
dependsOn(":webUI:copyBuild") dependsOn(":webUI:copyBuild")
} }
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk; package ir.armor.tachidesk;
/* 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 org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
//import android.content.res.Configuration // import android.content.res.Configuration
//import android.support.multidex.MultiDex // import android.support.multidex.MultiDex
//import timber.log.Timber // import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
@@ -2,19 +2,22 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import com.google.gson.Gson import com.google.gson.Gson
//import eu.kanade.tachiyomi.data.cache.ChapterCache // import eu.kanade.tachiyomi.data.cache.ChapterCache
//import eu.kanade.tachiyomi.data.cache.CoverCache // import eu.kanade.tachiyomi.data.cache.CoverCache
//import eu.kanade.tachiyomi.data.database.DatabaseHelper // import eu.kanade.tachiyomi.data.database.DatabaseHelper
//import eu.kanade.tachiyomi.data.download.DownloadManager // import eu.kanade.tachiyomi.data.download.DownloadManager
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
//import eu.kanade.tachiyomi.data.sync.LibrarySyncManager // import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
//import eu.kanade.tachiyomi.data.track.TrackManager // import eu.kanade.tachiyomi.data.track.TrackManager
//import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.* import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
} }
// rxAsync { get<DatabaseHelper>() } // rxAsync { get<DatabaseHelper>() }
} }
private fun rxAsync(block: () -> Unit) { private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe() Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
} }
} }
@@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
//import android.content.Context // import android.content.Context
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
//import kotlinx.coroutines.Dispatchers // import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext // import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
@@ -27,7 +27,7 @@ internal class ExtensionGithubApi {
// suspend fun checkForUpdates(): List<Extension.Installed> { // suspend fun checkForUpdates(): List<Extension.Installed> {
// val extensions = fin dExtensions() // val extensions = fin dExtensions()
// //
//// preferences.lastExtCheck().set(Date().time) // // preferences.lastExtCheck().set(Date().time)
// //
// val installedExtensions = ExtensionLoader.loadExtensions(context) // val installedExtensions = ExtensionLoader.loadExtensions(context)
// .filterIsInstance<LoadResult.Success>() // .filterIsInstance<LoadResult.Success>()
@@ -49,23 +49,23 @@ internal class ExtensionGithubApi {
private fun parseResponse(json: JsonArray): List<Extension.Available> { private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json return json
.filter { element -> .filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble() val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
} }
.map { element -> .map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ") val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1 val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}" val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
@@ -9,7 +9,7 @@ import retrofit2.Retrofit
import retrofit2.http.GET import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
/** /**
* Used to get the extension repo listing from GitHub. * Used to get the extension repo listing from GitHub.
@@ -1,28 +1,22 @@
package eu.kanade.tachiyomi.extension.util package eu.kanade.tachiyomi.extension.util
//import android.annotation.SuppressLint // import android.annotation.SuppressLint
//import android.content.Context // import android.content.Context
//import android.content.pm.PackageInfo // import android.content.pm.PackageInfo
//import android.content.pm.PackageManager // import android.content.pm.PackageManager
//import dalvik.system.PathClassLoader // import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw // import eu.kanade.tachiyomi.data.preference.PreferenceValues
//import eu.kanade.tachiyomi.data.preference.PreferenceValues // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.extension.model.Extension // import kotlinx.coroutines.async
import eu.kanade.tachiyomi.extension.model.LoadResult // import kotlinx.coroutines.runBlocking
import eu.kanade.tachiyomi.source.CatalogueSource // import timber.log.Timber
import eu.kanade.tachiyomi.source.Source // import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.source.SourceFactory
//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. * Class that handles the loading of the extensions installed in the system.
*/ */
//@SuppressLint("PackageManagerGetSignatures") // @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader { internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy() // private val preferences: PreferencesHelper by injectLazy()
@@ -1,30 +1,23 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
//import android.annotation.SuppressLint // import android.annotation.SuppressLint
//import android.content.Context // import android.content.Context
//import android.os.Build // import android.os.Build
//import android.os.Handler // import android.os.Handler
//import android.os.Looper // import android.os.Looper
//import android.webkit.WebSettings // import android.webkit.WebSettings
//import android.webkit.WebView // import android.webkit.WebView
//import android.widget.Toast // import android.widget.Toast
//import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource // import eu.kanade.tachiyomi.util.lang.launchUI
//import eu.kanade.tachiyomi.util.lang.launchUI // import eu.kanade.tachiyomi.util.system.WebViewClientCompat
//import eu.kanade.tachiyomi.util.system.WebViewClientCompat // import eu.kanade.tachiyomi.util.system.WebViewUtil
//import eu.kanade.tachiyomi.util.system.WebViewUtil // import eu.kanade.tachiyomi.util.system.isOutdated
//import eu.kanade.tachiyomi.util.system.isOutdated // import eu.kanade.tachiyomi.util.system.setDefaultSettings
//import eu.kanade.tachiyomi.util.system.setDefaultSettings // import eu.kanade.tachiyomi.util.system.toast
//import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor() : Interceptor { class CloudflareInterceptor() : Interceptor {
@@ -77,7 +70,7 @@ class CloudflareInterceptor() : Interceptor {
// } // }
} }
// //
//// @SuppressLint("SetJavaScriptEnabled") // // @SuppressLint("SetJavaScriptEnabled")
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) { // private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
// // We need to lock this thread until the WebView finds the challenge solution url, because // // We need to lock this thread until the WebView finds the challenge solution url, because
// // OkHttp doesn't support asynchronous interceptors. // // OkHttp doesn't support asynchronous interceptors.
@@ -1,17 +1,14 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
//import android.content.Context // import android.content.Context
//import eu.kanade.tachiyomi.BuildConfig // import eu.kanade.tachiyomi.BuildConfig
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import android.content.Context import android.content.Context
import okhttp3.Cache // import okhttp3.HttpUrl.Companion.toHttpUrl
//import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
//import okhttp3.dnsoverhttps.DnsOverHttps // import okhttp3.dnsoverhttps.DnsOverHttps
//import okhttp3.logging.HttpLoggingInterceptor // import okhttp3.logging.HttpLoggingInterceptor
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
@@ -30,6 +27,8 @@ class NetworkHelper(context: Context) {
// .cache(Cache(cacheDir, cacheSize)) // .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor()) // .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) { // if (BuildConfig.DEBUG) {
@@ -1,18 +1,14 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
//import kotlinx.coroutines.suspendCancellableCoroutine // import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -52,7 +48,7 @@ fun Call.asObservable(): Observable<Response> {
} }
// Based on https://github.com/gildor/kotlin-coroutines-okhttp // Based on https://github.com/gildor/kotlin-coroutines-okhttp
//suspend fun Call.await(assertSuccess: Boolean = false): Response { // suspend fun Call.await(assertSuccess: Boolean = false): Response {
// return suspendCancellableCoroutine { continuation -> // return suspendCancellableCoroutine { continuation ->
// enqueue( // enqueue(
// object : Callback { // object : Callback {
@@ -81,7 +77,7 @@ fun Call.asObservable(): Observable<Response> {
// } // }
// } // }
// } // }
//} // }
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response -> return asObservable().doOnNext { response ->
@@ -92,7 +88,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
} }
} }
//fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { // fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
// val progressClient = newBuilder() // val progressClient = newBuilder()
// .cache(null) // .cache(null)
// .addNetworkInterceptor { chain -> // .addNetworkInterceptor { chain ->
@@ -104,7 +100,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
// .build() // .build()
// //
// return progressClient.newCall(request) // return progressClient.newCall(request)
//} // }
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call { fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder() val progressClient = newBuilder()
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import androidx.preference.PreferenceScreen // import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source { interface ConfigurableSource : Source {
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import android.graphics.drawable.Drawable // import android.graphics.drawable.Drawable
//import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
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 rx.Observable import rx.Observable
//import uy.kohesive.injekt.Injekt // import uy.kohesive.injekt.Injekt
//import uy.kohesive.injekt.api.get // import uy.kohesive.injekt.api.get
/** /**
* A basic interface for creating a source. It could be an online source, a local source, etc... * A basic interface for creating a source. It could be an online source, a local source, etc...
@@ -46,6 +46,6 @@ interface Source {
fun fetchPageList(chapter: SChapter): Observable<List<Page>> fun fetchPageList(chapter: SChapter): Observable<List<Page>>
} }
//fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this) // fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
//fun Source.getPreferenceKey(): String = "source_$id" // fun Source.getPreferenceKey(): String = "source_$id"
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
//import android.content.Context // import android.content.Context
//import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
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
@@ -9,7 +9,7 @@ open class Page(
val url: String = "", val url: String = "",
var imageUrl: String? = null, var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions @Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
): ProgressListener { ) : ProgressListener {
val number: Int val number: Int
get() = index + 1 get() = index + 1
@@ -12,7 +12,7 @@ interface SChapter : Serializable {
var chapter_number: Float var chapter_number: Float
var scanlator: String? var scanlator: String?
fun copyFrom(other: SChapter) { fun copyFrom(other: SChapter) {
name = other.name name = other.name
@@ -16,7 +16,7 @@ import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.net.URI import java.net.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import java.security.MessageDigest import java.security.MessageDigest
@@ -1,8 +1,13 @@
package ir.armor.tachidesk package ir.armor.tachidesk
/* 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 net.harawata.appdirs.AppDirsFactory import net.harawata.appdirs.AppDirsFactory
object Config { object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null) val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
} val thumbnailsRoot = "$dataRoot/thumbnails"
}
@@ -1,8 +1,26 @@
package ir.armor.tachidesk package ir.armor.tachidesk
/* 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.App import eu.kanade.tachiyomi.App
import io.javalin.Javalin import io.javalin.Javalin
import ir.armor.tachidesk.util.* import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getMangaUpdateQueueThread
import ir.armor.tachidesk.util.getPages
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompat
@@ -23,31 +41,34 @@ class Main {
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
// System.getProperties()["proxySet"] = "true"
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
// System.getProperties()["socksProxyPort"] = "2020"
// make sure everything we need exists // make sure everything we need exists
applicationSetup() applicationSetup()
registerConfigModules() registerConfigModules()
//Load config API // Load config API
DI.global.addImport(ConfigKodeinModule().create()) DI.global.addImport(ConfigKodeinModule().create())
//Load Android compatibility dependencies // Load Android compatibility dependencies
AndroidCompatInitializer().init() AndroidCompatInitializer().init()
// start app // start app
androidCompat.startApp(App()) androidCompat.startApp(App())
Thread(getMangaUpdateQueueThread).start()
val app = Javalin.create { config -> val app = Javalin.create { config ->
try { try {
this::class.java.classLoader.getResource("/react/index.html") this::class.java.classLoader.getResource("/react/index.html")
config.addStaticFiles("/react") config.addStaticFiles("/react")
config.addSinglePageRoot("/","/react/index.html") config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
println("Warning: react build files are missing.") println("Warning: react build files are missing.")
} }
}.start(4567) }.start(4567)
app.before() { ctx -> app.before() { ctx ->
// allow the client which is running on another port // allow the client which is running on another port
ctx.header("Access-Control-Allow-Origin", "*") ctx.header("Access-Control-Allow-Origin", "*")
@@ -57,18 +78,31 @@ class Main {
ctx.json(getExtensionList()) ctx.json(getExtensionList())
} }
app.get("/api/v1/extension/install/:apkName") { ctx -> app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
println(apkName) println("installing $apkName")
ctx.status( ctx.status(
installAPK(apkName) installAPK(apkName)
) )
} }
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("uninstalling $apkName")
removeExtension(apkName)
ctx.status(200)
}
app.get("/api/v1/source/list") { ctx -> app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList()) ctx.json(getSourceList())
} }
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
@@ -96,6 +130,15 @@ class Main {
ctx.json(getPages(chapterId, mangaId)) ctx.json(getPages(chapterId, mangaId))
} }
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
println("got request for: $mangaId")
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search // global search
app.get("/api/v1/search/:searchTerm") { ctx -> app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
@@ -103,10 +146,11 @@ class Main {
} }
// single source search // single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm") { ctx -> app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceSearch(sourceId, searchTerm)) val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
} }
// source filter list // source filter list
@@ -114,11 +158,6 @@ class Main {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId)) ctx.json(sourceFilters(sourceId))
} }
} }
} }
} }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database package ir.armor.tachidesk.database
/* 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 ir.armor.tachidesk.Config import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.table.ChapterTable import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionsTable
@@ -25,4 +29,4 @@ fun makeDataBaseTables() {
SchemaUtils.create(MangaTable) SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable) SchemaUtils.create(ChapterTable)
} }
} }
@@ -1,11 +1,15 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* 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 ChapterDataClass( data class ChapterDataClass(
val id: Int, val id: Int,
val url: String, val url: String,
val name: String, val name: String,
val date_upload: Long, val date_upload: Long,
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
) )
@@ -1,14 +1,18 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* 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 ExtensionDataClass( data class ExtensionDataClass(
val name: String, val name: String,
val pkgName: String, val pkgName: String,
val versionName: String, val versionName: String,
val versionCode: Int, val versionCode: Int,
val lang: String, val lang: String,
val isNsfw: Boolean, val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String, val iconUrl: String,
val installed: Boolean, val installed: Boolean,
val classFQName: String, val classFQName: String,
) )
@@ -1,20 +1,29 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* 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 ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
data class MangaDataClass( data class MangaDataClass(
val id: Int, val id: Int,
val sourceId: Long, val sourceId: Long,
val url: String, val url: String,
val title: String, val title: String,
val thumbnail_url: String? = null, val thumbnailUrl: String? = null,
val initialized: Boolean = false, val initialized: Boolean = false,
val artist: String? = null, val artist: String? = null,
val author: String? = null, val author: String? = null,
val description: String? = null, val description: String? = null,
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name val status: String = MangaStatus.UNKNOWN.name
) )
data class PagedMangaListDataClass(
val mangaList: List<MangaDataClass>,
val hasNextPage: Boolean
)
@@ -1,6 +1,10 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* 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 PageDataClass( data class PageDataClass(
val index: Int, val index: Int,
var imageUrl: String, var imageUrl: String,
) )
@@ -1,9 +1,13 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* 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 SourceDataClass( data class SourceDataClass(
val id: String, val id: String,
val name: String, val name: String,
val lang: String, val lang: String,
val iconUrl: String, val iconUrl: String,
val supportsLatest: Boolean val supportsLatest: Boolean
) )
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* 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 ir.armor.tachidesk.database.table.ExtensionsTable import ir.armor.tachidesk.database.table.ExtensionsTable
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
@@ -18,4 +22,4 @@ class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
var iconUrl by ExtensionsTable.iconUrl var iconUrl by ExtensionsTable.iconUrl
var installed by ExtensionsTable.installed var installed by ExtensionsTable.installed
var classFQName by ExtensionsTable.classFQName var classFQName by ExtensionsTable.classFQName
} }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* 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 ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
@@ -20,4 +24,4 @@ class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
var thumbnail_url by MangaTable.thumbnail_url var thumbnail_url by MangaTable.thumbnail_url
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
} }
@@ -1,7 +1,12 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* 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 ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
class SourceEntity(id: EntityID<Long>) : LongEntity(id) { class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -13,4 +18,4 @@ class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
var extension by ExtensionEntity referencedOn SourceTable.extension var extension by ExtensionEntity referencedOn SourceTable.extension
var partOfFactorySource by SourceTable.partOfFactorySource var partOfFactorySource by SourceTable.partOfFactorySource
var positionInFactorySource by SourceTable.positionInFactorySource var positionInFactorySource by SourceTable.positionInFactorySource
} }
@@ -1,6 +1,5 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
import eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
@@ -8,7 +7,7 @@ object ChapterTable : IntIdTable() {
val name = varchar("name", 512) val name = varchar("name", 512)
val date_upload = long("date_upload").default(0) val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f) val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator",128).nullable() val scanlator = varchar("scanlator", 128).nullable()
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
@@ -2,7 +2,6 @@ package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() { object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128) val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128) val pkgName = varchar("pkg_name", 128)
@@ -15,4 +14,4 @@ object ExtensionsTable : IntIdTable() {
val installed = bool("installed").default(false) val installed = bool("installed").default(false)
val classFQName = varchar("class_name", 256).default("") // fully qualified name val classFQName = varchar("class_name", 256).default("") // fully qualified name
} }
@@ -30,4 +30,4 @@ enum class MangaStatus(val status: Int) {
companion object { companion object {
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
} }
} }
@@ -4,9 +4,9 @@ import org.jetbrains.exposed.dao.id.IdTable
object SourceTable : IdTable<Long>() { object SourceTable : IdTable<Long>() {
override val id = long("id").entityId() override val id = long("id").entityId()
val name= varchar("name", 128) val name = varchar("name", 128)
val lang = varchar("lang", 10) val lang = varchar("lang", 10)
val extension = reference("extension", ExtensionsTable) val extension = reference("extension", ExtensionsTable)
val partOfFactorySource = bool("part_of_factory_source").default(false) val partOfFactorySource = bool("part_of_factory_source").default(false)
val positionInFactorySource = integer("position_in_factory_source").nullable() val positionInFactorySource = integer("position_in_factory_source").nullable()
} }
@@ -1,28 +1,30 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.source.model.Page import eu.kanade.tachiyomi.source.model.Page
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.database.dataclass.ChapterDataClass import ir.armor.tachidesk.database.dataclass.ChapterDataClass
import ir.armor.tachidesk.database.dataclass.PageDataClass import ir.armor.tachidesk.database.dataclass.PageDataClass
import ir.armor.tachidesk.database.entity.MangaEntity
import ir.armor.tachidesk.database.table.ChapterTable import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
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
fun getChapterList(mangaId: Int): List<ChapterDataClass> { fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId) val source = getHttpSource(mangaDetails.sourceId)
val chapterList = source.fetchChapterList( val chapterList = source.fetchChapterList(
SManga.create().apply { SManga.create().apply {
title = mangaDetails.title title = mangaDetails.title
url = mangaDetails.url url = mangaDetails.url
} }
).toBlocking().first() ).toBlocking().first()
return transaction { return transaction {
@@ -41,22 +43,21 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
} }
return@transaction chapterList.map { return@transaction chapterList.map {
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
it.name, it.name,
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId mangaId
) )
} }
} }
} }
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> { fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
return transaction { return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
@@ -64,24 +65,35 @@ fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val pagesList = source.fetchPageList( val pagesList = source.fetchPageList(
SChapter.create().apply { SChapter.create().apply {
url = chapterEntry[ChapterTable.url] url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name] name = chapterEntry[ChapterTable.name]
} }
).toBlocking().first() ).toBlocking().first()
return@transaction pagesList.map { val chapter = ChapterDataClass(
chapterEntry[ChapterTable.id].value,
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId
)
val pages = pagesList.map {
PageDataClass( PageDataClass(
it.index, it.index,
getTrueImageUrl(it,source) getTrueImageUrl(it, source)
) )
} }
}
return@transaction Pair(chapter, pages)
}
} }
fun getTrueImageUrl(page: Page, source: HttpSource): String { fun getTrueImageUrl(page: Page, source: HttpSource): String {
return if ( page.imageUrl == null){ return if (page.imageUrl == null) {
source.fetchImageUrl(page).toBlocking().first()!! source.fetchImageUrl(page).toBlocking().first()!!
} else page.imageUrl!! } else page.imageUrl!!
} }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
@@ -67,18 +71,17 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
return transaction { return transaction {
return@transaction ExtensionsTable.selectAll().map { return@transaction ExtensionsTable.selectAll().map {
ExtensionDataClass( ExtensionDataClass(
it[ExtensionsTable.name], it[ExtensionsTable.name],
it[ExtensionsTable.pkgName], it[ExtensionsTable.pkgName],
it[ExtensionsTable.versionName], it[ExtensionsTable.versionName],
it[ExtensionsTable.versionCode], it[ExtensionsTable.versionCode],
it[ExtensionsTable.lang], it[ExtensionsTable.lang],
it[ExtensionsTable.isNsfw], it[ExtensionsTable.isNsfw],
it[ExtensionsTable.apkName], it[ExtensionsTable.apkName],
it[ExtensionsTable.iconUrl], it[ExtensionsTable.iconUrl],
it[ExtensionsTable.installed], it[ExtensionsTable.installed],
it[ExtensionsTable.classFQName] it[ExtensionsTable.classFQName]
) )
} }
} }
} }
@@ -0,0 +1,30 @@
package ir.armor.tachidesk.util
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
fun writeStream(fileStream: InputStream, path: String) {
Files.newOutputStream(Paths.get(path)).use { os ->
val buffer = ByteArray(1024)
var len: Int
while (fileStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
}
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
}
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName))
return "$directoryPath/${file.name}"
}
return null
}
@@ -1,78 +1,171 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
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
import java.io.InputStream
import java.util.concurrent.ArrayBlockingQueue
fun getManga(mangaId: Int): MangaDataClass { val getMangaUpdateQueue = ArrayBlockingQueue<Pair<Int, SManga?>>(1000)
return transaction { @Volatile
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! var getMangaCount = 0
return@transaction if (mangaEntry[MangaTable.initialized]) { val getMangaUpdateQueueThread = Runnable {
MangaDataClass( while (true) {
mangaId, val p = getMangaUpdateQueue.take()
mangaEntry[MangaTable.sourceReference].value, println("took ${p.first}")
while (getMangaCount > 0) {
println("count is $getMangaCount")
Thread.sleep(1000)
}
val mangaId = p.first
println("working on $mangaId")
val fetchedManga = p.second!!
try {
transaction {
println("transaction start $mangaId")
MangaTable.update({ MangaTable.id eq mangaId }) {
mangaEntry[MangaTable.url], it[MangaTable.initialized] = true
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true, it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
mangaEntry[MangaTable.artist], it[MangaTable.description] = fetchedManga.description
mangaEntry[MangaTable.author], it[MangaTable.genre] = fetchedManga.genre
mangaEntry[MangaTable.description], it[MangaTable.status] = fetchedManga.status
mangaEntry[MangaTable.genre], if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
) }
} else { // initialize manga println("transaction end $mangaId")
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
// update database
MangaTable.update({ MangaTable.id eq mangaId }) {
// it[url] = fetchedManga.url
// it[title] = fetchedManga.title
it[initialized] = true
it[artist] = fetchedManga.artist
it[author] = fetchedManga.author
it[description] = fetchedManga.description
it[genre] = fetchedManga.genre
it[status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[thumbnail_url] = fetchedManga.thumbnail_url
} }
} catch (e: Exception) {
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! println(e)
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} }
} }
} }
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
synchronized(getMangaCount) {
getMangaCount++
}
return try {
transaction {
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
return@transaction if (mangaEntry[MangaTable.initialized]) {
println("${mangaEntry[MangaTable.title]} is initialized")
println("${mangaEntry[MangaTable.thumbnail_url]}")
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
// update database
// TODO: sqlite gets fucked here
println("putting $mangaId")
getMangaUpdateQueue.put(Pair(mangaId, fetchedManga))
// mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val newThumbnail =
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) {
fetchedManga.thumbnail_url
} else mangaEntry[MangaTable.thumbnail_url]
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
)
}
}
} finally {
synchronized(getMangaCount) {
getMangaCount--
}
}
}
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
return transaction {
var filePath = Config.thumbnailsRoot + "/$mangaId"
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
if (potentialCache != null) {
println("using cached thumbnail file")
return@transaction Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter("$mangaId.")}"
)
}
val sourceId = mangaEntry[MangaTable.sourceReference].value
println("getting source for $mangaId")
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
println(thumbnailUrl)
val response = source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
println(response.code)
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += "." + contentType.substringAfter("image/")
writeStream(response.body!!.byteStream(), filePath)
return@transaction Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
}
@@ -1,15 +1,23 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
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
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> { fun proxyThumbnailUrl(mangaId: Int): String {
return "http://127.0.0.1:4567/api/v1/manga/$mangaId/thumbnail"
}
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong()) val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first() source.fetchPopularManga(pageNum).toBlocking().first()
@@ -19,11 +27,16 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
else else
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
} }
return transaction { return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga -> return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
var mangaEntityId = if (mangaEntry == null) { // create manga entry if (mangaEntry == null) { // create manga entry
MangaTable.insertAndGetId { val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url it[url] = manga.url
it[title] = manga.title it[title] = manga.title
@@ -32,21 +45,18 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
it[description] = manga.description it[description] = manga.description
it[genre] = manga.genre it[genre] = manga.genre
it[status] = manga.status it[status] = manga.status
it[thumbnail_url] = manga.genre it[thumbnail_url] = manga.thumbnail_url
it[sourceReference] = sourceId it[sourceReference] = sourceId
}.value }.value
} else {
mangaEntry[MangaTable.id].value
}
MangaDataClass( MangaDataClass(
mangaEntityId, mangaId,
sourceId.toLong(), sourceId,
manga.url, manga.url,
manga.title, manga.title,
manga.thumbnail_url, proxyThumbnailUrl(mangaId),
manga.initialized, manga.initialized,
@@ -55,7 +65,30 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
manga.description, manga.description,
manga.genre, manga.genre,
MangaStatus.valueOf(manga.status).name, MangaStatus.valueOf(manga.status).name,
) )
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId,
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
}
} }
} }
} return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
@@ -1,28 +1,31 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Filter /* This Source Code Form is subject to the terms of the Mozilla Public
import eu.kanade.tachiyomi.source.model.FilterList * 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.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
//source.getFilterList().toItems() // source.getFilterList().toItems()
} }
fun sourceSearch(sourceId: Long, searchTerm: String) { fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
//source.fetchSearchManga() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId)
} }
fun sourceGlobalSearch(searchTerm: String) { fun sourceGlobalSearch(searchTerm: String) {
} }
data class FilterWrapper( data class FilterWrapper(
val type: String, val type: String,
val filter: Any val filter: Any
) )
//private fun FilterList.toItems(): List<FilterWrapper> { // private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter -> // return mapNotNull { filter ->
// when (filter) { // when (filter) {
// is Filter.Header -> FilterWrapper("Header",filter) // is Filter.Header -> FilterWrapper("Header",filter)
@@ -56,4 +59,4 @@ data class FilterWrapper(
// } // }
// } // }
// } // }
//} // }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.Config import ir.armor.tachidesk.Config
@@ -13,7 +17,7 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.* import java.util.Locale
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>() private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>() private val extensionCache = mutableListOf<Pair<String, Any>>()
@@ -39,16 +43,16 @@ fun getHttpSource(sourceId: Long): HttpSource {
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath } val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false var usedCached = false
val instance = val instance =
if (cachedExtensionPair != null) { if (cachedExtensionPair != null) {
usedCached = true usedCached = true
println("Used cached Extension") println("Used cached Extension")
cachedExtensionPair.second cachedExtensionPair.second
} else { } else {
println("No Extension cache") println("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader) val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child) val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance() classToLoad.newInstance()
} }
if (sourceRecord.partOfFactorySource) { if (sourceRecord.partOfFactorySource) {
return@transaction if (usedCached) { return@transaction if (usedCached) {
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!] (instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
@@ -71,12 +75,26 @@ fun getSourceList(): List<SourceDataClass> {
return transaction { return transaction {
return@transaction SourceTable.selectAll().map { return@transaction SourceTable.selectAll().map {
SourceDataClass( SourceDataClass(
it[SourceTable.id].value.toString(), it[SourceTable.id].value.toString(),
it[SourceTable.name], it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])), Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl], ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(it[SourceTable.id].value).supportsLatest getHttpSource(it[SourceTable.id].value).supportsLatest
) )
} }
} }
} }
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
return@transaction SourceDataClass(
source[SourceTable.id].value.toString(),
source[SourceTable.name],
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq source[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(source[SourceTable.id].value).supportsLatest
)
}
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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 ir.armor.tachidesk.Config import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.makeDataBaseTables import ir.armor.tachidesk.database.makeDataBaseTables
import java.io.File import java.io.File
@@ -8,7 +12,7 @@ fun applicationSetup() {
// make dirs we need // make dirs we need
File(Config.dataRoot).mkdirs() File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs() File(Config.extensionsRoot).mkdirs()
File(Config.thumbnailsRoot).mkdirs()
makeDataBaseTables() makeDataBaseTables()
} }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
/* 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.googlecode.dex2jar.tools.Dex2jarCmd import com.googlecode.dex2jar.tools.Dex2jarCmd
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
@@ -13,6 +17,7 @@ import kotlinx.coroutines.runBlocking
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert 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
@@ -28,8 +33,8 @@ fun installAPK(apkName: String): Int {
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded // check if we don't have the dex file already downloaded
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(dexPath).exists()) { if (!File(jarPath).exists()) {
runBlocking { runBlocking {
val api = ExtensionGithubApi() val api = ExtensionGithubApi()
val apkToDownload = api.getApkUrl(extensionRecord) val apkToDownload = api.getApkUrl(extensionRecord)
@@ -41,7 +46,6 @@ fun installAPK(apkName: String): Int {
// download apk file // download apk file
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className) println(className)
// dex -> jar // dex -> jar
@@ -60,7 +64,7 @@ fun installAPK(apkName: String): Int {
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id] return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
} }
if (instance is HttpSource) {// single source if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource val httpSource = instance as HttpSource
transaction { transaction {
// SourceEntity.new { // SourceEntity.new {
@@ -80,7 +84,6 @@ fun installAPK(apkName: String): Int {
// println(httpSource.name) // println(httpSource.name)
// println() // println()
} }
} else { // multi source } else { // multi source
val sourceFactory = instance as SourceFactory val sourceFactory = instance as SourceFactory
transaction { transaction {
@@ -110,7 +113,6 @@ fun installAPK(apkName: String): Int {
it[classFQName] = className it[classFQName] = className
} }
} }
} }
return 201 // we downloaded successfully return 201 // we downloaded successfully
} else { } else {
@@ -122,10 +124,28 @@ val networkHelper: NetworkHelper by injectLazy()
private fun downloadAPKFile(url: String, apkPath: String) { private fun downloadAPKFile(url: String, apkPath: String) {
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val response = networkHelper.client.newCall(request).execute() val response = networkHelper.client.newCall(request).execute()
val downloadedFile = File(apkPath) val downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer() val sink = downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source()) sink.writeAll(response.body!!.source())
sink.close() sink.close()
} }
fun removeExtension(pkgName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
val fileNameWithoutType = pkgName.substringBefore(".apk")
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
it[ExtensionsTable.installed] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
+77 -29
View File
@@ -1,7 +1,15 @@
import React from 'react'; /* 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 React, { useState } from 'react';
import { import {
BrowserRouter as Router, Route, Switch, BrowserRouter as Router, Route, Switch,
} from 'react-router-dom'; } from 'react-router-dom';
import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import Home from './screens/Home'; import Home from './screens/Home';
import Sources from './screens/Sources'; import Sources from './screens/Sources';
@@ -9,39 +17,79 @@ import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import MangaList from './screens/MangaList';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/Search'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
import DarkTheme from './context/DarkTheme';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [darkTheme, setDarkTheme] = useState<boolean>(true);
const navTitleContext = { title, setTitle };
const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo(
() => createMuiTheme({
palette: {
type: darkTheme ? 'dark' : 'light',
},
overrides: {
MuiCssBaseline: {
'@global': {
'*::-webkit-scrollbar': {
width: '10px',
background: darkTheme ? '#222' : '#e1e1e1',
},
'*::-webkit-scrollbar-thumb': {
background: darkTheme ? '#111' : '#aaa',
borderRadius: '5px',
},
},
},
},
}),
[darkTheme],
);
return ( return (
<Router> <Router>
<NavBar />
<Switch> <ThemeProvider theme={theme}>
<Route path="/search"> <NavBarTitle.Provider value={navTitleContext}>
<Search /> <CssBaseline />
</Route> <DarkTheme.Provider value={darkThemeContext}>
<Route path="/extensions"> <NavBar />
<Extensions /> </DarkTheme.Provider>
</Route> <Container maxWidth={false} disableGutters>
<Route path="/sources/:sourceId/popular/"> <Switch>
<MangaList popular /> <Route path="/sources/:sourceId/search/">
</Route> <Search />
<Route path="/sources/:sourceId/latest/"> </Route>
<MangaList popular={false} /> <Route path="/extensions">
</Route> <Extensions />
<Route path="/sources"> </Route>
<Sources /> <Route path="/sources/:sourceId/popular/">
</Route> <MangaList popular />
<Route path="/manga/:mangaId/chapter/:chapterId"> </Route>
<Reader /> <Route path="/sources/:sourceId/latest/">
</Route> <MangaList popular={false} />
<Route path="/manga/:id"> </Route>
<Manga /> <Route path="/sources">
</Route> <Sources />
<Route path="/"> </Route>
<Home /> <Route path="/manga/:mangaId/chapter/:chapterId">
</Route> <Reader />
</Switch> </Route>
<Route path="/manga/:id">
<Manga />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Container>
</NavBarTitle.Provider>
</ThemeProvider>
</Router> </Router>
); );
} }
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
+22 -3
View File
@@ -1,3 +1,7 @@
/* 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 React, { useState } from 'react'; import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -42,7 +46,7 @@ export default function ExtensionCard(props: IProps) {
name, lang, versionName, iconUrl, installed, apkName, name, lang, versionName, iconUrl, installed, apkName,
}, },
} = props; } = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install')); const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
const classes = useStyles(); const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase(); const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
@@ -50,10 +54,25 @@ export default function ExtensionCard(props: IProps) {
function install() { function install() {
setInstalledState('installing'); setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => { fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
setInstalledState('installed'); setInstalledState('uninstall');
}); });
} }
function uninstall() {
setInstalledState('uninstalling');
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => {
setInstalledState('install');
});
}
function handleButtonClick() {
if (installedState === 'install') {
install();
} else {
uninstall();
}
}
return ( return (
<Card> <Card>
<CardContent className={classes.root}> <CardContent className={classes.root}>
@@ -76,7 +95,7 @@ export default function ExtensionCard(props: IProps) {
</div> </div>
</div> </div>
<Button variant="outlined" onClick={() => install()}>{installedState}</Button> <Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
</CardContent> </CardContent>
</Card> </Card>
); );
+28 -19
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -5,6 +9,7 @@ import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia'; import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@@ -39,7 +44,7 @@ const useStyles = makeStyles({
interface IProps { interface IProps {
manga: IManga manga: IManga
} }
export default function MangaCard(props: IProps) { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
manga: { manga: {
id, title, thumbnailUrl, id, title, thumbnailUrl,
@@ -48,22 +53,26 @@ export default function MangaCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Link to={`/manga/${id}/`}> <Grid item xs={6} sm={4} md={3} lg={2}>
<Card className={classes.root}> <Link to={`/manga/${id}/`}>
<CardActionArea> <Card className={classes.root} ref={ref}>
<div className={classes.wrapper}> <CardActionArea>
<CardMedia <div className={classes.wrapper}>
className={classes.image} <CardMedia
component="img" className={classes.image}
alt={title} component="img"
image={thumbnailUrl} alt={title}
title={title} image={thumbnailUrl}
/> title={title}
<div className={classes.gradient} /> />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography> <div className={classes.gradient} />
</div> <Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</CardActionArea> </div>
</Card> </CardActionArea>
</Link> </Card>
</Link>
</Grid>
); );
} });
export default MangaCard;
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
interface IProps{ interface IProps{
+44 -11
View File
@@ -1,26 +1,59 @@
import React from 'react'; /* 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 React, { useEffect, useRef } from 'react';
import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard'; import MangaCard from './MangaCard';
interface IProps{ interface IProps{
mangas: IManga[] mangas: IManga[]
message?: string message?: string
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
} }
export default function MangaGrid(props: IProps) { export default function MangaGrid(props: IProps) {
const { mangas, message } = props; const {
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped; let mapped;
const lastManga = useRef<HTMLInputElement>();
const scrollHandler = () => {
if (lastManga.current) {
const rect = lastManga.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
setLastPageNum(lastPageNum + 1);
}
}
};
useEffect(() => {
window.addEventListener('scroll', scrollHandler, true);
return () => {
window.removeEventListener('scroll', scrollHandler, true);
};
}, [hasNextPage, mangas]);
if (mangas.length === 0) { if (mangas.length === 0) {
mapped = <h3>{message !== undefined ? message : 'loading...'}</h3>; mapped = <h3>{message}</h3>;
} else { } else {
mapped = ( mapped = mangas.map((it, idx) => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}> if (idx === mangas.length - 1) {
{mangas.map((it) => ( return <MangaCard manga={it} ref={lastManga} />;
<MangaCard manga={it} /> }
))} return <MangaCard manga={it} />;
</div> });
);
} }
return mapped; return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
{mapped}
</Grid>
);
} }
MangaGrid.defaultProps = {
message: 'loading...',
};
+71 -3
View File
@@ -1,11 +1,21 @@
import React, { useState } from 'react'; /* 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 React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer'; import TemporaryDrawer from './TemporaryDrawer';
import NavBarTitle from '../context/NavbarTitle';
import DarkTheme from '../context/DarkTheme';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -19,13 +29,35 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() { export default function NavBar() {
const classes = useStyles(); const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title } = useContext(NavBarTitle);
const open = Boolean(anchorEl);
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AppBar position="static"> <AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
<Toolbar> <Toolbar>
<IconButton <IconButton
edge="start" edge="start"
@@ -38,8 +70,44 @@ export default function NavBar() {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" className={classes.title}> <Typography variant="h6" className={classes.title}>
Tachidesk {title}
</Typography> </Typography>
<IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<MoreIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
>
Dark Theme
</MenuItem>
<MenuItem
onClick={() => { setDarkTheme(false); handleClose(); }}
>
Light Theme
</MenuItem>
</Menu>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} /> <TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
+7 -2
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
@@ -65,8 +69,9 @@ export default function SourceCard(props: IProps) {
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/latest/`; }}>Latest</Button>} <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/popular/`; }}>Browse</Button> {supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer'; import Drawer from '@material-ui/core/Drawer';
@@ -48,14 +52,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" /> <ListItemText primary="Sources" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}> {/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search"> <ListItem button key="Search">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <InboxIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Global Search" /> <ListItemText primary="Global Search" />
</ListItem> </ListItem>
</Link> </Link> */}
</List> </List>
</div> </div>
); );
+17
View File
@@ -0,0 +1,17 @@
/* 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 React from 'react';
type ContextType = {
darkTheme: boolean
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>
};
const DarkTheme = React.createContext<ContextType>({
darkTheme: true,
setDarkTheme: ():void => {},
});
export default DarkTheme;
+17
View File
@@ -0,0 +1,17 @@
/* 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 React from 'react';
type ContextType = {
title: string
setTitle: React.Dispatch<React.SetStateAction<string>>
};
const NavBarTitle = React.createContext<ContextType>({
title: 'Tachidesk',
setTitle: ():void => {},
});
export default NavBarTitle;
+4
View File
@@ -1,3 +1,7 @@
/* 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/. */
body { body {
margin: 0; margin: 0;
} }
+4
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
+4
View File
@@ -1 +1,5 @@
/* 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/. */
/// <reference types="react-scripts" /> /// <reference types="react-scripts" />
+4
View File
@@ -1,3 +1,7 @@
/* 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 { ReportHandler } from 'web-vitals'; import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
+10 -7
View File
@@ -1,8 +1,14 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from '../components/ExtensionCard'; import ExtensionCard from '../components/ExtensionCard';
import NavBarTitle from '../context/NavbarTitle';
export default function Extensions() { export default function Extensions() {
let mapped; const { setTitle } = useContext(NavBarTitle);
setTitle('Extensions');
const [extensions, setExtensions] = useState<IExtension[]>([]); const [extensions, setExtensions] = useState<IExtension[]>([]);
useEffect(() => { useEffect(() => {
@@ -12,10 +18,7 @@ export default function Extensions() {
}, []); }, []);
if (extensions.length === 0) { if (extensions.length === 0) {
mapped = <h3>wait</h3>; return <h3>wait</h3>;
} else {
mapped = extensions.map((it) => <ExtensionCard extension={it} />);
} }
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
return <h2>{mapped}</h2>;
} }
+4
View File
@@ -1,3 +1,7 @@
/* 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 React from 'react'; import React from 'react';
export default function Home() { export default function Home() {
+11 -2
View File
@@ -1,10 +1,16 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useEffect, useState, useContext } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ChapterCard from '../components/ChapterCard'; import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails'; import MangaDetails from '../components/MangaDetails';
import NavBarTitle from '../context/NavbarTitle';
export default function Manga() { export default function Manga() {
const { id } = useParams<{id: string}>(); const { id } = useParams<{id: string}>();
const { setTitle } = useContext(NavBarTitle);
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
@@ -12,7 +18,10 @@ export default function Manga() {
useEffect(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`) fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setManga(data)); .then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, []); }, []);
useEffect(() => { useEffect(() => {
+32 -7
View File
@@ -1,20 +1,45 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) { export default function MangaList(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [lastPageNum] = useState<number>(1); const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
.then((data: { name: string }) => setTitle(data.name));
}, []);
useEffect(() => { useEffect(() => {
const sourceType = props.popular ? 'popular' : 'latest'; const sourceType = props.popular ? 'popular' : 'latest';
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas( .then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })), setMangas([
)); ...mangas,
}, []); ...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
});
}, [lastPageNum]);
return <MangaGrid mangas={mangas} />; return (
<MangaGrid
mangas={mangas}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
} }
+17 -2
View File
@@ -1,5 +1,10 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import NavBarTitle from '../context/NavbarTitle';
const style = { const style = {
display: 'flex', display: 'flex',
@@ -14,14 +19,24 @@ interface IPage {
imageUrl: string imageUrl: string
} }
interface IData {
first: IChapter
second: IPage[]
}
export default function Reader() { export default function Reader() {
const { setTitle } = useContext(NavBarTitle);
const [pages, setPages] = useState<IPage[]>([]); const [pages, setPages] = useState<IPage[]>([]);
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>(); const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
useEffect(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`) fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setPages(data)); .then((data:IData) => {
setTitle(data.first.name);
setPages(data.second);
});
}, []); }, []);
pages.sort((a, b) => (a.index - b.index)); pages.sort((a, b) => (a.index - b.index));
-48
View File
@@ -1,48 +0,0 @@
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import MangaGrid from '../components/MangaGrid';
const useStyles = makeStyles((theme) => ({
root: {
TextField: {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
export default function Search() {
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>('');
const textInput = React.createRef<HTMLInputElement>();
function doSearch() {
if (textInput.current) {
const { value } = textInput.current;
if (value === '') { setError(true); } else {
setError(false);
setMangas([]);
setMessage('button pressed');
}
}
}
const mangaGrid = <MangaGrid mangas={mangas} message={message} />;
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => doSearch()}>
Primary
</Button>
</form>
{mangaGrid}
</>
);
}
+95
View File
@@ -0,0 +1,95 @@
/* 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 React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
const useStyles = makeStyles((theme) => ({
root: {
TextField: {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
export default function SearchSingle() {
const { setTitle } = useContext(NavBarTitle);
const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
const textInput = React.createRef<HTMLInputElement>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
}, []);
function processInput() {
if (textInput.current) {
const { value } = textInput.current;
if (value === '') {
setError(true);
setMessage('Type something to search');
} else {
setError(false);
setSearchTerm(value);
setMessage('');
}
}
}
useEffect(() => {
if (searchTerm.length > 0) {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.json())
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
if (data.mangaList.length > 0) {
setMangas([
...mangas,
...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
} else {
setMessage('search qeury returned nothing.');
}
});
}
}, [searchTerm]);
const mangaGrid = (
<MangaGrid
mangas={mangas}
message={message}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => processInput()}>
Search
</Button>
</form>
{mangaGrid}
</>
);
}
+10 -7
View File
@@ -1,8 +1,14 @@
import React, { useEffect, useState } from 'react'; /* 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 React, { useContext, useEffect, useState } from 'react';
import SourceCard from '../components/SourceCard'; import SourceCard from '../components/SourceCard';
import NavBarTitle from '../context/NavbarTitle';
export default function Sources() { export default function Sources() {
let mapped; const { setTitle } = useContext(NavBarTitle);
setTitle('Sources');
const [sources, setSources] = useState<ISource[]>([]); const [sources, setSources] = useState<ISource[]>([]);
useEffect(() => { useEffect(() => {
@@ -12,10 +18,7 @@ export default function Sources() {
}, []); }, []);
if (sources.length === 0) { if (sources.length === 0) {
mapped = <h3>wait</h3>; return (<h3>wait</h3>);
} else {
mapped = sources.map((it) => <SourceCard source={it} />);
} }
return <>{sources.map((it) => <SourceCard source={it} />)}</>;
return <h2>{mapped}</h2>;
} }
+4
View File
@@ -1,3 +1,7 @@
/* 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/. */
interface IExtension { interface IExtension {
name: string name: string
lang: string lang: string