Compare commits

..

25 Commits

Author SHA1 Message Date
Aria Moradi d1cd2cfc8c [RELEASE CI] bump version 2021-01-29 15:26:08 +03:30
Aria Moradi 832c224ed4 uninstalling extensions implemented 2021-01-29 15:23:29 +03:30
Aria Moradi 99316f4bd5 revert react changes 2021-01-29 14:25:18 +03:30
Aria Moradi 9caae5f1e5 thumbnail caching 2021-01-29 14:19:24 +03:30
Aria Moradi 345be95ce9 [RELEASE CI] bump version 2021-01-28 15:13:56 +03:30
Aria Moradi 6fe68841b7 [SKIP CI] add docker thanks to @arbuilder 2021-01-28 15:08:37 +03:30
Aria Moradi eaff2c15a9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-26 23:33:14 +03:30
Aria Moradi 5eb8dc66a8 add license notice to everything 2021-01-26 23:32:12 +03:30
Aria Moradi 49715c81e4 Update README.md 2021-01-26 23:11:20 +03:30
Aria Moradi 3398409555 Update README.md 2021-01-26 23:07:54 +03:30
Aria Moradi f05aa0589a [SKIP CI] add apache 2 license link 2021-01-26 23:02:04 +03:30
Aria Moradi fbc71ce781 fix nav buttons 2021-01-23 02:57:32 +03:30
Aria Moradi ca9c671886 more css hacks: scroll bar 2021-01-23 02:53:00 +03:30
Aria Moradi bd109ba11f some css hacks 2021-01-23 02:32:18 +03:30
Aria Moradi 0ff770a98b fluid manga grid 2021-01-23 01:58:18 +03:30
Aria Moradi ed7bb408a3 fix light theme AppBar 2021-01-23 01:36:56 +03:30
Aria Moradi 84676b9156 remove some wierdness 2021-01-23 01:33:12 +03:30
Aria Moradi dcdd50ffe1 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-23 01:25:49 +03:30
Aria Moradi afb21c59f0 DarkTheme! my eyes can rest now :) 2021-01-23 01:20:16 +03:30
Aria Moradi e219179519 Update README.md 2021-01-23 00:28:32 +03:30
Aria Moradi 15a2115c5a Update README.md 2021-01-23 00:26:43 +03:30
Aria Moradi 94c6f33925 Update README.md 2021-01-23 00:23:40 +03:30
Aria Moradi 202e38871d [RELEASE CI] hotfix 2021-01-22 21:33:54 +03:30
Aria Moradi 3f75b84651 fix button text 2021-01-22 21:33:12 +03:30
Aria Moradi f171b785a0 [RELEASE CI] fix linting error 2021-01-22 21:29:09 +03:30
47 changed files with 652 additions and 158 deletions
+20 -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.
## 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,26 @@ How to do it is described in `webUI/react/README.md` but for short,
and supports HMR and all the other goodies you'll need.
## Is this application usable? Should I test it?
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented.
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Credit
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`.
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
You can obtain a copy of the license from http://www.apache.org/licenses/LICENSE-2.0
## License
Copyright (C) 2020-2021 Aria Moradi and contributors
+1 -1
View File
@@ -8,7 +8,7 @@ plugins {
id("org.jmailen.kotlinter") version "3.3.0"
}
val TachideskVersion = "v0.1.0"
val TachideskVersion = "v0.1.3"
repositories {
@@ -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;
@@ -27,6 +27,8 @@ class NetworkHelper(context: Context) {
// .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) {
@@ -1,8 +1,13 @@
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"
}
@@ -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 eu.kanade.tachiyomi.App
import io.javalin.Javalin
import ir.armor.tachidesk.util.applicationSetup
@@ -7,10 +11,13 @@ import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getMangaUpdateQueueThread
import ir.armor.tachidesk.util.getPages
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
@@ -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())
}
@@ -112,6 +130,15 @@ class Main {
ctx.json(getPages(chapterId, mangaId))
}
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
println("got request for: $mangaId")
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
@@ -1,5 +1,9 @@
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
@@ -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,
@@ -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(
@@ -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.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@@ -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,30 @@
package ir.armor.tachidesk.util
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
fun writeStream(fileStream: InputStream, path: String) {
Files.newOutputStream(Paths.get(path)).use { os ->
val buffer = ByteArray(1024)
var len: Int
while (fileStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
}
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
}
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName))
return "$directoryPath/${file.name}"
}
return null
}
@@ -1,76 +1,171 @@
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
import java.util.concurrent.ArrayBlockingQueue
fun getManga(mangaId: Int): MangaDataClass {
return transaction {
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val getMangaUpdateQueue = ArrayBlockingQueue<Pair<Int, SManga?>>(1000)
@Volatile
var getMangaCount = 0
return@transaction if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
val getMangaUpdateQueueThread = Runnable {
while (true) {
val p = getMangaUpdateQueue.take()
println("took ${p.first}")
while (getMangaCount > 0) {
println("count is $getMangaCount")
Thread.sleep(1000)
}
val mangaId = p.first
println("working on $mangaId")
val fetchedManga = p.second!!
try {
transaction {
println("transaction start $mangaId")
MangaTable.update({ MangaTable.id eq mangaId }) {
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
it[MangaTable.initialized] = 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]
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
}
).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
println("transaction end $mangaId")
}
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} catch (e: Exception) {
println(e)
}
}
}
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
synchronized(getMangaCount) {
getMangaCount++
}
return try {
transaction {
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
return@transaction if (mangaEntry[MangaTable.initialized]) {
println("${mangaEntry[MangaTable.title]} is initialized")
println("${mangaEntry[MangaTable.thumbnail_url]}")
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
// update database
// TODO: sqlite gets fucked here
println("putting $mangaId")
getMangaUpdateQueue.put(Pair(mangaId, fetchedManga))
// mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val newThumbnail =
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) {
fetchedManga.thumbnail_url
} else mangaEntry[MangaTable.thumbnail_url]
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
)
}
}
} finally {
synchronized(getMangaCount) {
getMangaCount--
}
}
}
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
return transaction {
var filePath = Config.thumbnailsRoot + "/$mangaId"
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
if (potentialCache != null) {
println("using cached thumbnail file")
return@transaction Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter("$mangaId.")}"
)
}
val sourceId = mangaEntry[MangaTable.sourceReference].value
println("getting source for $mangaId")
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
println(thumbnailUrl)
val response = source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
println(response.code)
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += "." + contentType.substringAfter("image/")
writeStream(response.body!!.byteStream(), filePath)
return@transaction Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
}
@@ -1,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.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
@@ -9,6 +13,10 @@ import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
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) {
@@ -27,8 +35,8 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
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
@@ -37,30 +45,46 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
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(
@@ -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.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) {
@@ -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()
}
@@ -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
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 {
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';
+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 { 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>
);
+24 -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 { 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: {
@@ -38,8 +43,6 @@ const useStyles = makeStyles({
interface IProps {
manga: IManga
// eslint-disable-next-line react/no-unused-prop-types, react/require-default-props
// ref?: false | React.MutableRefObject<HTMLInputElement | undefined>
}
const MangaCard = React.forwardRef((props: IProps, ref) => {
const {
@@ -50,23 +53,25 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
const classes = useStyles();
return (
<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 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>
);
});
@@ -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{
+7 -3
View File
@@ -1,4 +1,9 @@
/* 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{
@@ -41,12 +46,11 @@ export default function MangaGrid(props: IProps) {
return <MangaCard manga={it} />;
});
}
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
{mapped}
</div>
</Grid>
);
}
+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 { 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';
+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';
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 {
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 ReactDOM from 'react-dom';
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" />
+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';
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 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} />)}</>;
}
+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';
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 { useParams } from 'react-router-dom';
import ChapterCard from '../components/ChapterCard';
+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, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid';
+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, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import NavBarTitle from '../context/NavbarTitle';
+5 -1
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 { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
@@ -82,7 +86,7 @@ export default function SearchSingle() {
<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}
+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 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} />)}</>;
}
+4
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
interface IExtension {
name: string
lang: string