Compare commits

...

39 Commits

Author SHA1 Message Date
Aria Moradi 771030b911 [CI RELEASE] v.0.1.5 2021-02-04 04:28:00 +03:30
Aria Moradi 8d5744a2cf fix chapter naming, db naming 2021-02-04 04:27:25 +03:30
Aria Moradi a58aab9004 [CI RELEASE] v.0.1.4 2021-02-04 04:01:20 +03:30
Aria Moradi 61bd32f7f0 don't cancel me shit 2021-02-04 03:52:49 +03:30
Aria Moradi 63a444bd81 calling HttpSource.imageRequest now 2021-02-04 03:42:30 +03:30
Aria Moradi 8f28c3b74b eh missing shit from last commit 2021-02-04 00:38:23 +03:30
Aria Moradi d766206343 fix sqlite locking fuckery by replacing it with h2 2021-02-04 00:32:01 +03:30
Aria Moradi 172f83f5b3 Manga dir 2021-02-03 23:35:34 +03:30
Aria Moradi 9e308025c3 some initial code for MangaDex login 2021-02-03 22:07:39 +03:30
Aria Moradi aaa6a16778 Update README.md 2021-01-30 01:07:42 +03:30
Aria Moradi 2a21da2210 [SKIP CI] update README.md 2021-01-29 19:11:38 +03:30
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
57 changed files with 1093 additions and 241 deletions
+23 -6
View File
@@ -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. 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).
@@ -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. 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
@@ -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. 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.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## 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`.
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 ## License
Copyright (C) 2020-2021 Aria Moradi and contributors Copyright (C) 2020-2021 Aria Moradi and contributors
+3 -2
View File
@@ -8,7 +8,7 @@ plugins {
id("org.jmailen.kotlinter") version "3.3.0" id("org.jmailen.kotlinter") version "3.3.0"
} }
val TachideskVersion = "v0.0.3" val TachideskVersion = "v0.1.5"
repositories { repositories {
@@ -79,7 +79,8 @@ dependencies {
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version") implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version") implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$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 // AndroidCompat
implementation(project(":AndroidCompat")) implementation(project(":AndroidCompat"))
@@ -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;
@@ -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 private val cacheSize = 5L * 1024 * 1024 // 5 MiB
// val cookieManager = AndroidCookieJar() val cookieManager = MemoryCookieJar()
val client by lazy { val client by lazy {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
// .cookieJar(cookieManager) .cookieJar(cookieManager)
// .cache(Cache(cacheDir, cacheSize)) // .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor()) // .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) { // if (BuildConfig.DEBUG) {
@@ -34,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
} }
override fun unsubscribe() { override fun unsubscribe() {
call.cancel() // call.cancel()
} }
override fun isUnsubscribed(): Boolean { override fun isUnsubscribed(): Boolean {
@@ -80,17 +80,18 @@ fun Call.asObservable(): Observable<Response> {
// } // }
fun Call.asObservableSuccess(): Observable<Response> { fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response -> return asObservable()
if (!response.isSuccessful) { .doOnNext { response ->
response.close() if (!response.isSuccessful) {
throw Exception("HTTP error ${response.code}") response.close()
throw Exception("HTTP error ${response.code}")
}
} }
}
} }
// 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(nasObservableSuccessull)
// .addNetworkInterceptor { chain -> // .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request()) // val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder() // originalResponse.newBuilder()
@@ -104,7 +105,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 ->
// val originalResponse = chain.proceed(chain.request()) // val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder() // originalResponse.newBuilder()
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
/** /**
* Network service. * Network service.
*/ */
protected val network: NetworkHelper by injectLazy() val network: NetworkHelper by injectLazy()
// /** // /**
// * Preferences that a source may need. // * 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 * @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) return GET(page.imageUrl!!, headers)
} }
@@ -1,8 +1,14 @@
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"
val mangaRoot = "$dataRoot/manga"
} }
@@ -1,16 +1,23 @@
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.applicationSetup import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapter
import ir.armor.tachidesk.util.getChapterList import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionList import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList 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.getSource
import ir.armor.tachidesk.util.getSourceList import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch import ir.armor.tachidesk.util.sourceSearch
@@ -50,6 +57,8 @@ class Main {
// 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")
@@ -71,11 +80,20 @@ class Main {
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())
} }
@@ -101,6 +119,14 @@ class Main {
ctx.json(getManga(mangaId)) 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 -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId)) ctx.json(getChapterList(mangaId))
@@ -109,7 +135,17 @@ class Main {
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx -> app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt() val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").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 // global search
@@ -1,9 +1,14 @@
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
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SchemaUtils
@@ -11,18 +16,21 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer { object DBMangaer {
val db by lazy { 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() { fun makeDataBaseTables() {
// mention db object to connect // mention db object to connect
DBMangaer.db DBMangaer.db
// val db = DBMangaer.db
// db.useNestedTransactions = true
transaction { transaction {
SchemaUtils.create(ExtensionsTable) SchemaUtils.create(ExtensionsTable)
SchemaUtils.create(SourceTable) SchemaUtils.create(SourceTable)
SchemaUtils.create(MangaTable) SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable) SchemaUtils.create(ChapterTable)
SchemaUtils.create(PageTable)
} }
} }
@@ -1,5 +1,9 @@
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,
@@ -8,4 +12,5 @@ data class ChapterDataClass(
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
val pageCount: Int? = null,
) )
@@ -1,5 +1,9 @@
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,
@@ -1,5 +1,9 @@
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(
@@ -18,3 +22,8 @@ data class MangaDataClass(
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,5 +1,9 @@
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,5 +1,9 @@
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,
@@ -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
@@ -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
@@ -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.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity import org.jetbrains.exposed.dao.LongEntity
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.table 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 import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.table 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 import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() { object ExtensionsTable : IntIdTable() {
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.table 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 eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IntIdTable 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 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 import org.jetbrains.exposed.dao.id.IdTable
object SourceTable : IdTable<Long>() { object SourceTable : IdTable<Long>() {
@@ -1,13 +1,17 @@
package ir.armor.tachidesk.util 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.ChapterDataClass
import ir.armor.tachidesk.database.dataclass.PageDataClass
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 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.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
@@ -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 { 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
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val pagesList = source.fetchPageList( val pageList = source.fetchPageList(
SChapter.create().apply { SChapter.create().apply {
url = chapterEntry[ChapterTable.url] url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name] name = chapterEntry[ChapterTable.name]
@@ -74,22 +78,24 @@ fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageData
chapterEntry[ChapterTable.date_upload], chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number], chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterEntry[ChapterTable.scanlator],
mangaId mangaId,
pageList.count()
) )
val pages = pagesList.map { pageList.forEach { page ->
PageDataClass( val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
it.index, if (pageEntry == null) {
getTrueImageUrl(it, source) 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 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
@@ -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 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
fun getManga(mangaId: Int): MangaDataClass { fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
return transaction { var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
return@transaction if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass( MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].value, mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url], if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true, true,
mangaEntry[MangaTable.artist], mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author], mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description], mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails( val fetchedManga = source.fetchMangaDetails(
SManga.create().apply { SManga.create().apply {
url = mangaEntry[MangaTable.url] url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title] 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
} }
).toBlocking().first()
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
MangaDataClass( it[MangaTable.initialized] = true
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url], it[MangaTable.artist] = fetchedManga.artist
mangaEntry[MangaTable.title], it[MangaTable.author] = fetchedManga.author
mangaEntry[MangaTable.thumbnail_url], it[MangaTable.description] = fetchedManga.description
it[MangaTable.genre] = fetchedManga.genre
true, it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
mangaEntry[MangaTable.artist], it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
mangaEntry[MangaTable.author], }
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} }
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 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 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 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()
@@ -21,13 +30,13 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
return mangasPage.processEntries(sourceId) return mangasPage.processEntries(sourceId)
} }
fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> { fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this val mangasPage = this
return transaction { 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
@@ -36,30 +45,50 @@ fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> {
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
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 { } 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 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) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
// source.getFilterList().toItems() // 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 source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)
@@ -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
@@ -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,6 +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)
@@ -126,3 +131,21 @@ private fun downloadAPKFile(url: String, apkPath: String) {
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()
}
}
+72 -30
View File
@@ -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 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';
@@ -11,43 +19,77 @@ import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/SearchSingle'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle'; 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 [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 ( return (
<Router> <Router>
<NavBarTitle.Provider value={contextValue}>
<NavBar />
<Switch> <ThemeProvider theme={theme}>
<Route path="/sources/:sourceId/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>
</NavBarTitle.Provider> <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...',
};
+67 -1
View File
@@ -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 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 NavBarTitle from '../context/NavbarTitle';
import DarkTheme from '../context/DarkTheme';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -20,14 +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 { 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"
@@ -42,6 +72,42 @@ export default function NavBar() {
<Typography variant="h6" className={classes.title}> <Typography variant="h6" className={classes.title}>
{title} {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} />
@@ -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';
@@ -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';
+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;
+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';
type ContextType = { type ContextType = {
+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) => {
+6 -6
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, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import ExtensionCard from '../components/ExtensionCard'; import ExtensionCard from '../components/ExtensionCard';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
@@ -6,7 +10,6 @@ export default function Extensions() {
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
setTitle('Extensions'); setTitle('Extensions');
const [extensions, setExtensions] = useState<IExtension[]>([]); const [extensions, setExtensions] = useState<IExtension[]>([]);
let mapped;
useEffect(() => { useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/extension/list') fetch('http://127.0.0.1:4567/api/v1/extension/list')
@@ -15,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() {
+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, { useEffect, useState, useContext } from 'react'; 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';
+23 -6
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, { useContext, useEffect, useState } from 'react'; 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';
@@ -7,7 +11,8 @@ export default function MangaList(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle); 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(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`) 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'; 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: IManga[]) => setMangas( .then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnailUrl, 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}
/>
);
} }
+19 -23
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, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
@@ -10,44 +14,36 @@ const style = {
backgroundColor: '#343a40', backgroundColor: '#343a40',
} as React.CSSProperties; } as React.CSSProperties;
interface IPage { const range = (n:number) => Array.from({ length: n }, (value, key) => key);
index: number
imageUrl: string
}
interface IData {
first: IChapter
second: IPage[]
}
export default function Reader() { export default function Reader() {
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
const [pages, setPages] = useState<IPage[]>([]); const [pageCount, setPageCount] = useState<number>(-1);
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:IData) => { .then((data:IChapter) => {
setTitle(data.first.name); setTitle(data.name);
setPages(data.second); setPageCount(data.pageCount);
}); });
}, []); }, []);
pages.sort((a, b) => (a.index - b.index)); if (pageCount === -1) {
return (
let mapped; <div style={style}>
if (pages.length === 0) { <h3>wait</h3>
mapped = <h3>wait</h3>;
} else {
mapped = pages.map(({ imageUrl }) => (
<div style={{ margin: '0 auto' }}>
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
</div> </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 ( return (
<div style={style}> <div style={style}>
{mapped} {mapped}
+24 -10
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, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
@@ -22,8 +26,9 @@ export default function SearchSingle() {
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [lastPageNum] = useState<number>(1);
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
const textInput = React.createRef<HTMLInputElement>(); const textInput = React.createRef<HTMLInputElement>();
@@ -51,13 +56,14 @@ export default function SearchSingle() {
if (searchTerm.length > 0) { if (searchTerm.length > 0) {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`) fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data: IManga[]) => { .then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
if (data.length > 0) { if (data.mangaList.length > 0) {
setMangas( setMangas([
data.map((it) => ( ...mangas,
{ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id } ...data.mangaList.map((it) => ({
)), title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
); }))]);
setHasNextPage(data.hasNextPage);
} else { } else {
setMessage('search qeury returned nothing.'); setMessage('search qeury returned nothing.');
} }
@@ -65,14 +71,22 @@ export default function SearchSingle() {
} }
}, [searchTerm]); }, [searchTerm]);
const mangaGrid = <MangaGrid mangas={mangas} message={message} />; const mangaGrid = (
<MangaGrid
mangas={mangas}
message={message}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
return ( return (
<> <>
<form className={classes.root} noValidate autoComplete="off"> <form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." /> <TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => processInput()}> <Button variant="contained" color="primary" onClick={() => processInput()}>
Primary Search
</Button> </Button>
</form> </form>
{mangaGrid} {mangaGrid}
+6 -6
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, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import SourceCard from '../components/SourceCard'; import SourceCard from '../components/SourceCard';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
@@ -6,7 +10,6 @@ export default function Sources() {
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
setTitle('Sources'); setTitle('Sources');
const [sources, setSources] = useState<ISource[]>([]); const [sources, setSources] = useState<ISource[]>([]);
let mapped;
useEffect(() => { useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/source/list') fetch('http://127.0.0.1:4567/api/v1/source/list')
@@ -15,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>;
} }
+5
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
@@ -30,4 +34,5 @@ interface IChapter {
chapter_number: number chapter_number: number
scanlator: String scanlator: String
mangaId: number mangaId: number
pageCount: number
} }