Compare commits

..

54 Commits

Author SHA1 Message Date
Aria Moradi
202e38871d [RELEASE CI] hotfix 2021-01-22 21:33:54 +03:30
Aria Moradi
3f75b84651 fix button text 2021-01-22 21:33:12 +03:30
Aria Moradi
f171b785a0 [RELEASE CI] fix linting error 2021-01-22 21:29:09 +03:30
Aria Moradi
088dd6a856 [RELEASE CI] Milestone: The application is considered usable. 2021-01-22 21:18:14 +03:30
Aria Moradi
6318628ea2 [RELEASE CI] bump version 2021-01-22 21:15:14 +03:30
Aria Moradi
0757ea5d0d implemented infinite scroll 2021-01-22 21:11:00 +03:30
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
Aria Moradi
8d9f5b0bd9 [RELEASE CI] bug fixes, half-baked search 2021-01-22 02:39:24 +03:30
Aria Moradi
5cbb2a0481 fix SPA urls 2021-01-22 02:33:41 +03:30
Aria Moradi
a3b17365b7 [RELEASE CI] fix build issues 2021-01-22 01:55:41 +03:30
Aria Moradi
2847554b8f [RELEASE CI] test release 2021-01-22 01:47:28 +03:30
Aria Moradi
fb166eadd4 Update README.md 2021-01-22 01:45:14 +03:30
Aria Moradi
046c11f785 Update README.md 2021-01-22 01:40:47 +03:30
Aria Moradi
eac7436b18 clean up bulid file name 2021-01-22 01:37:24 +03:30
Aria Moradi
1a23804e51 only release when told to :D 2021-01-22 00:40:05 +03:30
Aria Moradi
08be142858 Update README.md 2021-01-22 00:36:00 +03:30
Aria Moradi
f421dbfe69 Update README.md 2021-01-22 00:34:49 +03:30
Aria Moradi
0d7ad65dbe Update README.md 2021-01-22 00:27:16 +03:30
Aria Moradi
9e946406bc Update README.md 2021-01-22 00:25:47 +03:30
Aria Moradi
9142d46fae Update README.md 2021-01-22 00:23:53 +03:30
Aria Moradi
39ed134f96 improve builds 2021-01-21 23:04:47 +03:30
Aria Moradi
7e4b495398 dummy file to trigger gh actions 2021-01-21 22:53:00 +03:30
Aria Moradi
c12242b760 rm package-lock in favor of yarn 2021-01-21 22:43:17 +03:30
Aria Moradi
8b2fd28a54 remove shit, add shit to improve performance 2021-01-21 22:39:22 +03:30
Aria Moradi
466f21a7e8 set -e is the poison 2021-01-21 22:27:03 +03:30
Aria Moradi
26a7e2f1cd fix again? 2021-01-21 16:27:27 +03:30
Aria Moradi
c155d78d52 fix build? 2021-01-21 16:20:22 +03:30
Aria Moradi
687dad5fc0 new scripts 2021-01-21 16:01:55 +03:30
Aria Moradi
33c4cdbc48 remove dummyFile 2021-01-21 15:38:20 +03:30
Aria Moradi
e46cb9738f add latest revision 2021-01-21 15:35:14 +03:30
Aria Moradi
aef37fcc9e add revision 2021-01-21 15:34:21 +03:30
Aria Moradi
ac51f40a91 dummy file to trigger gh actions 2021-01-21 15:14:00 +03:30
Aria Moradi
cd82af2d76 fix yarn build task 2021-01-21 15:04:11 +03:30
Aria Moradi
790040cd68 add stacktrace 2021-01-21 14:41:45 +03:30
Aria Moradi
95465ec265 update getAndroid 2021-01-21 14:33:02 +03:30
Aria Moradi
5313d91bf2 remove unfinished code for now 2021-01-21 14:29:13 +03:30
Aria Moradi
63783984c6 add getAndroid to workflow 2021-01-21 14:23:22 +03:30
Aria Moradi
97b9b1b6c9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-21 14:15:34 +03:30
Aria Moradi
34a7c24e0b add build workflow 2021-01-21 14:15:05 +03:30
Aria Moradi
12765a771f Update README.md 2021-01-21 13:58:49 +03:30
Aria Moradi
39850c71b0 Update README.md 2021-01-21 13:58:15 +03:30
Aria Moradi
9e43645a67 Update README.md 2021-01-21 13:42:17 +03:30
Aria Moradi
7dc7f4d905 Update README.md 2021-01-21 13:41:03 +03:30
Aria Moradi
c537c1bf29 manga search UI done 2021-01-20 15:26:52 +03:30
Aria Moradi
5f3ddbd1b2 Update README.md 2021-01-20 12:24:47 +03:30
Aria Moradi
f297e3790c Update README.md 2021-01-20 03:42:01 +03:30
Aria Moradi
ef3532357f Update README.md 2021-01-20 03:41:22 +03:30
Aria Moradi
48f29edf6c add dummy file 2021-01-20 03:30:20 +03:30
60 changed files with 852 additions and 369 deletions

