Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 771030b911 | |||
| 8d5744a2cf | |||
| a58aab9004 | |||
| 61bd32f7f0 | |||
| 63a444bd81 | |||
| 8f28c3b74b | |||
| d766206343 | |||
| 172f83f5b3 | |||
| 9e308025c3 | |||
| aaa6a16778 | |||
| 2a21da2210 | |||
| d1cd2cfc8c | |||
| 832c224ed4 | |||
| 99316f4bd5 | |||
| 9caae5f1e5 | |||
| 345be95ce9 | |||
| 6fe68841b7 | |||
| eaff2c15a9 | |||
| 5eb8dc66a8 | |||
| 49715c81e4 | |||
| 3398409555 | |||
| f05aa0589a | |||
| fbc71ce781 | |||
| ca9c671886 | |||
| bd109ba11f | |||
| 0ff770a98b | |||
| ed7bb408a3 | |||
| 84676b9156 | |||
| dcdd50ffe1 | |||
| afb21c59f0 | |||
| e219179519 | |||
| 15a2115c5a | |||
| 94c6f33925 | |||
| 202e38871d | |||
| 3f75b84651 | |||
| f171b785a0 | |||
| 088dd6a856 | |||
| 6318628ea2 | |||
| 0757ea5d0d |
@@ -5,12 +5,10 @@ Tachidesk is as multi-platform as you can get. Any platform that runs java and/o
|
||||
|
||||
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?
|
||||
#### 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
|
||||
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).
|
||||
|
||||
@@ -18,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.
|
||||
|
||||
#### Running on Docker
|
||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||
|
||||
## Building from source
|
||||
### Get Android stubs jar
|
||||
#### Manual download
|
||||
@@ -37,13 +38,29 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Support
|
||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
|
||||
|
||||
## 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`.
|
||||
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||
|
||||
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||
|
||||
@@ -8,7 +8,7 @@ plugins {
|
||||
id("org.jmailen.kotlinter") version "3.3.0"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.0.3"
|
||||
val TachideskVersion = "v0.1.5"
|
||||
|
||||
|
||||
repositories {
|
||||
@@ -79,7 +79,8 @@ dependencies {
|
||||
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||
implementation ("org.xerial:sqlite-jdbc:3.30.1")
|
||||
implementation ("com.h2database:h2:1.4.199")
|
||||
|
||||
|
||||
// AndroidCompat
|
||||
implementation(project(":AndroidCompat"))
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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.NamedNodeMap;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class MemoryCookieJar : CookieJar {
|
||||
private val cache = mutableSetOf<WrappedCookie>()
|
||||
|
||||
@Synchronized
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val cookiesToRemove = mutableSetOf<WrappedCookie>()
|
||||
val validCookies = mutableSetOf<WrappedCookie>()
|
||||
|
||||
cache.forEach { cookie ->
|
||||
if (cookie.isExpired()) {
|
||||
cookiesToRemove.add(cookie)
|
||||
} else if (cookie.matches(url)) {
|
||||
validCookies.add(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
cache.removeAll(cookiesToRemove)
|
||||
|
||||
return validCookies.toList().map(WrappedCookie::unwrap)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
|
||||
|
||||
cache.removeAll(cookiesToAdd)
|
||||
cache.addAll(cookiesToAdd)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||
fun unwrap() = cookie
|
||||
|
||||
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||
|
||||
fun matches(url: HttpUrl) = cookie.matches(url)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is WrappedCookie) return false
|
||||
|
||||
return other.cookie.name == cookie.name &&
|
||||
other.cookie.domain == cookie.domain &&
|
||||
other.cookie.path == cookie.path &&
|
||||
other.cookie.secure == cookie.secure &&
|
||||
other.cookie.hostOnly == cookie.hostOnly
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var hash = 17
|
||||
hash = 31 * hash + cookie.name.hashCode()
|
||||
hash = 31 * hash + cookie.domain.hashCode()
|
||||
hash = 31 * hash + cookie.path.hashCode()
|
||||
hash = 31 * hash + if (cookie.secure) 0 else 1
|
||||
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
|
||||
return hash
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,17 @@ class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
// val cookieManager = AndroidCookieJar()
|
||||
val cookieManager = MemoryCookieJar()
|
||||
|
||||
val client by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
// .cookieJar(cookieManager)
|
||||
.cookieJar(cookieManager)
|
||||
// .cache(Cache(cacheDir, cacheSize))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.writeTimeout(5, TimeUnit.MINUTES)
|
||||
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||
|
||||
// .addInterceptor(UserAgentInterceptor())
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
|
||||
@@ -34,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
call.cancel()
|
||||
// call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
@@ -80,17 +80,18 @@ fun Call.asObservable(): Observable<Response> {
|
||||
// }
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
return asObservable()
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
// val progressClient = newBuilder()
|
||||
// .cache(null)
|
||||
// .cache(nasObservableSuccessull)
|
||||
// .addNetworkInterceptor { chain ->
|
||||
// val originalResponse = chain.proceed(chain.request())
|
||||
// originalResponse.newBuilder()
|
||||
@@ -104,7 +105,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
// .cache(null)
|
||||
// .addNetworkInterceptor { chain ->
|
||||
// val originalResponse = chain.proceed(chain.request())
|
||||
// originalResponse.newBuilder()
|
||||
|
||||
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
protected val network: NetworkHelper by injectLazy()
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
// /**
|
||||
// * Preferences that a source may need.
|
||||
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
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
|
||||
|
||||
object Config {
|
||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||
val mangaRoot = "$dataRoot/manga"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
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 io.javalin.Javalin
|
||||
import ir.armor.tachidesk.util.applicationSetup
|
||||
import ir.armor.tachidesk.util.getChapter
|
||||
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.getPages
|
||||
import ir.armor.tachidesk.util.getPageImage
|
||||
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
|
||||
@@ -50,6 +57,8 @@ class Main {
|
||||
// start app
|
||||
androidCompat.startApp(App())
|
||||
|
||||
// Thread(getMangaUpdateQueueThread).start()
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
this::class.java.classLoader.getResource("/react/index.html")
|
||||
@@ -71,11 +80,20 @@ class Main {
|
||||
|
||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
println(apkName)
|
||||
println("installing $apkName")
|
||||
|
||||
ctx.status(
|
||||
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 ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
@@ -101,6 +119,14 @@ class Main {
|
||||
ctx.json(getManga(mangaId))
|
||||
}
|
||||
|
||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val result = getThumbnail(mangaId)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapterList(mangaId))
|
||||
@@ -109,7 +135,17 @@ class Main {
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getPages(chapterId, mangaId))
|
||||
ctx.json(getChapter(chapterId, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterId, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
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.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
@@ -11,18 +16,21 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object DBMangaer {
|
||||
val db by lazy {
|
||||
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
|
||||
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDataBaseTables() {
|
||||
// mention db object to connect
|
||||
DBMangaer.db
|
||||
// val db = DBMangaer.db
|
||||
// db.useNestedTransactions = true
|
||||
|
||||
transaction {
|
||||
SchemaUtils.create(ExtensionsTable)
|
||||
SchemaUtils.create(SourceTable)
|
||||
SchemaUtils.create(MangaTable)
|
||||
SchemaUtils.create(ChapterTable)
|
||||
SchemaUtils.create(PageTable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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(
|
||||
val id: Int,
|
||||
val url: String,
|
||||
@@ -8,4 +12,5 @@ data class ChapterDataClass(
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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(
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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
|
||||
|
||||
data class MangaDataClass(
|
||||
@@ -18,3 +22,8 @@ data class MangaDataClass(
|
||||
val genre: String? = null,
|
||||
val status: String = MangaStatus.UNKNOWN.name
|
||||
)
|
||||
|
||||
data class PagedMangaListDataClass(
|
||||
val mangaList: List<MangaDataClass>,
|
||||
val hasNextPage: Boolean
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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(
|
||||
val index: Int,
|
||||
var imageUrl: String,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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(
|
||||
val id: String,
|
||||
val name: String,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.LongEntity
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ChapterTable : IntIdTable() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ExtensionsTable : IntIdTable() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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.SManga
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object PageTable : IntIdTable() {
|
||||
val index = integer("index")
|
||||
val url = varchar("url", 2048)
|
||||
val imageUrl = varchar("imageUrl", 2048).nullable()
|
||||
|
||||
val chapter = reference("chapter", ChapterTable)
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* 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.jetbrains.exposed.dao.id.IdTable
|
||||
|
||||
object SourceTable : IdTable<Long>() {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
/* 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.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.PageDataClass
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
@@ -53,14 +57,14 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
|
||||
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
return transaction {
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
|
||||
val pagesList = source.fetchPageList(
|
||||
val pageList = source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
@@ -74,22 +78,24 @@ fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageData
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId
|
||||
mangaId,
|
||||
pageList.count()
|
||||
)
|
||||
|
||||
val pages = pagesList.map {
|
||||
PageDataClass(
|
||||
it.index,
|
||||
getTrueImageUrl(it, source)
|
||||
)
|
||||
pageList.forEach { page ->
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||
if (pageEntry == null) {
|
||||
transaction {
|
||||
PageTable.insert {
|
||||
it[index] = page.index
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
it[this.chapter] = chapterId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@transaction Pair(chapter, pages)
|
||||
return@transaction chapter
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
return if (page.imageUrl == null) {
|
||||
source.fetchImageUrl(page).toBlocking().first()!!
|
||||
} else page.imageUrl!!
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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.model.Extension
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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 okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
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(128 * 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given source to an output stream and closes both resources.
|
||||
*
|
||||
* @param stream the stream where the source is copied.
|
||||
*/
|
||||
fun BufferedSource.saveTo(stream: OutputStream) {
|
||||
use { input ->
|
||||
stream.sink().buffer().use {
|
||||
it.writeAll(input)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +1,122 @@
|
||||
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 ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.InputStream
|
||||
|
||||
fun getManga(mangaId: Int): MangaDataClass {
|
||||
return transaction {
|
||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
|
||||
return@transaction if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
return if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
mangaEntry[MangaTable.thumbnail_url],
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||
|
||||
true,
|
||||
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
|
||||
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
|
||||
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()
|
||||
|
||||
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
it[MangaTable.initialized] = true
|
||||
|
||||
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,
|
||||
)
|
||||
it[MangaTable.artist] = fetchedManga.artist
|
||||
it[MangaTable.author] = fetchedManga.author
|
||||
it[MangaTable.description] = fetchedManga.description
|
||||
it[MangaTable.genre] = fetchedManga.genre
|
||||
it[MangaTable.status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||
}
|
||||
}
|
||||
|
||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val newThumbnail = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
var filePath = "${Config.thumbnailsRoot}/$mangaId."
|
||||
|
||||
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
|
||||
if (potentialCache != null) {
|
||||
println("using cached thumbnail file")
|
||||
return Pair(
|
||||
pathToInputStream(potentialCache),
|
||||
"image/${potentialCache.substringAfter(filePath)}"
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if (response.code == 200) {
|
||||
val contentType = response.headers["content-type"]!!
|
||||
filePath += contentType.substringAfter("image/")
|
||||
|
||||
writeStream(response.body!!.byteStream(), filePath)
|
||||
|
||||
return Pair(
|
||||
pathToInputStream(filePath),
|
||||
contentType
|
||||
)
|
||||
} else {
|
||||
throw Exception("request error! ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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.POST
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.URLEncoder
|
||||
|
||||
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
||||
|
||||
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
||||
|
||||
private fun clientBuilder(
|
||||
r18Toggle: Int,
|
||||
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
||||
): OkHttpClient = okHttpClient.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalCookies = chain.request().header("Cookie") ?: ""
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
||||
.build()
|
||||
chain.proceed(newReq)
|
||||
}.build()
|
||||
|
||||
private fun cookiesHeader(r18Toggle: Int): String {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
||||
return buildCookies(cookies)
|
||||
}
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) =
|
||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
// fun isLogged(): Boolean {
|
||||
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
||||
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||
// }
|
||||
|
||||
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("login_username", username)
|
||||
.add("login_password", password)
|
||||
.add("no_js", "1")
|
||||
.add("remember_me", "1")
|
||||
|
||||
twoFactorCode.let {
|
||||
formBody.add("two_factor", it)
|
||||
}
|
||||
|
||||
val response = clientBuilder().newCall(
|
||||
POST(
|
||||
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
mangaDexSource.headers,
|
||||
formBody.build()
|
||||
)
|
||||
).execute()
|
||||
return response.body!!.string().isEmpty()
|
||||
}
|
||||
//
|
||||
// fun logout(): Boolean {
|
||||
// return withContext(Dispatchers.IO) {
|
||||
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
||||
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||
// val token = cookie?.value
|
||||
// if (token.isNullOrEmpty()) {
|
||||
// return@withContext true
|
||||
// }
|
||||
// val result = clientBuilder().newCall(
|
||||
// POSTWithCookie(
|
||||
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
||||
// REMEMBER_ME,
|
||||
// token,
|
||||
// headers
|
||||
// )
|
||||
// ).execute()
|
||||
// val resultStr = result.body!!.string()
|
||||
// if (resultStr.contains("success", true)) {
|
||||
// network.cookieManager.remove(httpUrl)
|
||||
// return@withContext true
|
||||
// }
|
||||
//
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
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.PagedMangaListDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
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 mangasPage = if (popular) {
|
||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
||||
@@ -21,13 +30,13 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
||||
return mangasPage.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> {
|
||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangasPage = this
|
||||
return transaction {
|
||||
val mangaList = transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||
var mangaEntityId = if (mangaEntry == null) { // create manga entry
|
||||
MangaTable.insertAndGetId {
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
@@ -36,30 +45,50 @@ fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> {
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.genre
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId,
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
manga.initialized,
|
||||
|
||||
manga.artist,
|
||||
manga.author,
|
||||
manga.description,
|
||||
manga.genre,
|
||||
MangaStatus.valueOf(manga.status).name,
|
||||
)
|
||||
} else {
|
||||
mangaEntry[MangaTable.id].value
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
MangaDataClass(
|
||||
mangaEntityId,
|
||||
sourceId,
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
manga.thumbnail_url,
|
||||
|
||||
manga.initialized,
|
||||
|
||||
manga.artist,
|
||||
manga.author,
|
||||
manga.description,
|
||||
manga.genre,
|
||||
MangaStatus.valueOf(manga.status).name,
|
||||
)
|
||||
}
|
||||
}
|
||||
return PagedMangaListDataClass(
|
||||
mangaList,
|
||||
mangasPage.hasNextPage
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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.online.HttpSource
|
||||
import ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
if (page.imageUrl == null) {
|
||||
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
||||
}
|
||||
return page.imageUrl!!
|
||||
}
|
||||
|
||||
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||
|
||||
val tachiPage = Page(
|
||||
pageEntry[PageTable.index],
|
||||
pageEntry[PageTable.url],
|
||||
pageEntry[PageTable.imageUrl]
|
||||
)
|
||||
|
||||
if (pageEntry[PageTable.imageUrl] == null) {
|
||||
transaction {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val saveDir = getChapterDir(mangaId, chapterId)
|
||||
File(saveDir).mkdirs()
|
||||
var filePath = "$saveDir/$index."
|
||||
|
||||
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
|
||||
if (potentialCache != null) {
|
||||
println("using cached page file for $index")
|
||||
return Pair(
|
||||
pathToInputStream(potentialCache),
|
||||
"image/${potentialCache.substringAfter("$filePath")}"
|
||||
)
|
||||
}
|
||||
|
||||
val response = source.fetchImage(tachiPage).toBlocking().first()
|
||||
|
||||
if (response.code == 200) {
|
||||
val contentType = response.headers["content-type"]!!
|
||||
filePath += contentType.substringAfter("image/")
|
||||
|
||||
Files.newOutputStream(Paths.get(filePath)).use { os ->
|
||||
|
||||
response.body!!.source().saveTo(os)
|
||||
}
|
||||
|
||||
// writeStream(response.body!!.source(), filePath)
|
||||
|
||||
return Pair(
|
||||
pathToInputStream(filePath),
|
||||
contentType
|
||||
)
|
||||
} else {
|
||||
throw Exception("request error! ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||
val source = getHttpSource(sourceId)
|
||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||
|
||||
val chapterDir = when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
|
||||
val mangaTitle = mangaEntry[MangaTable.title]
|
||||
val sourceName = source.toString()
|
||||
|
||||
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||
// make sure dirs exist
|
||||
File(mangaDir).mkdirs()
|
||||
return mangaDir
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
/* 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.dataclass.PagedMangaListDataClass
|
||||
|
||||
fun sourceFilters(sourceId: Long) {
|
||||
val source = getHttpSource(sourceId)
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
|
||||
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): List<MangaDataClass> {
|
||||
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
|
||||
return searchManga.processEntries(sourceId)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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.online.HttpSource
|
||||
import ir.armor.tachidesk.Config
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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.database.makeDataBaseTables
|
||||
import java.io.File
|
||||
@@ -8,6 +12,7 @@ fun applicationSetup() {
|
||||
// make dirs we need
|
||||
File(Config.dataRoot).mkdirs()
|
||||
File(Config.extensionsRoot).mkdirs()
|
||||
File(Config.thumbnailsRoot).mkdirs()
|
||||
|
||||
makeDataBaseTables()
|
||||
}
|
||||
|
||||
+25
-2
@@ -1,5 +1,9 @@
|
||||
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 eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
@@ -13,6 +17,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
@@ -28,8 +33,8 @@ fun installAPK(apkName: String): Int {
|
||||
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
||||
|
||||
// check if we don't have the dex file already downloaded
|
||||
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
if (!File(dexPath).exists()) {
|
||||
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
if (!File(jarPath).exists()) {
|
||||
runBlocking {
|
||||
val api = ExtensionGithubApi()
|
||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
||||
@@ -126,3 +131,21 @@ private fun downloadAPKFile(url: String, apkPath: String) {
|
||||
sink.writeAll(response.body!!.source())
|
||||
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()
|
||||
}
|
||||
}
|
||||
+72
-30
@@ -1,7 +1,15 @@
|
||||
/* 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 {
|
||||
BrowserRouter as Router, Route, Switch,
|
||||
} 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 Home from './screens/Home';
|
||||
import Sources from './screens/Sources';
|
||||
@@ -11,43 +19,77 @@ import Manga from './screens/Manga';
|
||||
import Reader from './screens/Reader';
|
||||
import Search from './screens/SearchSingle';
|
||||
import NavBarTitle from './context/NavbarTitle';
|
||||
import DarkTheme from './context/DarkTheme';
|
||||
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
const contextValue = { title, setTitle };
|
||||
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 (
|
||||
<Router>
|
||||
<NavBarTitle.Provider value={contextValue}>
|
||||
<NavBar />
|
||||
|
||||
<Switch>
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route path="/extensions">
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<MangaList popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<MangaList popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</NavBarTitle.Provider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NavBarTitle.Provider value={navTitleContext}>
|
||||
<CssBaseline />
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<NavBar />
|
||||
</DarkTheme.Provider>
|
||||
<Container maxWidth={false} disableGutters>
|
||||
<Switch>
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route path="/extensions">
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<MangaList popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<MangaList popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Container>
|
||||
</NavBarTitle.Provider>
|
||||
</ThemeProvider>
|
||||
</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 { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/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, { useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
@@ -42,7 +46,7 @@ export default function ExtensionCard(props: IProps) {
|
||||
name, lang, versionName, iconUrl, installed, apkName,
|
||||
},
|
||||
} = props;
|
||||
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
|
||||
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||
|
||||
const classes = useStyles();
|
||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||
@@ -50,10 +54,25 @@ export default function ExtensionCard(props: IProps) {
|
||||
function install() {
|
||||
setInstalledState('installing');
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className={classes.root}>
|
||||
@@ -76,7 +95,7 @@ export default function ExtensionCard(props: IProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
|
||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
|
||||
</CardContent>
|
||||
</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 { makeStyles } from '@material-ui/core/styles';
|
||||
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 Typography from '@material-ui/core/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Grid } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
@@ -39,7 +44,7 @@ const useStyles = makeStyles({
|
||||
interface IProps {
|
||||
manga: IManga
|
||||
}
|
||||
export default function MangaCard(props: IProps) {
|
||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
manga: {
|
||||
id, title, thumbnailUrl,
|
||||
@@ -48,22 +53,26 @@ export default function MangaCard(props: IProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Link to={`/manga/${id}/`}>
|
||||
<Card className={classes.root}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
component="img"
|
||||
alt={title}
|
||||
image={thumbnailUrl}
|
||||
title={title}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||
<Link to={`/manga/${id}/`}>
|
||||
<Card className={classes.root} ref={ref}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
component="img"
|
||||
alt={title}
|
||||
image={thumbnailUrl}
|
||||
title={title}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</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';
|
||||
|
||||
interface IProps{
|
||||
|
||||
@@ -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';
|
||||
|
||||
interface IProps{
|
||||
mangas: IManga[]
|
||||
message?: string
|
||||
hasNextPage: boolean
|
||||
lastPageNum: number
|
||||
setLastPageNum: (lastPageNum: number) => void
|
||||
}
|
||||
|
||||
export default function MangaGrid(props: IProps) {
|
||||
const { mangas, message } = props;
|
||||
const {
|
||||
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
|
||||
} = props;
|
||||
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) {
|
||||
mapped = <h3>{message !== undefined ? message : 'loading...'}</h3>;
|
||||
mapped = <h3>{message}</h3>;
|
||||
} else {
|
||||
mapped = (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
|
||||
{mangas.map((it) => (
|
||||
<MangaCard manga={it} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
mapped = mangas.map((it, idx) => {
|
||||
if (idx === mangas.length - 1) {
|
||||
return <MangaCard manga={it} ref={lastManga} />;
|
||||
}
|
||||
return <MangaCard manga={it} />;
|
||||
});
|
||||
}
|
||||
|
||||
return mapped;
|
||||
return (
|
||||
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
|
||||
{mapped}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
MangaGrid.defaultProps = {
|
||||
message: 'loading...',
|
||||
};
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
/* 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 MoreIcon from '@material-ui/icons/MoreVert';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
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 NavBarTitle from '../context/NavbarTitle';
|
||||
import DarkTheme from '../context/DarkTheme';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -20,14 +29,35 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// const theme = createMuiTheme({
|
||||
// overrides: {
|
||||
// MuiAppBar: {
|
||||
// colorPrimary: { backgroundColor: '#FFC0CB' },
|
||||
// },
|
||||
// },
|
||||
// palette: { type: 'dark' },
|
||||
// });
|
||||
|
||||
export default function NavBar() {
|
||||
const classes = useStyles();
|
||||
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 (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="static">
|
||||
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
@@ -42,6 +72,42 @@ export default function NavBar() {
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
{title}
|
||||
</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>
|
||||
</AppBar>
|
||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||
|
||||
@@ -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 { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/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 { makeStyles } from '@material-ui/core/styles';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
type ContextType = {
|
||||
|
||||
@@ -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 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -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 ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
Vendored
+4
@@ -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" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
|
||||
@@ -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, { useContext, useEffect, useState } from 'react';
|
||||
import ExtensionCard from '../components/ExtensionCard';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
@@ -6,7 +10,6 @@ export default function Extensions() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
setTitle('Extensions');
|
||||
const [extensions, setExtensions] = useState<IExtension[]>([]);
|
||||
let mapped;
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:4567/api/v1/extension/list')
|
||||
@@ -15,10 +18,7 @@ export default function Extensions() {
|
||||
}, []);
|
||||
|
||||
if (extensions.length === 0) {
|
||||
mapped = <h3>wait</h3>;
|
||||
} else {
|
||||
mapped = extensions.map((it) => <ExtensionCard extension={it} />);
|
||||
return <h3>wait</h3>;
|
||||
}
|
||||
|
||||
return <h2>{mapped}</h2>;
|
||||
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
export default function Home() {
|
||||
|
||||
@@ -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, { useEffect, useState, useContext } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ChapterCard from '../components/ChapterCard';
|
||||
|
||||
@@ -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, { useContext, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
@@ -7,7 +11,8 @@ export default function MangaList(props: { popular: boolean }) {
|
||||
const { sourceId } = useParams<{sourceId: string}>();
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
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}`)
|
||||
@@ -19,10 +24,22 @@ export default function MangaList(props: { popular: boolean }) {
|
||||
const sourceType = props.popular ? 'popular' : 'latest';
|
||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
||||
.then((response) => response.json())
|
||||
.then((data: IManga[]) => setMangas(
|
||||
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id })),
|
||||
));
|
||||
}, []);
|
||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, { useContext, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
@@ -10,44 +14,36 @@ const style = {
|
||||
backgroundColor: '#343a40',
|
||||
} as React.CSSProperties;
|
||||
|
||||
interface IPage {
|
||||
index: number
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
interface IData {
|
||||
first: IChapter
|
||||
second: IPage[]
|
||||
}
|
||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||
|
||||
export default function Reader() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
|
||||
const [pages, setPages] = useState<IPage[]>([]);
|
||||
const [pageCount, setPageCount] = useState<number>(-1);
|
||||
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||
.then((response) => response.json())
|
||||
.then((data:IData) => {
|
||||
setTitle(data.first.name);
|
||||
setPages(data.second);
|
||||
.then((data:IChapter) => {
|
||||
setTitle(data.name);
|
||||
setPageCount(data.pageCount);
|
||||
});
|
||||
}, []);
|
||||
|
||||
pages.sort((a, b) => (a.index - b.index));
|
||||
|
||||
let mapped;
|
||||
if (pages.length === 0) {
|
||||
mapped = <h3>wait</h3>;
|
||||
} else {
|
||||
mapped = pages.map(({ imageUrl }) => (
|
||||
<div style={{ margin: '0 auto' }}>
|
||||
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
|
||||
if (pageCount === -1) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<h3>wait</h3>
|
||||
</div>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
const mapped = range(pageCount).map((index) => (
|
||||
<div style={{ margin: '0 auto' }}>
|
||||
<img src={`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="f" style={{ maxWidth: '100%' }} />
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div style={style}>
|
||||
{mapped}
|
||||
|
||||
@@ -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, { useContext, useEffect, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
@@ -22,8 +26,9 @@ export default function SearchSingle() {
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [lastPageNum] = useState<number>(1);
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
|
||||
const textInput = React.createRef<HTMLInputElement>();
|
||||
|
||||
@@ -51,13 +56,14 @@ export default function SearchSingle() {
|
||||
if (searchTerm.length > 0) {
|
||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
|
||||
.then((response) => response.json())
|
||||
.then((data: IManga[]) => {
|
||||
if (data.length > 0) {
|
||||
setMangas(
|
||||
data.map((it) => (
|
||||
{ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id }
|
||||
)),
|
||||
);
|
||||
.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.');
|
||||
}
|
||||
@@ -65,14 +71,22 @@ export default function SearchSingle() {
|
||||
}
|
||||
}, [searchTerm]);
|
||||
|
||||
const mangaGrid = <MangaGrid mangas={mangas} message={message} />;
|
||||
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()}>
|
||||
Primary
|
||||
Search
|
||||
</Button>
|
||||
</form>
|
||||
{mangaGrid}
|
||||
|
||||
@@ -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, { useContext, useEffect, useState } from 'react';
|
||||
import SourceCard from '../components/SourceCard';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
@@ -6,7 +10,6 @@ export default function Sources() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
setTitle('Sources');
|
||||
const [sources, setSources] = useState<ISource[]>([]);
|
||||
let mapped;
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:4567/api/v1/source/list')
|
||||
@@ -15,10 +18,7 @@ export default function Sources() {
|
||||
}, []);
|
||||
|
||||
if (sources.length === 0) {
|
||||
mapped = <h3>wait</h3>;
|
||||
} else {
|
||||
mapped = sources.map((it) => <SourceCard source={it} />);
|
||||
return (<h3>wait</h3>);
|
||||
}
|
||||
|
||||
return <h2>{mapped}</h2>;
|
||||
return <>{sources.map((it) => <SourceCard source={it} />)}</>;
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -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 {
|
||||
name: string
|
||||
lang: string
|
||||
@@ -30,4 +34,5 @@ interface IChapter {
|
||||
chapter_number: number
|
||||
scanlator: String
|
||||
mangaId: number
|
||||
pageCount: number
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user