Compare commits

...

7 Commits

Author SHA1 Message Date
Aria Moradi 2c76ad9b74 [RELEASE CI] Search implemented, other improvements 2021-01-22 18:14:59 +03:30
Aria Moradi 7d1c63e181 single source search done 2021-01-22 18:07:31 +03:30
Aria Moradi 6401b946b6 add page title 2021-01-22 17:00:33 +03:30
Aria Moradi 9a61f58043 add kotlinter 2021-01-22 12:34:03 +03:30
Aria Moradi b854fdeadb fix matching 2021-01-22 11:50:33 +03:30
Aria Moradi 6eb4f1ba88 Update README.md 2021-01-22 11:25:11 +03:30
Aria Moradi ded5e3a73a Update README.md 2021-01-22 11:13:25 +03:30
52 changed files with 498 additions and 394 deletions
+3 -4
View File
@@ -1,14 +1,13 @@
#!/bin/bash #!/bin/bash
mkdir -p repo/
# Get last commit message # Get last commit message
last_commit_log=$(git log -1 --pretty=format:"%s") last_commit_log=$(git log -1 --pretty=format:"%s")
echo "last commit log: $last_commit_log" echo "last commit log: $last_commit_log"
filter_count=$(echo "$last_commit_log" | grep -c "[RELEASE CI]" ) filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
echo "count is: $filter_count"
if [ "$filter_count" -gt 0 ]; then if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/ cp server/build/Tachidesk-*.jar repo/
fi fi
+3 -1
View File
@@ -1,5 +1,7 @@
# Tachidesk # Tachidesk
A free and open source manga reader than runs extensions built for [Tachiyomi](https://tachiyomi.org/) which runs on desktop operating systems. A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
+9 -1
View File
@@ -5,9 +5,10 @@ plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21" // id("org.jetbrains.kotlin.jvm") version "1.4.21"
application application
id("com.github.johnrengelman.shadow") version "6.1.0" id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
} }
val TachideskVersion = "v0.0.2" val TachideskVersion = "v0.0.3"
repositories { repositories {
@@ -139,9 +140,16 @@ tasks {
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build") destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
} }
tasks.named("processResources") { tasks.named("processResources") {
dependsOn(":webUI:copyBuild") dependsOn(":webUI:copyBuild")
} }
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -11,10 +11,13 @@ import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.track.TrackManager // import eu.kanade.tachiyomi.data.track.TrackManager
// import eu.kanade.tachiyomi.extension.ExtensionManager // import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import rx.Observable import rx.Observable
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.* import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
} }
// rxAsync { get<DatabaseHelper>() } // rxAsync { get<DatabaseHelper>() }
} }
private fun rxAsync(block: () -> Unit) { private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe() Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
} }
} }
@@ -5,14 +5,8 @@ package eu.kanade.tachiyomi.extension.util
// import android.content.pm.PackageInfo // import android.content.pm.PackageInfo
// import android.content.pm.PackageManager // import android.content.pm.PackageManager
// import dalvik.system.PathClassLoader // import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw
// import eu.kanade.tachiyomi.data.preference.PreferenceValues // import eu.kanade.tachiyomi.data.preference.PreferenceValues
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
// import eu.kanade.tachiyomi.util.lang.Hash // import eu.kanade.tachiyomi.util.lang.Hash
// import kotlinx.coroutines.async // import kotlinx.coroutines.async
// import kotlinx.coroutines.runBlocking // import kotlinx.coroutines.runBlocking
@@ -9,22 +9,15 @@ package eu.kanade.tachiyomi.network
// import android.webkit.WebView // import android.webkit.WebView
// import android.widget.Toast // import android.widget.Toast
// import eu.kanade.tachiyomi.R // import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
// import eu.kanade.tachiyomi.util.lang.launchUI // import eu.kanade.tachiyomi.util.lang.launchUI
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat // import eu.kanade.tachiyomi.util.system.WebViewClientCompat
// import eu.kanade.tachiyomi.util.system.WebViewUtil // import eu.kanade.tachiyomi.util.system.WebViewUtil
// import eu.kanade.tachiyomi.util.system.isOutdated // import eu.kanade.tachiyomi.util.system.isOutdated
// import eu.kanade.tachiyomi.util.system.setDefaultSettings // import eu.kanade.tachiyomi.util.system.setDefaultSettings
// import eu.kanade.tachiyomi.util.system.toast // import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
// import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CloudflareInterceptor() : Interceptor { class CloudflareInterceptor() : Interceptor {
@@ -4,14 +4,11 @@ package eu.kanade.tachiyomi.network
// import eu.kanade.tachiyomi.BuildConfig // import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import android.content.Context import android.content.Context
import okhttp3.Cache
// import okhttp3.HttpUrl.Companion.toHttpUrl // import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
// import okhttp3.dnsoverhttps.DnsOverHttps // import okhttp3.dnsoverhttps.DnsOverHttps
// import okhttp3.logging.HttpLoggingInterceptor // import okhttp3.logging.HttpLoggingInterceptor
// import uy.kohesive.injekt.injectLazy // import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
@@ -2,17 +2,13 @@ package eu.kanade.tachiyomi.network
// import kotlinx.coroutines.suspendCancellableCoroutine // import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> { fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber -> return Observable.unsafeCreate { subscriber ->
@@ -2,7 +2,18 @@ package ir.armor.tachidesk
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import io.javalin.Javalin import io.javalin.Javalin
import ir.armor.tachidesk.util.* import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPages
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompat
@@ -23,6 +34,10 @@ class Main {
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
// System.getProperties()["proxySet"] = "true"
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
// System.getProperties()["socksProxyPort"] = "2020"
// make sure everything we need exists // make sure everything we need exists
applicationSetup() applicationSetup()
@@ -35,7 +50,6 @@ class Main {
// start app // start app
androidCompat.startApp(App()) androidCompat.startApp(App())
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")
@@ -46,8 +60,6 @@ class Main {
} }
}.start(4567) }.start(4567)
app.before() { ctx -> app.before() { ctx ->
// allow the client which is running on another port // allow the client which is running on another port
ctx.header("Access-Control-Allow-Origin", "*") ctx.header("Access-Control-Allow-Origin", "*")
@@ -57,7 +69,6 @@ class Main {
ctx.json(getExtensionList()) ctx.json(getExtensionList())
} }
app.get("/api/v1/extension/install/:apkName") { ctx -> app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName") val apkName = ctx.pathParam("apkName")
println(apkName) println(apkName)
@@ -69,6 +80,11 @@ class Main {
ctx.json(getSourceList()) ctx.json(getSourceList())
} }
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt() val pageNum = ctx.pathParam("pageNum").toInt()
@@ -103,10 +119,11 @@ class Main {
} }
// single source search // single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm") { ctx -> app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm") val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceSearch(sourceId, searchTerm)) val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
} }
// source filter list // source filter list
@@ -114,11 +131,6 @@ class Main {
val sourceId = ctx.pathParam("sourceId").toLong() val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId)) ctx.json(sourceFilters(sourceId))
} }
} }
} }
} }
@@ -8,7 +8,7 @@ data class MangaDataClass(
val url: String, val url: String,
val title: String, val title: String,
val thumbnail_url: String? = null, val thumbnailUrl: String? = null,
val initialized: Boolean = false, val initialized: Boolean = false,
@@ -1,7 +1,8 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.dao.EntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
class SourceEntity(id: EntityID<Long>) : LongEntity(id) { class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -1,6 +1,5 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
import eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
@@ -2,7 +2,6 @@ package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() { object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128) val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128) val pkgName = varchar("pkg_name", 128)
@@ -41,7 +41,6 @@ fun installAPK(apkName: String): Int {
// download apk file // download apk file
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className) println(className)
// dex -> jar // dex -> jar
@@ -80,7 +79,6 @@ fun installAPK(apkName: String): Int {
// println(httpSource.name) // println(httpSource.name)
// println() // println()
} }
} else { // multi source } else { // multi source
val sourceFactory = instance as SourceFactory val sourceFactory = instance as SourceFactory
transaction { transaction {
@@ -110,7 +108,6 @@ fun installAPK(apkName: String): Int {
it[classFQName] = className it[classFQName] = className
} }
} }
} }
return 201 // we downloaded successfully return 201 // we downloaded successfully
} else { } else {
@@ -6,14 +6,12 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.dataclass.ChapterDataClass import ir.armor.tachidesk.database.dataclass.ChapterDataClass
import ir.armor.tachidesk.database.dataclass.PageDataClass import ir.armor.tachidesk.database.dataclass.PageDataClass
import ir.armor.tachidesk.database.entity.MangaEntity
import ir.armor.tachidesk.database.table.ChapterTable import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
fun getChapterList(mangaId: Int): List<ChapterDataClass> { fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId) val source = getHttpSource(mangaDetails.sourceId)
@@ -41,7 +39,6 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
} }
return@transaction chapterList.map { return@transaction chapterList.map {
ChapterDataClass( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
@@ -56,7 +53,7 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
} }
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> { fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
return transaction { return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
@@ -70,14 +67,25 @@ fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
} }
).toBlocking().first() ).toBlocking().first()
return@transaction pagesList.map { val chapter = ChapterDataClass(
chapterEntry[ChapterTable.id].value,
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId
)
val pages = pagesList.map {
PageDataClass( PageDataClass(
it.index, it.index,
getTrueImageUrl(it, source) getTrueImageUrl(it, source)
) )
} }
}
return@transaction Pair(chapter, pages)
}
} }
fun getTrueImageUrl(page: Page, source: HttpSource): String { fun getTrueImageUrl(page: Page, source: HttpSource): String {
@@ -79,6 +79,5 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
it[ExtensionsTable.classFQName] it[ExtensionsTable.classFQName]
) )
} }
} }
} }
@@ -59,7 +59,6 @@ fun getManga(mangaId: Int): 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], mangaEntry[MangaTable.thumbnail_url],
@@ -75,4 +74,3 @@ fun getManga(mangaId: Int): MangaDataClass {
} }
} }
} }
@@ -1,10 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
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.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -19,6 +18,11 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
else else
throw Exception("Source $source doesn't support latest") throw Exception("Source $source doesn't support latest")
} }
return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> {
val mangasPage = this
return transaction { return 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()
@@ -42,7 +46,7 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
MangaDataClass( MangaDataClass(
mangaEntityId, mangaEntityId,
sourceId.toLong(), sourceId,
manga.url, manga.url,
manga.title, manga.title,
@@ -1,20 +1,19 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Filter import ir.armor.tachidesk.database.dataclass.MangaDataClass
import eu.kanade.tachiyomi.source.model.FilterList
fun sourceFilters(sourceId: Long) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
// source.getFilterList().toItems() // source.getFilterList().toItems()
} }
fun sourceSearch(sourceId: Long, searchTerm: String) { fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): List<MangaDataClass> {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
//source.fetchSearchManga() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId)
} }
fun sourceGlobalSearch(searchTerm: String) { fun sourceGlobalSearch(searchTerm: String) {
} }
data class FilterWrapper( data class FilterWrapper(
@@ -22,7 +21,7 @@ data class FilterWrapper(
val filter: Any val filter: Any
) )
//private fun FilterList.toItems(): List<FilterWrapper> { // private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter -> // return mapNotNull { filter ->
// when (filter) { // when (filter) {
// is Filter.Header -> FilterWrapper("Header",filter) // is Filter.Header -> FilterWrapper("Header",filter)
@@ -13,7 +13,7 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.* import java.util.Locale
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>() private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>() private val extensionCache = mutableListOf<Pair<String, Any>>()
@@ -80,3 +80,17 @@ fun getSourceList(): List<SourceDataClass> {
} }
} }
} }
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
return@transaction SourceDataClass(
source[SourceTable.id].value.toString(),
source[SourceTable.name],
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq source[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(source[SourceTable.id].value).supportsLatest
)
}
}
@@ -9,6 +9,5 @@ fun applicationSetup() {
File(Config.dataRoot).mkdirs() File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs() File(Config.extensionsRoot).mkdirs()
makeDataBaseTables() makeDataBaseTables()
} }
+9 -3
View File
@@ -1,4 +1,4 @@
import React 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';
@@ -9,15 +9,20 @@ import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import MangaList from './screens/MangaList';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/Search'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const contextValue = { title, setTitle };
return ( return (
<Router> <Router>
<NavBarTitle.Provider value={contextValue}>
<NavBar /> <NavBar />
<Switch> <Switch>
<Route path="/search"> <Route path="/sources/:sourceId/search/">
<Search /> <Search />
</Route> </Route>
<Route path="/extensions"> <Route path="/extensions">
@@ -42,6 +47,7 @@ export default function App() {
<Home /> <Home />
</Route> </Route>
</Switch> </Switch>
</NavBarTitle.Provider>
</Router> </Router>
); );
} }
+4 -2
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
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';
@@ -6,6 +6,7 @@ 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 TemporaryDrawer from './TemporaryDrawer'; import TemporaryDrawer from './TemporaryDrawer';
import NavBarTitle from '../context/NavbarTitle';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -22,6 +23,7 @@ const useStyles = makeStyles((theme) => ({
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 { title } = useContext(NavBarTitle);
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -38,7 +40,7 @@ export default function NavBar() {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" className={classes.title}> <Typography variant="h6" className={classes.title}>
Tachidesk {title}
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
+3 -2
View File
@@ -65,8 +65,9 @@ export default function SourceCard(props: IProps) {
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/latest/`; }}>Latest</Button>} <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/popular/`; }}>Browse</Button> {supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -48,14 +48,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" /> <ListItemText primary="Sources" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}> {/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search"> <ListItem button key="Search">
<ListItemIcon> <ListItemIcon>
<InboxIcon /> <InboxIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Global Search" /> <ListItemText primary="Global Search" />
</ListItem> </ListItem>
</Link> </Link> */}
</List> </List>
</div> </div>
); );
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
type ContextType = {
title: string
setTitle: React.Dispatch<React.SetStateAction<string>>
};
const NavBarTitle = React.createContext<ContextType>({
title: 'Tachidesk',
setTitle: ():void => {},
});
export default NavBarTitle;
+5 -2
View File
@@ -1,9 +1,12 @@
import React, { 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';
export default function Extensions() { export default function Extensions() {
let mapped; const { setTitle } = useContext(NavBarTitle);
setTitle('Extensions');
const [extensions, setExtensions] = useState<IExtension[]>([]); const [extensions, setExtensions] = useState<IExtension[]>([]);
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')
+7 -2
View File
@@ -1,10 +1,12 @@
import React, { useEffect, useState } 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';
import MangaDetails from '../components/MangaDetails'; import MangaDetails from '../components/MangaDetails';
import NavBarTitle from '../context/NavbarTitle';
export default function Manga() { export default function Manga() {
const { id } = useParams<{id: string}>(); const { id } = useParams<{id: string}>();
const { setTitle } = useContext(NavBarTitle);
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
@@ -12,7 +14,10 @@ export default function Manga() {
useEffect(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`) fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setManga(data)); .then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, []); }, []);
useEffect(() => { useEffect(() => {
+11 -3
View File
@@ -1,18 +1,26 @@
import React, { 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';
import NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) { export default function MangaList(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [lastPageNum] = useState<number>(1); const [lastPageNum] = useState<number>(1);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
.then((data: { name: string }) => setTitle(data.name));
}, []);
useEffect(() => { useEffect(() => {
const sourceType = props.popular ? 'popular' : 'latest'; const sourceType = props.popular ? 'popular' : 'latest';
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas( .then((data: IManga[]) => setMangas(
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })), data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id })),
)); ));
}, []); }, []);
+13 -2
View File
@@ -1,5 +1,6 @@
import React, { 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';
const style = { const style = {
display: 'flex', display: 'flex',
@@ -14,14 +15,24 @@ interface IPage {
imageUrl: string imageUrl: string
} }
interface IData {
first: IChapter
second: IPage[]
}
export default function Reader() { export default function Reader() {
const { setTitle } = useContext(NavBarTitle);
const [pages, setPages] = useState<IPage[]>([]); const [pages, setPages] = useState<IPage[]>([]);
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>(); const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
useEffect(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`) fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => setPages(data)); .then((data:IData) => {
setTitle(data.first.name);
setPages(data.second);
});
}, []); }, []);
pages.sort((a, b) => (a.index - b.index)); pages.sort((a, b) => (a.index - b.index));
-48
View File
@@ -1,48 +0,0 @@
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import MangaGrid from '../components/MangaGrid';
const useStyles = makeStyles((theme) => ({
root: {
TextField: {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
export default function Search() {
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>('');
const textInput = React.createRef<HTMLInputElement>();
function doSearch() {
if (textInput.current) {
const { value } = textInput.current;
if (value === '') { setError(true); } else {
setError(false);
setMangas([]);
setMessage('button pressed');
}
}
}
const mangaGrid = <MangaGrid mangas={mangas} message={message} />;
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => doSearch()}>
Primary
</Button>
</form>
{mangaGrid}
</>
);
}
+81
View File
@@ -0,0 +1,81 @@
import React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
const useStyles = makeStyles((theme) => ({
root: {
TextField: {
margin: theme.spacing(1),
width: '25ch',
},
},
}));
export default function SearchSingle() {
const { setTitle } = useContext(NavBarTitle);
const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>('');
const [lastPageNum] = useState<number>(1);
const [searchTerm, setSearchTerm] = useState<string>('');
const textInput = React.createRef<HTMLInputElement>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
}, []);
function processInput() {
if (textInput.current) {
const { value } = textInput.current;
if (value === '') {
setError(true);
setMessage('Type something to search');
} else {
setError(false);
setSearchTerm(value);
setMessage('');
}
}
}
useEffect(() => {
if (searchTerm.length > 0) {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.json())
.then((data: IManga[]) => {
if (data.length > 0) {
setMangas(
data.map((it) => (
{ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id }
)),
);
} else {
setMessage('search qeury returned nothing.');
}
});
}
}, [searchTerm]);
const mangaGrid = <MangaGrid mangas={mangas} message={message} />;
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => processInput()}>
Primary
</Button>
</form>
{mangaGrid}
</>
);
}
+5 -2
View File
@@ -1,9 +1,12 @@
import React, { 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';
export default function Sources() { export default function Sources() {
let mapped; const { setTitle } = useContext(NavBarTitle);
setTitle('Sources');
const [sources, setSources] = useState<ISource[]>([]); const [sources, setSources] = useState<ISource[]>([]);
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')