Compare commits

...

85 Commits

Author SHA1 Message Date
Aria Moradi a58aab9004 [CI RELEASE] v.0.1.4 2021-02-04 04:01:20 +03:30
Aria Moradi 61bd32f7f0 don't cancel me shit 2021-02-04 03:52:49 +03:30
Aria Moradi 63a444bd81 calling HttpSource.imageRequest now 2021-02-04 03:42:30 +03:30
Aria Moradi 8f28c3b74b eh missing shit from last commit 2021-02-04 00:38:23 +03:30
Aria Moradi d766206343 fix sqlite locking fuckery by replacing it with h2 2021-02-04 00:32:01 +03:30
Aria Moradi 172f83f5b3 Manga dir 2021-02-03 23:35:34 +03:30
Aria Moradi 9e308025c3 some initial code for MangaDex login 2021-02-03 22:07:39 +03:30
Aria Moradi aaa6a16778 Update README.md 2021-01-30 01:07:42 +03:30
Aria Moradi 2a21da2210 [SKIP CI] update README.md 2021-01-29 19:11:38 +03:30
Aria Moradi d1cd2cfc8c [RELEASE CI] bump version 2021-01-29 15:26:08 +03:30
Aria Moradi 832c224ed4 uninstalling extensions implemented 2021-01-29 15:23:29 +03:30
Aria Moradi 99316f4bd5 revert react changes 2021-01-29 14:25:18 +03:30
Aria Moradi 9caae5f1e5 thumbnail caching 2021-01-29 14:19:24 +03:30
Aria Moradi 345be95ce9 [RELEASE CI] bump version 2021-01-28 15:13:56 +03:30
Aria Moradi 6fe68841b7 [SKIP CI] add docker thanks to @arbuilder 2021-01-28 15:08:37 +03:30
Aria Moradi eaff2c15a9 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-26 23:33:14 +03:30
Aria Moradi 5eb8dc66a8 add license notice to everything 2021-01-26 23:32:12 +03:30
Aria Moradi 49715c81e4 Update README.md 2021-01-26 23:11:20 +03:30
Aria Moradi 3398409555 Update README.md 2021-01-26 23:07:54 +03:30
Aria Moradi f05aa0589a [SKIP CI] add apache 2 license link 2021-01-26 23:02:04 +03:30
Aria Moradi fbc71ce781 fix nav buttons 2021-01-23 02:57:32 +03:30
Aria Moradi ca9c671886 more css hacks: scroll bar 2021-01-23 02:53:00 +03:30
Aria Moradi bd109ba11f some css hacks 2021-01-23 02:32:18 +03:30
Aria Moradi 0ff770a98b fluid manga grid 2021-01-23 01:58:18 +03:30
Aria Moradi ed7bb408a3 fix light theme AppBar 2021-01-23 01:36:56 +03:30
Aria Moradi 84676b9156 remove some wierdness 2021-01-23 01:33:12 +03:30
Aria Moradi dcdd50ffe1 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-01-23 01:25:49 +03:30
Aria Moradi afb21c59f0 DarkTheme! my eyes can rest now :) 2021-01-23 01:20:16 +03:30
Aria Moradi e219179519 Update README.md 2021-01-23 00:28:32 +03:30
Aria Moradi 15a2115c5a Update README.md 2021-01-23 00:26:43 +03:30
Aria Moradi 94c6f33925 Update README.md 2021-01-23 00:23:40 +03:30
Aria Moradi 202e38871d [RELEASE CI] hotfix 2021-01-22 21:33:54 +03:30
Aria Moradi 3f75b84651 fix button text 2021-01-22 21:33:12 +03:30
Aria Moradi f171b785a0 [RELEASE CI] fix linting error 2021-01-22 21:29:09 +03:30
Aria Moradi 088dd6a856 [RELEASE CI] Milestone: The application is considered usable. 2021-01-22 21:18:14 +03:30
Aria Moradi 6318628ea2 [RELEASE CI] bump version 2021-01-22 21:15:14 +03:30
Aria Moradi 0757ea5d0d implemented infinite scroll 2021-01-22 21:11:00 +03:30
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
76 changed files with 1734 additions and 472 deletions
@@ -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
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
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
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
+52 -16
View File
@@ -1,33 +1,69 @@
# 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/).
This project has two components:
1. **server:** contains some of the original Tachiyomi code and serves a REST API
2. **webUI:** A react project that works with the server to do the presentation
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 do I run the thing?
### Get Android stubs jar(do this only once)
#### Prerequisites
You should have java 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
#### Running pre-built jar packages
Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
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.
#### Running on Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## Building from source
### Get Android stubs jar
#### Manual download
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)
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
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.
## running for development purposes
### The Server
run `./gradlew :server:run -x :webUI:yarn_build --stacktrace` to run the server
### the webUI
how to do it is described in `webUI/react/README.md` but for short,
Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
## Running for development purposes
### `server` module
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### `webUI` module
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)
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?
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented.
## Is this application usable? Should I test it?
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## Credit
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`.
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
## License
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
License, v. 2.0. If a copy of the MPL was not distributed with this
+2 -3
View File
@@ -58,9 +58,8 @@ function dedup() {
pushd ..
dedup AndroidCompat/src/main/java
dedup TachiServer/src/main/java
dedup Tachiyomi-App/src/main/java
dedup Tachiyomi-App/src/compat/java
dedup server/src/main/java
dedup server/src/main/kotlin
popd
popd
+29 -2
View File
@@ -1,11 +1,15 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import java.io.BufferedReader
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.1.4"
repositories {
mavenCentral()
@@ -75,7 +79,8 @@ dependencies {
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("org.xerial:sqlite-jdbc:3.30.1")
implementation ("com.h2database:h2:1.4.199")
// AndroidCompat
implementation(project(":AndroidCompat"))
@@ -102,6 +107,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 {
jar {
manifest {
@@ -115,15 +133,24 @@ tasks {
}
shadowJar {
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> {
destinationDir = File("$rootDir/server/build")
//dependsOn(":webUI:copyBuild")
dependsOn("lintKotlin")
}
tasks.named("processResources") {
dependsOn(":webUI:copyBuild")
}
tasks.named("run") {
dependsOn("formatKotlin", "lintKotlin")
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
@@ -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.
@@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.network
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
class MemoryCookieJar : CookieJar {
private val cache = mutableSetOf<WrappedCookie>()
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
val cookiesToRemove = mutableSetOf<WrappedCookie>()
val validCookies = mutableSetOf<WrappedCookie>()
cache.forEach { cookie ->
if (cookie.isExpired()) {
cookiesToRemove.add(cookie)
} else if (cookie.matches(url)) {
validCookies.add(cookie)
}
}
cache.removeAll(cookiesToRemove)
return validCookies.toList().map(WrappedCookie::unwrap)
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
cache.removeAll(cookiesToAdd)
cache.addAll(cookiesToAdd)
}
@Synchronized
fun clear() {
cache.clear()
}
}
class WrappedCookie private constructor(val cookie: Cookie) {
fun unwrap() = cookie
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
fun matches(url: HttpUrl) = cookie.matches(url)
override fun equals(other: Any?): Boolean {
if (other !is WrappedCookie) return false
return other.cookie.name == cookie.name &&
other.cookie.domain == cookie.domain &&
other.cookie.path == cookie.path &&
other.cookie.secure == cookie.secure &&
other.cookie.hostOnly == cookie.hostOnly
}
override fun hashCode(): Int {
var hash = 17
hash = 31 * hash + cookie.name.hashCode()
hash = 31 * hash + cookie.domain.hashCode()
hash = 31 * hash + cookie.path.hashCode()
hash = 31 * hash + if (cookie.secure) 0 else 1
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
return hash
}
companion object {
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
}
}
@@ -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) {
@@ -22,14 +19,17 @@ class NetworkHelper(context: Context) {
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
// val cookieManager = AndroidCookieJar()
val cookieManager = MemoryCookieJar()
val client by lazy {
val builder = OkHttpClient.Builder()
// .cookieJar(cookieManager)
.cookieJar(cookieManager)
// .cache(Cache(cacheDir, cacheSize))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
// .addInterceptor(UserAgentInterceptor())
// if (BuildConfig.DEBUG) {
@@ -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 ->
@@ -38,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
}
override fun unsubscribe() {
call.cancel()
// call.cancel()
}
override fun isUnsubscribed(): Boolean {
@@ -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,20 +77,21 @@ fun Call.asObservable(): Observable<Response> {
// }
// }
// }
//}
// }
fun Call.asObservableSuccess(): Observable<Response> {
return asObservable().doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}")
return asObservable()
.doOnNext { response ->
if (!response.isSuccessful) {
response.close()
throw Exception("HTTP error ${response.code}")
}
}
}
}
//fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
// val progressClient = newBuilder()
// .cache(null)
// .cache(nasObservableSuccessull)
// .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request())
// originalResponse.newBuilder()
@@ -104,11 +101,11 @@ fun Call.asObservableSuccess(): Observable<Response> {
// .build()
//
// return progressClient.newCall(request)
//}
// }
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
// .cache(null)
// .addNetworkInterceptor { chain ->
// val originalResponse = chain.proceed(chain.request())
// originalResponse.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
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
/**
* Network service.
*/
protected val network: NetworkHelper by injectLazy()
val network: NetworkHelper by injectLazy()
// /**
// * Preferences that a source may need.
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
*
* @param page the chapter whose page list has to be fetched
*/
protected open fun imageRequest(page: Page): Request {
open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers)
}
@@ -1,8 +1,14 @@
package ir.armor.tachidesk
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
val extensionsRoot = "$dataRoot/extensions"
}
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaRoot = "$dataRoot/manga"
}
@@ -1,8 +1,26 @@
package ir.armor.tachidesk
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.App
import io.javalin.Javalin
import ir.armor.tachidesk.util.*
import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapter
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.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
@@ -23,27 +41,34 @@ 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())
// Thread(getMangaUpdateQueueThread).start()
val app = Javalin.create { config ->
// config.addSinglePageRoot("/", "")
config.addStaticFiles("/react")
try {
this::class.java.classLoader.getResource("/react/index.html")
config.addStaticFiles("/react")
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", "*")
@@ -53,27 +78,40 @@ class Main {
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println(apkName)
println("installing $apkName")
ctx.status(
installAPK(apkName)
installAPK(apkName)
)
}
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("uninstalling $apkName")
removeExtension(apkName)
ctx.status(200)
}
app.get("/api/v1/source/list") { ctx ->
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()
ctx.json(getMangaList(sourceId,pageNum,popular = true))
ctx.json(getMangaList(sourceId, pageNum, popular = true))
}
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId,pageNum,popular = false))
ctx.json(getMangaList(sourceId, pageNum, popular = false))
}
app.get("/api/v1/manga/:mangaId/") { ctx ->
@@ -81,6 +119,14 @@ class Main {
ctx.json(getManga(mangaId))
}
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
@@ -89,11 +135,38 @@ class Main {
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getPages(chapterId,mangaId))
ctx.json(getChapter(chapterId, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterId, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
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))
}
}
}
}
@@ -1,9 +1,14 @@
package ir.armor.tachidesk.database
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.ExtensionsTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
@@ -11,18 +16,21 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
Database.connect("jdbc:h2:${Config.dataRoot}/database.h2", "org.h2.Driver")
}
}
fun makeDataBaseTables() {
// mention db object to connect
DBMangaer.db
// val db = DBMangaer.db
// db.useNestedTransactions = true
transaction {
SchemaUtils.create(ExtensionsTable)
SchemaUtils.create(SourceTable)
SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable)
SchemaUtils.create(PageTable)
}
}
}
@@ -1,11 +1,16 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ChapterDataClass(
val id: Int,
val url: String,
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,
val pageCount: Int? = null,
)
@@ -1,14 +1,18 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class ExtensionDataClass(
val name: String,
val pkgName: String,
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,
)
@@ -1,20 +1,29 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.MangaStatus
data class MangaDataClass(
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
)
data class PagedMangaListDataClass(
val mangaList: List<MangaDataClass>,
val hasNextPage: Boolean
)
@@ -1,6 +1,10 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class PageDataClass(
val index: Int,
var imageUrl: String,
)
val index: Int,
var imageUrl: String,
)
@@ -1,9 +1,13 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class SourceDataClass(
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
)
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
)
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.ExtensionsTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
@@ -18,4 +22,4 @@ class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
var iconUrl by ExtensionsTable.iconUrl
var installed by ExtensionsTable.installed
var classFQName by ExtensionsTable.classFQName
}
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
@@ -20,4 +24,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,12 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.dao.*
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 +18,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,9 @@
package ir.armor.tachidesk.database.table
import eu.kanade.tachiyomi.source.model.SManga
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object ChapterTable : IntIdTable() {
@@ -8,7 +11,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)
}
}
@@ -1,7 +1,10 @@
package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() {
val name = varchar("name", 128)
@@ -15,4 +18,4 @@ object ExtensionsTable : IntIdTable() {
val installed = bool("installed").default(false)
val classFQName = varchar("class_name", 256).default("") // fully qualified name
}
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SManga
import org.jetbrains.exposed.dao.id.IntIdTable
@@ -30,4 +34,4 @@ enum class MangaStatus(val status: Int) {
companion object {
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
}
}
}
@@ -0,0 +1,15 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object PageTable : IntIdTable() {
val index = integer("index")
val url = varchar("url", 2048)
val imageUrl = varchar("imageUrl", 2048).nullable()
val chapter = reference("chapter", ChapterTable)
}
@@ -1,12 +1,16 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IdTable
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()
}
}
@@ -1,28 +1,30 @@
package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Page
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.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 ir.armor.tachidesk.database.table.PageTable
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.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,47 +43,59 @@ 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 getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val pagesList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).toBlocking().first()
return@transaction pagesList.map {
PageDataClass(
it.index,
getTrueImageUrl(it,source)
)
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,
pageList.count()
)
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
transaction {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[this.chapter] = chapterId
}
}
}
}
return@transaction chapter
}
}
fun getTrueImageUrl(page: Page, source: HttpSource): String {
return if ( page.imageUrl == null){
source.fetchImageUrl(page).toBlocking().first()!!
} else page.imageUrl!!
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
@@ -67,18 +71,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]
)
}
}
}
@@ -0,0 +1,52 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okio.BufferedSource
import okio.buffer
import okio.sink
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Paths
fun writeStream(fileStream: InputStream, path: String) {
Files.newOutputStream(Paths.get(path)).use { os ->
val buffer = ByteArray(128 * 1024)
var len: Int
while (fileStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
}
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
}
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
File(directoryPath).listFiles().forEach { file ->
if (file.name.startsWith(fileName))
return "$directoryPath/${file.name}"
}
return null
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
@@ -1,78 +1,138 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
fun getManga(mangaId: Int): MangaDataClass {
return transaction {
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return@transaction if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
true,
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
// update database
MangaTable.update({ MangaTable.id eq mangaId }) {
// it[url] = fetchedManga.url
// it[title] = fetchedManga.title
it[initialized] = true
it[artist] = fetchedManga.artist
it[author] = fetchedManga.author
it[description] = fetchedManga.description
it[genre] = fetchedManga.genre
it[status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[thumbnail_url] = fetchedManga.thumbnail_url
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
).toBlocking().first()
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
transaction {
MangaTable.update({ MangaTable.id eq mangaId }) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
it[MangaTable.initialized] = true
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
mangaEntry[MangaTable.thumbnail_url],
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
it[MangaTable.artist] = fetchedManga.artist
it[MangaTable.author] = fetchedManga.author
it[MangaTable.description] = fetchedManga.description
it[MangaTable.genre] = fetchedManga.genre
it[MangaTable.status] = fetchedManga.status
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
}
}
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
true,
fetchedManga.artist,
fetchedManga.author,
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
)
}
}
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
var filePath = "${Config.thumbnailsRoot}/$mangaId."
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
if (potentialCache != null) {
println("using cached thumbnail file")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter(filePath)}"
)
}
val sourceId = mangaEntry[MangaTable.sourceReference].value
println("getting source for $mangaId")
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
println(thumbnailUrl)
val response = source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
writeStream(response.body!!.byteStream(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = sourceEntry[SourceTable.name]
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -0,0 +1,95 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import java.net.URLEncoder
class MangaDexHelper(private val mangaDexSource: HttpSource) {
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
private fun clientBuilder(
r18Toggle: Int,
okHttpClient: OkHttpClient = mangaDexSource.network.client
): OkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
.build()
chain.proceed(newReq)
}.build()
private fun cookiesHeader(r18Toggle: Int): String {
val cookies = mutableMapOf<String, String>()
cookies["mangadex_h_toggle"] = r18Toggle.toString()
return buildCookies(cookies)
}
private fun buildCookies(cookies: Map<String, String>) =
cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
// fun isLogged(): Boolean {
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
// }
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = clientBuilder().newCall(
POST(
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
mangaDexSource.headers,
formBody.build()
)
).execute()
return response.body!!.string().isEmpty()
}
//
// fun logout(): Boolean {
// return withContext(Dispatchers.IO) {
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
// val listOfDexCookies = network.cookieManager.get(httpUrl)
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
// val token = cookie?.value
// if (token.isNullOrEmpty()) {
// return@withContext true
// }
// val result = clientBuilder().newCall(
// POSTWithCookie(
// "$baseUrl/ajax/actions.ajax.php?function=logout",
// REMEMBER_ME,
// token,
// headers
// )
// ).execute()
// val resultStr = result.body!!.string()
// if (resultStr.contains("success", true)) {
// network.cookieManager.remove(httpUrl)
// return@withContext true
// }
//
// false
// }
// }
}
@@ -1,15 +1,23 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
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
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> {
fun proxyThumbnailUrl(mangaId: Int): String {
return "http://127.0.0.1:4567/api/v1/manga/$mangaId/thumbnail"
}
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first()
@@ -19,11 +27,16 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
else
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 ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
var mangaEntityId = if (mangaEntry == null) { // create manga entry
MangaTable.insertAndGetId {
if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
@@ -32,21 +45,18 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.genre
it[thumbnail_url] = manga.thumbnail_url
it[sourceReference] = sourceId
}.value
} else {
mangaEntry[MangaTable.id].value
}
MangaDataClass(
mangaEntityId,
sourceId.toLong(),
MangaDataClass(
mangaId,
sourceId,
manga.url,
manga.title,
manga.thumbnail_url,
proxyThumbnailUrl(mangaId),
manga.initialized,
@@ -55,7 +65,30 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name,
)
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId,
manga.url,
manga.title,
proxyThumbnailUrl(mangaId),
true,
mangaEntry[MangaTable.artist],
mangaEntry[MangaTable.author],
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
)
}
}
}
}
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
}
@@ -0,0 +1,81 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
}
return page.imageUrl!!
}
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl]
)
if (pageEntry[PageTable.imageUrl] == null) {
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
it[imageUrl] = getTrueImageUrl(tachiPage, source)
}
}
}
val saveDir = getMangaDir(mangaId) + "/" + chapterEntry[ChapterTable.chapter_number]
File(saveDir).mkdirs()
var filePath = "$saveDir/$index."
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
if (potentialCache != null) {
println("using cached page file for $index")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter("$filePath")}"
)
}
val response = source.fetchImage(tachiPage).toBlocking().first()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(filePath)).use { os ->
response.body!!.source().saveTo(os)
}
// writeStream(response.body!!.source(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
@@ -0,0 +1,62 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) {
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
// }
// }
// }
// }
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.Config
@@ -13,7 +17,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 +43,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 +75,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
)
}
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.makeDataBaseTables
import java.io.File
@@ -8,7 +12,7 @@ fun applicationSetup() {
// make dirs we need
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
File(Config.thumbnailsRoot).mkdirs()
makeDataBaseTables()
}
}
@@ -1,5 +1,9 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.googlecode.dex2jar.tools.Dex2jarCmd
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.NetworkHelper
@@ -13,6 +17,7 @@ import kotlinx.coroutines.runBlocking
import okhttp3.Request
import okio.buffer
import okio.sink
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -28,8 +33,8 @@ fun installAPK(apkName: String): Int {
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(dexPath).exists()) {
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) {
runBlocking {
val api = ExtensionGithubApi()
val apkToDownload = api.getApkUrl(extensionRecord)
@@ -41,7 +46,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 +64,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 +84,6 @@ fun installAPK(apkName: String): Int {
// println(httpSource.name)
// println()
}
} else { // multi source
val sourceFactory = instance as SourceFactory
transaction {
@@ -110,7 +113,6 @@ fun installAPK(apkName: String): Int {
it[classFQName] = className
}
}
}
return 201 // we downloaded successfully
} else {
@@ -122,10 +124,28 @@ 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()
sink.writeAll(response.body!!.source())
sink.close()
}
fun removeExtension(pkgName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
val fileNameWithoutType = pkgName.substringBefore(".apk")
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
it[ExtensionsTable.installed] = false
}
}
if (File(jarPath).exists()) {
File(jarPath).delete()
}
}
View File
+2 -2
View File
@@ -4,11 +4,11 @@ plugins {
node {
workDir = file("${project.projectDir}/react/")
nodeModulesDir = file("${project.projectDir}/react/node_modules")
nodeModulesDir = file("${project.projectDir}/react/")
}
tasks.named("yarn_build") {
dependsOn("yarn_install")
dependsOn("yarn") // install node_moduels
}
tasks.register<Copy>("copyBuild") {
+1 -1
View File
@@ -47,4 +47,4 @@
"eslint-plugin-react-hooks": "^4.2.0",
"typescript": "^4.1.0"
}
}
}
+77 -25
View File
@@ -1,7 +1,15 @@
import React from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useState } from 'react';
import {
BrowserRouter as Router, Route, Switch,
} from 'react-router-dom';
import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar';
import Home from './screens/Home';
import Sources from './screens/Sources';
@@ -9,35 +17,79 @@ import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList';
import Manga from './screens/Manga';
import Reader from './screens/Reader';
import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
import DarkTheme from './context/DarkTheme';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [darkTheme, setDarkTheme] = useState<boolean>(true);
const navTitleContext = { title, setTitle };
const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo(
() => createMuiTheme({
palette: {
type: darkTheme ? 'dark' : 'light',
},
overrides: {
MuiCssBaseline: {
'@global': {
'*::-webkit-scrollbar': {
width: '10px',
background: darkTheme ? '#222' : '#e1e1e1',
},
'*::-webkit-scrollbar-thumb': {
background: darkTheme ? '#111' : '#aaa',
borderRadius: '5px',
},
},
},
},
}),
[darkTheme],
);
return (
<Router>
<NavBar />
<Switch>
<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>
<ThemeProvider theme={theme}>
<NavBarTitle.Provider value={navTitleContext}>
<CssBaseline />
<DarkTheme.Provider value={darkThemeContext}>
<NavBar />
</DarkTheme.Provider>
<Container maxWidth={false} disableGutters>
<Switch>
<Route path="/sources/:sourceId/search/">
<Search />
</Route>
<Route path="/extensions">
<Extensions />
</Route>
<Route path="/sources/:sourceId/popular/">
<MangaList popular />
</Route>
<Route path="/sources/:sourceId/latest/">
<MangaList popular={false} />
</Route>
<Route path="/sources">
<Sources />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterId">
<Reader />
</Route>
<Route path="/manga/:id">
<Manga />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Container>
</NavBarTitle.Provider>
</ThemeProvider>
</Router>
);
}
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
+22 -3
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
@@ -42,7 +46,7 @@ export default function ExtensionCard(props: IProps) {
name, lang, versionName, iconUrl, installed, apkName,
},
} = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
@@ -50,10 +54,25 @@ export default function ExtensionCard(props: IProps) {
function install() {
setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
setInstalledState('installed');
setInstalledState('uninstall');
});
}
function uninstall() {
setInstalledState('uninstalling');
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => {
setInstalledState('install');
});
}
function handleButtonClick() {
if (installedState === 'install') {
install();
} else {
uninstall();
}
}
return (
<Card>
<CardContent className={classes.root}>
@@ -76,7 +95,7 @@ export default function ExtensionCard(props: IProps) {
</div>
</div>
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
<Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
</CardContent>
</Card>
);
+28 -19
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
@@ -5,6 +9,7 @@ import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
const useStyles = makeStyles({
root: {
@@ -39,7 +44,7 @@ const useStyles = makeStyles({
interface IProps {
manga: IManga
}
export default function MangaCard(props: IProps) {
const MangaCard = React.forwardRef((props: IProps, ref) => {
const {
manga: {
id, title, thumbnailUrl,
@@ -48,22 +53,26 @@ export default function MangaCard(props: IProps) {
const classes = useStyles();
return (
<Link to={`/manga/${id}/`}>
<Card className={classes.root}>
<CardActionArea>
<div className={classes.wrapper}>
<CardMedia
className={classes.image}
component="img"
alt={title}
image={thumbnailUrl}
title={title}
/>
<div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</div>
</CardActionArea>
</Card>
</Link>
<Grid item xs={6} sm={4} md={3} lg={2}>
<Link to={`/manga/${id}/`}>
<Card className={classes.root} ref={ref}>
<CardActionArea>
<div className={classes.wrapper}>
<CardMedia
className={classes.image}
component="img"
alt={title}
image={thumbnailUrl}
title={title}
/>
<div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</div>
</CardActionArea>
</Card>
</Link>
</Grid>
);
}
});
export default MangaCard;
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
interface IProps{
+59
View File
@@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, useRef } from 'react';
import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard';
interface IProps{
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 (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
{mapped}
</Grid>
);
}
MangaGrid.defaultProps = {
message: 'loading...',
};
+71 -3
View File
@@ -1,11 +1,21 @@
import React, { useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarTitle from '../context/NavbarTitle';
import DarkTheme from '../context/DarkTheme';
const useStyles = makeStyles((theme) => ({
root: {
@@ -19,13 +29,35 @@ const useStyles = makeStyles((theme) => ({
},
}));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title } = useContext(NavBarTitle);
const open = Boolean(anchorEl);
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div className={classes.root}>
<AppBar position="static">
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
@@ -38,8 +70,44 @@ export default function NavBar() {
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Tachidesk
{title}
</Typography>
<IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<MoreIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
>
Dark Theme
</MenuItem>
<MenuItem
onClick={() => { setDarkTheme(false); handleClose(); }}
>
Light Theme
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
+7 -2
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
@@ -65,8 +69,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>
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
@@ -48,6 +52,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" />
</ListItem>
</Link>
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link> */}
</List>
</div>
);
+17
View File
@@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
type ContextType = {
darkTheme: boolean
setDarkTheme: React.Dispatch<React.SetStateAction<boolean>>
};
const DarkTheme = React.createContext<ContextType>({
darkTheme: true,
setDarkTheme: ():void => {},
});
export default DarkTheme;
+17
View File
@@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
type ContextType = {
title: string
setTitle: React.Dispatch<React.SetStateAction<string>>
};
const NavBarTitle = React.createContext<ContextType>({
title: 'Tachidesk',
setTitle: ():void => {},
});
export default NavBarTitle;
+4
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
body {
margin: 0;
}
+4
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
+4
View File
@@ -1 +1,5 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/// <reference types="react-scripts" />
+4
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
+10 -7
View File
@@ -1,8 +1,14 @@
import React, { useEffect, useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { 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[]>([]);
useEffect(() => {
@@ -12,10 +18,7 @@ export default function Extensions() {
}, []);
if (extensions.length === 0) {
mapped = <h3>wait</h3>;
} else {
mapped = extensions.map((it) => <ExtensionCard extension={it} />);
return <h3>wait</h3>;
}
return <h2>{mapped}</h2>;
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
}
+4
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
export default function Home() {
+11 -2
View File
@@ -1,10 +1,16 @@
import React, { useEffect, useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, 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 +18,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(() => {
+33 -21
View File
@@ -1,33 +1,45 @@
import React, { useEffect, useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
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 }) {
const { sourceId } = useParams<{sourceId: string}>();
let mapped;
const { setTitle } = useContext(NavBarTitle);
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(() => {
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: { mangaList: IManga[], hasNextPage: boolean }) => {
setMangas([
...mangas,
...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
});
}, [lastPageNum]);
if (mangas.length === 0) {
mapped = <h3>wait</h3>;
} else {
mapped = (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
{mangas.map((it) => (
<MangaCard manga={it} />
))}
</div>
);
}
return mapped;
return (
<MangaGrid
mangas={mangas}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
}
+24 -17
View File
@@ -1,5 +1,10 @@
import React, { useEffect, useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import NavBarTitle from '../context/NavbarTitle';
const style = {
display: 'flex',
@@ -9,34 +14,36 @@ const style = {
backgroundColor: '#343a40',
} as React.CSSProperties;
interface IPage {
index: number
imageUrl: string
}
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
export default function Reader() {
const [pages, setPages] = useState<IPage[]>([]);
const { setTitle } = useContext(NavBarTitle);
const [pageCount, setPageCount] = useState<number>(-1);
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:IChapter) => {
setTitle(data.name);
setPageCount(data.pageCount);
});
}, []);
pages.sort((a, b) => (a.index - b.index));
let mapped;
if (pages.length === 0) {
mapped = <h3>wait</h3>;
} else {
mapped = pages.map(({ imageUrl }) => (
<div style={{ margin: '0 auto' }}>
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
if (pageCount === -1) {
return (
<div style={style}>
<h3>wait</h3>
</div>
));
);
}
const mapped = range(pageCount).map((index) => (
<div style={{ margin: '0 auto' }}>
<img src={`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="f" style={{ maxWidth: '100%' }} />
</div>
));
return (
<div style={style}>
{mapped}
+95
View File
@@ -0,0 +1,95 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
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}
</>
);
}
+10 -7
View File
@@ -1,8 +1,14 @@
import React, { useEffect, useState } from 'react';
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { 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[]>([]);
useEffect(() => {
@@ -12,10 +18,7 @@ export default function Sources() {
}, []);
if (sources.length === 0) {
mapped = <h3>wait</h3>;
} else {
mapped = sources.map((it) => <SourceCard source={it} />);
return (<h3>wait</h3>);
}
return <h2>{mapped}</h2>;
return <>{sources.map((it) => <SourceCard source={it} />)}</>;
}
+5
View File
@@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
interface IExtension {
name: string
lang: string
@@ -30,4 +34,5 @@ interface IChapter {
chapter_number: number
scanlator: String
mangaId: number
pageCount: number
}