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
mkdir -p repo/
# Get last commit message
last_commit_log=$(git log -1 --pretty=format:"%s")
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
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
fi
+3 -1
View File
@@ -1,5 +1,7 @@
# 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.
+9 -1
View File
@@ -5,9 +5,10 @@ plugins {
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
application
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 {
@@ -139,9 +140,16 @@ tasks {
tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
}
tasks.named("processResources") {
dependsOn(":webUI:copyBuild")
}
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
//import android.content.res.Configuration
//import android.support.multidex.MultiDex
//import timber.log.Timber
// import android.content.res.Configuration
// import android.support.multidex.MultiDex
// import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
@@ -2,19 +2,22 @@ package eu.kanade.tachiyomi
import android.app.Application
import com.google.gson.Gson
//import eu.kanade.tachiyomi.data.cache.ChapterCache
//import eu.kanade.tachiyomi.data.cache.CoverCache
//import eu.kanade.tachiyomi.data.database.DatabaseHelper
//import eu.kanade.tachiyomi.data.download.DownloadManager
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
//import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
//import eu.kanade.tachiyomi.data.track.TrackManager
//import eu.kanade.tachiyomi.extension.ExtensionManager
// import eu.kanade.tachiyomi.data.cache.ChapterCache
// import eu.kanade.tachiyomi.data.cache.CoverCache
// import eu.kanade.tachiyomi.data.database.DatabaseHelper
// import eu.kanade.tachiyomi.data.download.DownloadManager
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
// import eu.kanade.tachiyomi.data.track.TrackManager
// import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import rx.Observable
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 {
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
}
// rxAsync { get<DatabaseHelper>() }
}
private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
}
}
@@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.extension.api
//import android.content.Context
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import android.content.Context
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
//import kotlinx.coroutines.Dispatchers
//import kotlinx.coroutines.withContext
// import kotlinx.coroutines.Dispatchers
// import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
//import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
@@ -27,7 +27,7 @@ internal class ExtensionGithubApi {
// suspend fun checkForUpdates(): List<Extension.Installed> {
// val extensions = fin dExtensions()
//
//// preferences.lastExtCheck().set(Date().time)
// // preferences.lastExtCheck().set(Date().time)
//
// val installedExtensions = ExtensionLoader.loadExtensions(context)
// .filterIsInstance<LoadResult.Success>()
@@ -49,23 +49,23 @@ internal class ExtensionGithubApi {
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
fun getApkUrl(extension: Extension.Available): String {
@@ -9,7 +9,7 @@ import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
@@ -1,28 +1,22 @@
package eu.kanade.tachiyomi.extension.util
//import android.annotation.SuppressLint
//import android.content.Context
//import android.content.pm.PackageInfo
//import android.content.pm.PackageManager
//import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annoations.Nsfw
//import eu.kanade.tachiyomi.data.preference.PreferenceValues
//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 kotlinx.coroutines.async
//import kotlinx.coroutines.runBlocking
//import timber.log.Timber
//import uy.kohesive.injekt.injectLazy
// import android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
// import android.content.pm.PackageManager
// import dalvik.system.PathClassLoader
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import eu.kanade.tachiyomi.util.lang.Hash
// import kotlinx.coroutines.async
// import kotlinx.coroutines.runBlocking
// import timber.log.Timber
// import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
//@SuppressLint("PackageManagerGetSignatures")
// @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy()
@@ -1,30 +1,23 @@
package eu.kanade.tachiyomi.network
//import android.annotation.SuppressLint
//import android.content.Context
//import android.os.Build
//import android.os.Handler
//import android.os.Looper
//import android.webkit.WebSettings
//import android.webkit.WebView
//import android.widget.Toast
//import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
//import eu.kanade.tachiyomi.util.lang.launchUI
//import eu.kanade.tachiyomi.util.system.WebViewClientCompat
//import eu.kanade.tachiyomi.util.system.WebViewUtil
//import eu.kanade.tachiyomi.util.system.isOutdated
//import eu.kanade.tachiyomi.util.system.setDefaultSettings
//import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
// import android.annotation.SuppressLint
// import android.content.Context
// import android.os.Build
// import android.os.Handler
// import android.os.Looper
// import android.webkit.WebSettings
// import android.webkit.WebView
// import android.widget.Toast
// import eu.kanade.tachiyomi.R
// import eu.kanade.tachiyomi.util.lang.launchUI
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
// import eu.kanade.tachiyomi.util.system.WebViewUtil
// import eu.kanade.tachiyomi.util.system.isOutdated
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
// import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
//import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
// import uy.kohesive.injekt.injectLazy
class CloudflareInterceptor() : Interceptor {
@@ -77,7 +70,7 @@ class CloudflareInterceptor() : Interceptor {
// }
}
//
//// @SuppressLint("SetJavaScriptEnabled")
// // @SuppressLint("SetJavaScriptEnabled")
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
// // We need to lock this thread until the WebView finds the challenge solution url, because
// // OkHttp doesn't support asynchronous interceptors.
@@ -1,17 +1,14 @@
package eu.kanade.tachiyomi.network
//import android.content.Context
//import eu.kanade.tachiyomi.BuildConfig
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import android.content.Context
// import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import android.content.Context
import okhttp3.Cache
//import okhttp3.HttpUrl.Companion.toHttpUrl
// import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
//import okhttp3.dnsoverhttps.DnsOverHttps
//import okhttp3.logging.HttpLoggingInterceptor
//import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
// import okhttp3.dnsoverhttps.DnsOverHttps
// import okhttp3.logging.HttpLoggingInterceptor
// import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) {
@@ -1,18 +1,14 @@
package eu.kanade.tachiyomi.network
//import kotlinx.coroutines.suspendCancellableCoroutine
// import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
@@ -52,7 +48,7 @@ fun Call.asObservable(): Observable<Response> {
}
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
//suspend fun Call.await(assertSuccess: Boolean = false): Response {
// suspend fun Call.await(assertSuccess: Boolean = false): Response {
// return suspendCancellableCoroutine { continuation ->
// enqueue(
// object : Callback {
@@ -81,7 +77,7 @@ fun Call.asObservable(): Observable<Response> {
// }
// }
// }
//}
// }
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->
@@ -92,7 +88,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
}
}
//fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
// val progressClient = newBuilder()
// .cache(null)
// .addNetworkInterceptor { chain ->
@@ -104,7 +100,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
// .build()
//
// return progressClient.newCall(request)
//}
// }
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
@@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.source
//import androidx.preference.PreferenceScreen
// import androidx.preference.PreferenceScreen
interface ConfigurableSource : Source {
@@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.source
//import android.graphics.drawable.Drawable
//import eu.kanade.tachiyomi.extension.ExtensionManager
// import android.graphics.drawable.Drawable
// import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
//import uy.kohesive.injekt.Injekt
//import uy.kohesive.injekt.api.get
// import uy.kohesive.injekt.Injekt
// import uy.kohesive.injekt.api.get
/**
* A basic interface for creating a source. It could be an online source, a local source, etc...
@@ -46,6 +46,6 @@ interface Source {
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
}
//fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
//fun Source.getPreferenceKey(): String = "source_$id"
// fun Source.getPreferenceKey(): String = "source_$id"
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source
//import android.content.Context
//import eu.kanade.tachiyomi.R
// import android.content.Context
// import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
@@ -9,7 +9,7 @@ open class Page(
val url: String = "",
var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
): ProgressListener {
) : ProgressListener {
val number: Int
get() = index + 1
@@ -12,7 +12,7 @@ interface SChapter : Serializable {
var chapter_number: Float
var scanlator: String?
var scanlator: String?
fun copyFrom(other: SChapter) {
name = other.name
@@ -16,7 +16,7 @@ import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
//import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
@@ -3,6 +3,6 @@ package ir.armor.tachidesk
import net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
val extensionsRoot = "$dataRoot/extensions"
}
}
@@ -2,7 +2,18 @@ package ir.armor.tachidesk
import eu.kanade.tachiyomi.App
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.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
@@ -23,31 +34,32 @@ class Main {
@JvmStatic
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
applicationSetup()
registerConfigModules()
//Load config API
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
//Load Android compatibility dependencies
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
val app = Javalin.create { config ->
try {
this::class.java.classLoader.getResource("/react/index.html")
config.addStaticFiles("/react")
config.addSinglePageRoot("/","/react/index.html")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
}
}.start(4567)
app.before() { ctx ->
// allow the client which is running on another port
ctx.header("Access-Control-Allow-Origin", "*")
@@ -57,18 +69,22 @@ class Main {
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println(apkName)
ctx.status(
installAPK(apkName)
installAPK(apkName)
)
}
app.get("/api/v1/source/list") { ctx ->
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 ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
@@ -103,10 +119,11 @@ class Main {
}
// 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 searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceSearch(sourceId, searchTerm))
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
@@ -114,11 +131,6 @@ class Main {
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
}
}
}
@@ -25,4 +25,4 @@ fun makeDataBaseTables() {
SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable)
}
}
}
@@ -1,11 +1,11 @@
package ir.armor.tachidesk.database.dataclass
data class ChapterDataClass(
val id: Int,
val url: String,
val name: String,
val date_upload: Long,
val chapter_number: Float,
val scanlator: String?,
val mangaId: Int,
)
val id: Int,
val url: String,
val name: String,
val date_upload: Long,
val chapter_number: Float,
val scanlator: String?,
val mangaId: Int,
)
@@ -1,14 +1,14 @@
package ir.armor.tachidesk.database.dataclass
data class ExtensionDataClass(
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String,
val installed: Boolean,
val classFQName: String,
)
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String,
val installed: Boolean,
val classFQName: String,
)
@@ -3,18 +3,18 @@ package ir.armor.tachidesk.database.dataclass
import ir.armor.tachidesk.database.table.MangaStatus
data class MangaDataClass(
val id: Int,
val sourceId: Long,
val id: Int,
val sourceId: Long,
val url: String,
val title: String,
val thumbnail_url: String? = null,
val url: String,
val title: String,
val thumbnailUrl: String? = null,
val initialized: Boolean = false,
val initialized: Boolean = false,
val artist: String? = null,
val author: String? = null,
val description: String? = null,
val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name
)
val artist: String? = null,
val author: String? = null,
val description: String? = null,
val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name
)
@@ -1,6 +1,6 @@
package ir.armor.tachidesk.database.dataclass
data class PageDataClass(
val index: Int,
var imageUrl: String,
)
val index: Int,
var imageUrl: String,
)
@@ -1,9 +1,9 @@
package ir.armor.tachidesk.database.dataclass
data class SourceDataClass(
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
)
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
)
@@ -18,4 +18,4 @@ class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
var iconUrl by ExtensionsTable.iconUrl
var installed by ExtensionsTable.installed
var classFQName by ExtensionsTable.classFQName
}
}
@@ -20,4 +20,4 @@ class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
var thumbnail_url by MangaTable.thumbnail_url
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
}
}
@@ -1,7 +1,8 @@
package ir.armor.tachidesk.database.entity
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
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -13,4 +14,4 @@ class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
var extension by ExtensionEntity referencedOn SourceTable.extension
var partOfFactorySource by SourceTable.partOfFactorySource
var positionInFactorySource by SourceTable.positionInFactorySource
}
}
@@ -1,6 +1,5 @@
package ir.armor.tachidesk.database.table
import eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() {
@@ -8,7 +7,7 @@ object ChapterTable : IntIdTable() {
val name = varchar("name", 512)
val date_upload = long("date_upload").default(0)
val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator",128).nullable()
val scanlator = varchar("scanlator", 128).nullable()
val manga = reference("manga", MangaTable)
}
}
@@ -2,7 +2,6 @@ package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
@@ -15,4 +14,4 @@ object ExtensionsTable : IntIdTable() {
val installed = bool("installed").default(false)
val classFQName = varchar("class_name", 256).default("") // fully qualified name
}
}
@@ -30,4 +30,4 @@ enum class MangaStatus(val status: Int) {
companion object {
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
}
}
}
@@ -4,9 +4,9 @@ import org.jetbrains.exposed.dao.id.IdTable
object SourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name= varchar("name", 128)
val name = varchar("name", 128)
val lang = varchar("lang", 10)
val extension = reference("extension", ExtensionsTable)
val partOfFactorySource = bool("part_of_factory_source").default(false)
val positionInFactorySource = integer("position_in_factory_source").nullable()
}
}
@@ -41,7 +41,6 @@ fun installAPK(apkName: String): Int {
// download apk file
downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className)
// dex -> jar
@@ -60,7 +59,7 @@ fun installAPK(apkName: String): Int {
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
}
if (instance is HttpSource) {// single source
if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource
transaction {
// SourceEntity.new {
@@ -80,7 +79,6 @@ fun installAPK(apkName: String): Int {
// println(httpSource.name)
// println()
}
} else { // multi source
val sourceFactory = instance as SourceFactory
transaction {
@@ -110,7 +108,6 @@ fun installAPK(apkName: String): Int {
it[classFQName] = className
}
}
}
return 201 // we downloaded successfully
} else {
@@ -122,7 +119,7 @@ val networkHelper: NetworkHelper by injectLazy()
private fun downloadAPKFile(url: String, apkPath: String) {
val request = Request.Builder().url(url).build()
val response = networkHelper.client.newCall(request).execute()
val response = networkHelper.client.newCall(request).execute()
val downloadedFile = File(apkPath)
val sink = downloadedFile.sink().buffer()
@@ -6,23 +6,21 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
import ir.armor.tachidesk.database.dataclass.PageDataClass
import ir.armor.tachidesk.database.entity.MangaEntity
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId)
val chapterList = source.fetchChapterList(
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
).toBlocking().first()
return transaction {
@@ -41,22 +39,21 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
}
}
return@transaction chapterList.map {
ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url,
it.name,
it.date_upload,
it.chapter_number,
it.scanlator,
mangaId
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url,
it.name,
it.date_upload,
it.chapter_number,
it.scanlator,
mangaId
)
}
}
}
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
@@ -64,24 +61,35 @@ fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val pagesList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).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(
it.index,
getTrueImageUrl(it,source)
it.index,
getTrueImageUrl(it, source)
)
}
}
return@transaction Pair(chapter, pages)
}
}
fun getTrueImageUrl(page: Page, source: HttpSource): String {
return if ( page.imageUrl == null){
return if (page.imageUrl == null) {
source.fetchImageUrl(page).toBlocking().first()!!
} else page.imageUrl!!
}
@@ -67,18 +67,17 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
return transaction {
return@transaction ExtensionsTable.selectAll().map {
ExtensionDataClass(
it[ExtensionsTable.name],
it[ExtensionsTable.pkgName],
it[ExtensionsTable.versionName],
it[ExtensionsTable.versionCode],
it[ExtensionsTable.lang],
it[ExtensionsTable.isNsfw],
it[ExtensionsTable.apkName],
it[ExtensionsTable.iconUrl],
it[ExtensionsTable.installed],
it[ExtensionsTable.classFQName]
it[ExtensionsTable.name],
it[ExtensionsTable.pkgName],
it[ExtensionsTable.versionName],
it[ExtensionsTable.versionCode],
it[ExtensionsTable.lang],
it[ExtensionsTable.isNsfw],
it[ExtensionsTable.apkName],
it[ExtensionsTable.iconUrl],
it[ExtensionsTable.installed],
it[ExtensionsTable.classFQName]
)
}
}
}
@@ -14,28 +14,28 @@ fun getManga(mangaId: Int): MangaDataClass {
return@transaction if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true,
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
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]
}
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
// update database
@@ -56,23 +56,21 @@ fun getManga(mangaId: Int): MangaDataClass {
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true,
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
}
}
}
@@ -1,10 +1,9 @@
package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
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.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -19,6 +18,11 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
else
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 mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
@@ -41,21 +45,21 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
}
MangaDataClass(
mangaEntityId,
sourceId.toLong(),
mangaEntityId,
sourceId,
manga.url,
manga.title,
manga.thumbnail_url,
manga.url,
manga.title,
manga.thumbnail_url,
manga.initialized,
manga.initialized,
manga.artist,
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name,
manga.artist,
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name,
)
}
}
}
}
@@ -1,28 +1,27 @@
package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import ir.armor.tachidesk.database.dataclass.MangaDataClass
fun sourceFilters(sourceId: Long) {
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)
//source.fetchSearchManga()
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId)
}
fun sourceGlobalSearch(searchTerm: String) {
}
data class FilterWrapper(
val type: String,
val filter: Any
val type: String,
val filter: Any
)
//private fun FilterList.toItems(): List<FilterWrapper> {
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter ->
// when (filter) {
// is Filter.Header -> FilterWrapper("Header",filter)
@@ -56,4 +55,4 @@ data class FilterWrapper(
// }
// }
// }
//}
// }
@@ -13,7 +13,7 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.net.URL
import java.net.URLClassLoader
import java.util.*
import java.util.Locale
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>()
@@ -39,16 +39,16 @@ fun getHttpSource(sourceId: Long): HttpSource {
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false
val instance =
if (cachedExtensionPair != null) {
usedCached = true
println("Used cached Extension")
cachedExtensionPair.second
} else {
println("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance()
}
if (cachedExtensionPair != null) {
usedCached = true
println("Used cached Extension")
cachedExtensionPair.second
} else {
println("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance()
}
if (sourceRecord.partOfFactorySource) {
return@transaction if (usedCached) {
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
@@ -71,12 +71,26 @@ fun getSourceList(): List<SourceDataClass> {
return transaction {
return@transaction SourceTable.selectAll().map {
SourceDataClass(
it[SourceTable.id].value.toString(),
it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(it[SourceTable.id].value).supportsLatest
it[SourceTable.id].value.toString(),
it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(it[SourceTable.id].value).supportsLatest
)
}
}
}
}
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
return@transaction SourceDataClass(
source[SourceTable.id].value.toString(),
source[SourceTable.name],
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq source[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getHttpSource(source[SourceTable.id].value).supportsLatest
)
}
}
@@ -9,6 +9,5 @@ fun applicationSetup() {
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
makeDataBaseTables()
}
}
+35 -29
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
BrowserRouter as Router, Route, Switch,
} from 'react-router-dom';
@@ -9,39 +9,45 @@ import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList';
import Manga from './screens/Manga';
import Reader from './screens/Reader';
import Search from './screens/Search';
import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const contextValue = { title, setTitle };
return (
<Router>
<NavBar />
<NavBarTitle.Provider value={contextValue}>
<NavBar />
<Switch>
<Route path="/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>
<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>
</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 AppBar from '@material-ui/core/AppBar';
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 MenuIcon from '@material-ui/icons/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarTitle from '../context/NavbarTitle';
const useStyles = makeStyles((theme) => ({
root: {
@@ -22,6 +23,7 @@ const useStyles = makeStyles((theme) => ({
export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const { title } = useContext(NavBarTitle);
return (
<div className={classes.root}>
@@ -38,7 +40,7 @@ export default function NavBar() {
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Tachidesk
{title}
</Typography>
</Toolbar>
</AppBar>
+3 -2
View File
@@ -65,8 +65,9 @@ export default function SourceCard(props: IProps) {
</div>
</div>
<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}/popular/`; }}>Browse</Button>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</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>
</CardContent>
</Card>
@@ -48,14 +48,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" />
</ListItem>
</Link>
<Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link>
</Link> */}
</List>
</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 NavBarTitle from '../context/NavbarTitle';
export default function Extensions() {
let mapped;
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')
+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 ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails';
import NavBarTitle from '../context/NavbarTitle';
export default function Manga() {
const { id } = useParams<{id: string}>();
const { setTitle } = useContext(NavBarTitle);
const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]);
@@ -12,7 +14,10 @@ export default function Manga() {
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
.then((response) => response.json())
.then((data) => setManga(data));
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, []);
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 MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]);
const [lastPageNum] = useState<number>(1);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
.then((data: { name: string }) => setTitle(data.name));
}, []);
useEffect(() => {
const sourceType = props.popular ? 'popular' : 'latest';
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.json())
.then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas(
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })),
.then((data: IManga[]) => setMangas(
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 NavBarTitle from '../context/NavbarTitle';
const style = {
display: 'flex',
@@ -14,14 +15,24 @@ interface IPage {
imageUrl: string
}
interface IData {
first: IChapter
second: IPage[]
}
export default function Reader() {
const { setTitle } = useContext(NavBarTitle);
const [pages, setPages] = useState<IPage[]>([]);
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.json())
.then((data) => setPages(data));
.then((data:IData) => {
setTitle(data.first.name);
setPages(data.second);
});
}, []);
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 NavBarTitle from '../context/NavbarTitle';
export default function Sources() {
let mapped;
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')