View File

@@ -0,0 +1,7 @@
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=5
org.gradle.parallel=true
kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process

18
.github/scripts/commit-repo.sh vendored Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
cp ../master/repo/* .
new_build=$(ls | tail -1)
echo "New build file name: $new_build"
cp -f $new_build Tachidesk-latest.jar
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update repo"
git push
else
echo "No changes to commit"
fi

13
.github/scripts/create-repo.sh vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# 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\]' )
echo "count is: $filter_count"
if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
fi

84
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: CI
on:
push:
branches:
- master
pull_request:
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
path: master
fetch-depth: 0
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
cd master
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download and process android.jar
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
run: |
cd master
./scripts/getAndroid.sh
- name: Build the Jar
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
- name: Create repo artifacts
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
run: |
cd master
./.github/scripts/create-repo.sh
- name: Checkout repo branch
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
uses: actions/checkout@v2
with:
ref: repo
path: repo
- name: Deploy repo
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
run: |
cd repo
../master/.github/scripts/commit-repo.sh

View File

@@ -1,33 +1,52 @@
# Tachidesk # Tachidesk
A not so much port of [Tachiyomi](https://tachiyomi.org/) to the web (and later Electron for the desktop experience)! 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.
## How does it work?
This project has two components: This project has two components:
1. **server:** contains some of the original Tachiyomi code and serves a REST API 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 project that works with the server to do the presentation 2. **webUI:** A react SPA project that works with the server to do the presentation.
## How do I run the thing? ## How do I run the thing?
### Get Android stubs jar(do this only once) #### 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).
Double click on the jar file or run `java -jar Tachidesk-latest.jar` or `java -jar Tachidesk-vX.Y.Z-rxxx.jar`
The server will be running on `http://localhost:4567` open this url in your browser.
## Building from source
### Get Android stubs jar
#### Manual download #### Manual download
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`. Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work) #### Building from source(needs `bash`, `curl`, `base64`, `zip` to work)
run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository. Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
### building the jar ### building the jar
run `./gradlew :server:fatJar` the resulting jar file will be `server/build/server-1.0-all.jar`. Simply double click on it or run `java -jar server-1.0-all.jar`. The server will be running on `http://localhost:4567` open this url in your browser. Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
## running for development purposes ## Running for development purposes
### The Server ### `server` module
run `./gradlew :server:run -x :webUI:yarn_build --stacktrace` to run the server Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### the webUI ### `webUI` module
how to do it is described in `webUI/react/README.md` but for short, How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once) first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
then `yarn start` to start the client if a new browser window doesn't start automatically, then `yarn start` to start the client if a new browser window doesn't start automatically,
then open `http://127.0.0.1:3000` in a modern browser. then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need.
## Is the application usable? Should I test it? ## Is this application usable? Should I test it?
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented. Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented.
## 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`.
## License ## License
Copyright (C) 2020 Aria Moradi Copyright (C) 2020-2021 Aria Moradi and contributors
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this

View File

@@ -58,9 +58,8 @@ function dedup() {
pushd .. pushd ..
dedup AndroidCompat/src/main/java dedup AndroidCompat/src/main/java
dedup TachiServer/src/main/java dedup server/src/main/java
dedup Tachiyomi-App/src/main/java dedup server/src/main/kotlin
dedup Tachiyomi-App/src/compat/java
popd popd
popd popd

View File

@@ -1,11 +1,15 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.io.BufferedReader
plugins { 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.1.1"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -102,6 +106,19 @@ sourceSets {
} }
} }
val TachideskRevision = Runtime
.getRuntime()
.exec("git rev-list master --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r"+output.trim()
}
tasks { tasks {
jar { jar {
manifest { manifest {
@@ -115,15 +132,24 @@ tasks {
} }
shadowJar { shadowJar {
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
archiveBaseName.set("Tachidesk")
archiveVersion.set(TachideskVersion)
archiveClassifier.set(TachideskRevision)
} }
} }
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build") destinationDir = File("$rootDir/server/build")
//dependsOn(":webUI:copyBuild") dependsOn("lintKotlin")
} }
tasks.named("processResources") { tasks.named("processResources") {
dependsOn(":webUI:copyBuild") dependsOn(":webUI:copyBuild")
} }
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}

View File

@@ -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()
} }
} }

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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 ->

View File

@@ -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,15 +50,16 @@ class Main {
// start app // start app
androidCompat.startApp(App()) androidCompat.startApp(App())
val app = Javalin.create { config -> val app = Javalin.create { config ->
// config.addSinglePageRoot("/", "") try {
this::class.java.classLoader.getResource("/react/index.html")
config.addStaticFiles("/react") config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
}
}.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", "*")
@@ -53,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)
@@ -65,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()
@@ -91,9 +111,26 @@ class Main {
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getPages(chapterId, mangaId)) ctx.json(getPages(chapterId, mangaId))
} }
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
} }
// single source search
} app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
} }
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
}
}
}

View File

@@ -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,
@@ -18,3 +18,8 @@ data class MangaDataClass(
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name val status: String = MangaStatus.UNKNOWN.name
) )
data class PagedMangaListDataClass(
val mangaList: List<MangaDataClass>,
val hasNextPage: Boolean
)

View File

@@ -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) {

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -79,6 +79,5 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
it[ExtensionsTable.classFQName] it[ExtensionsTable.classFQName]
) )
} }
} }
} }

View File

@@ -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 {
} }
} }
} }

View File

@@ -1,15 +1,15 @@
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.dataclass.PagedMangaListDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import 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
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> { fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong()) val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first() source.fetchPopularManga(pageNum).toBlocking().first()
@@ -19,7 +19,12 @@ 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 transaction { return mangasPage.processEntries(sourceId)
}
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga -> return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
var mangaEntityId = if (mangaEntry == null) { // create manga entry var mangaEntityId = if (mangaEntry == null) { // create manga entry
@@ -42,7 +47,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,
@@ -58,4 +63,8 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
) )
} }
} }
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
} }

View File

@@ -0,0 +1,58 @@
package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
}
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId)
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
)
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
// return mapNotNull { filter ->
// when (filter) {
// is Filter.Header -> FilterWrapper("Header",filter)
// is Filter.Separator -> FilterWrapper("Separator",filter)
// is Filter.CheckBox -> FilterWrapper("CheckBox",filter)
// is Filter.TriState -> FilterWrapper("TriState",filter)
// is Filter.Text -> FilterWrapper("Text",filter)
// is Filter.Select<*> -> FilterWrapper("Select",filter)
// is Filter.Group<*> -> {
// val group = GroupItem(filter)
// val subItems = filter.state.mapNotNull {
// when (it) {
// is Filter.CheckBox -> FilterWrapper("CheckBox",filter)
// is Filter.TriState -> FilterWrapper("TriState",filter)
// is Filter.Text -> FilterWrapper("Text",filter)
// is Filter.Select<*> -> FilterWrapper("Select",filter)
// else -> null
// } as? ISectionable<*, *>
// }
// subItems.forEach { it.header = group }
// group.subItems = subItems
// group
// }
// is Filter.Sort -> {
// val group = SortGroup(filter)
// val subItems = filter.values.map {
// SortItem(it, group)
// }
// group.subItems = subItems
// group
// }
// }
// }
// }

View File

@@ -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
)
}
}

View File

@@ -9,6 +9,5 @@ fun applicationSetup() {
File(Config.dataRoot).mkdirs() File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs() File(Config.extensionsRoot).mkdirs()
makeDataBaseTables() makeDataBaseTables()
} }

View File

View File

@@ -4,11 +4,11 @@ plugins {
node { node {
workDir = file("${project.projectDir}/react/") workDir = file("${project.projectDir}/react/")
nodeModulesDir = file("${project.projectDir}/react/node_modules") nodeModulesDir = file("${project.projectDir}/react/")
} }
tasks.named("yarn_build") { tasks.named("yarn_build") {
dependsOn("yarn_install") dependsOn("yarn") // install node_moduels
} }
tasks.register<Copy>("copyBuild") { tasks.register<Copy>("copyBuild") {

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,13 +9,22 @@ 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/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="/sources/:sourceId/search/">
<Search />
</Route>
<Route path="/extensions"> <Route path="/extensions">
<Extensions /> <Extensions />
</Route> </Route>
@@ -38,6 +47,7 @@ export default function App() {
<Home /> <Home />
</Route> </Route>
</Switch> </Switch>
</NavBarTitle.Provider>
</Router> </Router>
); );
} }

View File

@@ -38,8 +38,10 @@ const useStyles = makeStyles({
interface IProps { interface IProps {
manga: IManga manga: IManga
// eslint-disable-next-line react/no-unused-prop-types, react/require-default-props
// ref?: false | React.MutableRefObject<HTMLInputElement | undefined>
} }
export default function MangaCard(props: IProps) { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
manga: { manga: {
id, title, thumbnailUrl, id, title, thumbnailUrl,
@@ -49,7 +51,7 @@ export default function MangaCard(props: IProps) {
return ( return (
<Link to={`/manga/${id}/`}> <Link to={`/manga/${id}/`}>
<Card className={classes.root}> <Card className={classes.root} ref={ref}>
<CardActionArea> <CardActionArea>
<div className={classes.wrapper}> <div className={classes.wrapper}>
<CardMedia <CardMedia
@@ -66,4 +68,6 @@ export default function MangaCard(props: IProps) {
</Card> </Card>
</Link> </Link>
); );
} });
export default MangaCard;

View File

@@ -0,0 +1,54 @@
import React, { useEffect, useRef } from 'react';
import MangaCard from './MangaCard';
interface IProps{
mangas: IManga[]
message?: string
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
}
export default function MangaGrid(props: IProps) {
const {
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped;
const lastManga = useRef<HTMLInputElement>();
const scrollHandler = () => {
if (lastManga.current) {
const rect = lastManga.current.getBoundingClientRect();
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
setLastPageNum(lastPageNum + 1);
}
}
};
useEffect(() => {
window.addEventListener('scroll', scrollHandler, true);
return () => {
window.removeEventListener('scroll', scrollHandler, true);
};
}, [hasNextPage, mangas]);
if (mangas.length === 0) {
mapped = <h3>{message}</h3>;
} else {
mapped = mangas.map((it, idx) => {
if (idx === mangas.length - 1) {
return <MangaCard manga={it} ref={lastManga} />;
}
return <MangaCard manga={it} />;
});
}
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
{mapped}
</div>
);
}
MangaGrid.defaultProps = {
message: 'loading...',
};

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>

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>

View File

@@ -48,6 +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' }}>
<ListItem button key="Search">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link> */}
</List> </List>
</div> </div>
); );

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;

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')

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(() => {

View File

@@ -1,33 +1,41 @@
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 MangaCard from '../components/MangaCard'; 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}>();
let mapped; const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [lastPageNum] = useState<number>(1); const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => {
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: { mangaList: IManga[], hasNextPage: boolean }) => {
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })), setMangas([
)); ...mangas,
}, []); ...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
});
}, [lastPageNum]);
if (mangas.length === 0) { return (
mapped = <h3>wait</h3>; <MangaGrid
} else { mangas={mangas}
mapped = ( hasNextPage={hasNextPage}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}> lastPageNum={lastPageNum}
{mangas.map((it) => ( setLastPageNum={setLastPageNum}
<MangaCard manga={it} /> />
))}
</div>
); );
} }
return mapped;
}

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));

View File

@@ -0,0 +1,91 @@
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 [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
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: { mangaList: IManga[], hasNextPage: boolean }) => {
if (data.mangaList.length > 0) {
setMangas([
...mangas,
...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
} else {
setMessage('search qeury returned nothing.');
}
});
}
}, [searchTerm]);
const mangaGrid = (
<MangaGrid
mangas={mangas}
message={message}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<Button variant="contained" color="primary" onClick={() => processInput()}>
Search
</Button>
</form>
{mangaGrid}
</>
);
}

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')