Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a58aab9004 | |||
| 61bd32f7f0 | |||
| 63a444bd81 | |||
| 8f28c3b74b | |||
| d766206343 | |||
| 172f83f5b3 | |||
| 9e308025c3 | |||
| aaa6a16778 | |||
| 2a21da2210 | |||
| d1cd2cfc8c | |||
| 832c224ed4 | |||
| 99316f4bd5 | |||
| 9caae5f1e5 | |||
| 345be95ce9 | |||
| 6fe68841b7 | |||
| eaff2c15a9 | |||
| 5eb8dc66a8 | |||
| 49715c81e4 | |||
| 3398409555 | |||
| f05aa0589a | |||
| fbc71ce781 | |||
| ca9c671886 | |||
| bd109ba11f | |||
| 0ff770a98b | |||
| ed7bb408a3 | |||
| 84676b9156 | |||
| dcdd50ffe1 | |||
| afb21c59f0 | |||
| e219179519 | |||
| 15a2115c5a | |||
| 94c6f33925 | |||
| 202e38871d | |||
| 3f75b84651 | |||
| f171b785a0 | |||
| 088dd6a856 | |||
| 6318628ea2 | |||
| 0757ea5d0d | |||
| 2c76ad9b74 | |||
| 7d1c63e181 | |||
| 6401b946b6 | |||
| 9a61f58043 | |||
| b854fdeadb | |||
| 6eb4f1ba88 | |||
| ded5e3a73a | |||
| 8d9f5b0bd9 | |||
| 5cbb2a0481 | |||
| a3b17365b7 | |||
| 2847554b8f | |||
| fb166eadd4 | |||
| 046c11f785 | |||
| eac7436b18 | |||
| 1a23804e51 | |||
| 08be142858 | |||
| f421dbfe69 | |||
| 0d7ad65dbe | |||
| 9e946406bc | |||
| 9142d46fae | |||
| 39ed134f96 | |||
| 7e4b495398 | |||
| c12242b760 | |||
| 8b2fd28a54 | |||
| 466f21a7e8 | |||
| 26a7e2f1cd | |||
| c155d78d52 | |||
| 687dad5fc0 | |||
| 33c4cdbc48 | |||
| e46cb9738f | |||
| aef37fcc9e | |||
| ac51f40a91 | |||
| cd82af2d76 | |||
| 790040cd68 | |||
| 95465ec265 | |||
| 5313d91bf2 | |||
| 63783984c6 | |||
| 97b9b1b6c9 | |||
| 34a7c24e0b | |||
| 12765a771f | |||
| 39850c71b0 | |||
| 9e43645a67 | |||
| 7dc7f4d905 | |||
| c537c1bf29 | |||
| 5f3ddbd1b2 | |||
| f297e3790c | |||
| ef3532357f | |||
| 48f29edf6c |
@@ -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
|
||||||
Executable
+18
@@ -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
|
||||||
Executable
+13
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,33 +1,69 @@
|
|||||||
# Tachidesk
|
# Tachidesk
|
||||||
A not so much port of [Tachiyomi](https://tachiyomi.org/) to the web (and later Electron for the desktop experience)!
|
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
This project has two components:
|
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
||||||
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
|
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||||
|
|
||||||
## How do I run the thing?
|
## 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
|
#### Manual download
|
||||||
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work)
|
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||||
run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
### building the jar
|
### building the jar
|
||||||
run `./gradlew :server:fatJar` the resulting jar file will be `server/build/server-1.0-all.jar`. Simply double click on it or run `java -jar server-1.0-all.jar`. The server will be running on `http://localhost:4567` open this url in your browser.
|
Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
## running for development purposes
|
## Running for development purposes
|
||||||
### The Server
|
### `server` module
|
||||||
run `./gradlew :server:run -x :webUI:yarn_build --stacktrace` to run the server
|
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||||
### the webUI
|
### `webUI` module
|
||||||
how to do it is described in `webUI/react/README.md` but for short,
|
How to do it is described in `webUI/react/README.md` but for short,
|
||||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||||
then `yarn start` to start the client if a new browser window doesn't start automatically,
|
then `yarn start` to start the client if a new browser window doesn't start automatically,
|
||||||
then open `http://127.0.0.1:3000` in a modern browser.
|
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||||
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
## Is the application usable? Should I test it?
|
## Is this application usable? Should I test it?
|
||||||
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented.
|
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
|
## License
|
||||||
|
|
||||||
Copyright (C) 2020 Aria Moradi
|
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
|||||||
@@ -58,9 +58,8 @@ function dedup() {
|
|||||||
|
|
||||||
pushd ..
|
pushd ..
|
||||||
dedup AndroidCompat/src/main/java
|
dedup AndroidCompat/src/main/java
|
||||||
dedup TachiServer/src/main/java
|
dedup server/src/main/java
|
||||||
dedup Tachiyomi-App/src/main/java
|
dedup server/src/main/kotlin
|
||||||
dedup Tachiyomi-App/src/compat/java
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|||||||
+29
-2
@@ -1,11 +1,15 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
import java.io.BufferedReader
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||||
|
id("org.jmailen.kotlinter") version "3.3.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val TachideskVersion = "v0.1.4"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -75,7 +79,8 @@ dependencies {
|
|||||||
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-jdbc:$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
|
// AndroidCompat
|
||||||
implementation(project(":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 {
|
tasks {
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
@@ -115,15 +133,24 @@ tasks {
|
|||||||
}
|
}
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
||||||
|
archiveBaseName.set("Tachidesk")
|
||||||
|
archiveVersion.set(TachideskVersion)
|
||||||
|
archiveClassifier.set(TachideskRevision)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
tasks.withType<ShadowJar> {
|
||||||
destinationDir = File("$rootDir/server/build")
|
destinationDir = File("$rootDir/server/build")
|
||||||
//dependsOn(":webUI:copyBuild")
|
dependsOn("lintKotlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("processResources") {
|
tasks.named("processResources") {
|
||||||
dependsOn(":webUI:copyBuild")
|
dependsOn(":webUI:copyBuild")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named("run") {
|
||||||
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk;
|
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.Document;
|
||||||
import org.w3c.dom.NamedNodeMap;
|
import org.w3c.dom.NamedNodeMap;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
//import android.content.res.Configuration
|
// import android.content.res.Configuration
|
||||||
//import android.support.multidex.MultiDex
|
// import android.support.multidex.MultiDex
|
||||||
//import timber.log.Timber
|
// import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.InjektScope
|
import uy.kohesive.injekt.api.InjektScope
|
||||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||||
|
|||||||
@@ -2,19 +2,22 @@ package eu.kanade.tachiyomi
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
//import eu.kanade.tachiyomi.data.cache.ChapterCache
|
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
//import eu.kanade.tachiyomi.data.cache.CoverCache
|
// import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
//import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
// import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
//import eu.kanade.tachiyomi.data.download.DownloadManager
|
// import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
//import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
|
// import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
|
||||||
//import eu.kanade.tachiyomi.data.track.TrackManager
|
// import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
//import eu.kanade.tachiyomi.extension.ExtensionManager
|
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.api.*
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
|
import uy.kohesive.injekt.api.addSingletonFactory
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class AppModule(val app: Application) : InjektModule {
|
class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rxAsync { get<DatabaseHelper>() }
|
// rxAsync { get<DatabaseHelper>() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rxAsync(block: () -> Unit) {
|
private fun rxAsync(block: () -> Unit) {
|
||||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.extension.api
|
package eu.kanade.tachiyomi.extension.api
|
||||||
|
|
||||||
//import android.content.Context
|
// import android.content.Context
|
||||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||||
//import kotlinx.coroutines.Dispatchers
|
// import kotlinx.coroutines.Dispatchers
|
||||||
//import kotlinx.coroutines.withContext
|
// import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.JsonArray
|
import kotlinx.serialization.json.JsonArray
|
||||||
import kotlinx.serialization.json.int
|
import kotlinx.serialization.json.int
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
//import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ internal class ExtensionGithubApi {
|
|||||||
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
||||||
// val extensions = fin dExtensions()
|
// val extensions = fin dExtensions()
|
||||||
//
|
//
|
||||||
//// preferences.lastExtCheck().set(Date().time)
|
// // preferences.lastExtCheck().set(Date().time)
|
||||||
//
|
//
|
||||||
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||||
// .filterIsInstance<LoadResult.Success>()
|
// .filterIsInstance<LoadResult.Success>()
|
||||||
@@ -49,23 +49,23 @@ internal class ExtensionGithubApi {
|
|||||||
|
|
||||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||||
return json
|
return json
|
||||||
.filter { element ->
|
.filter { element ->
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map { element ->
|
.map { element ->
|
||||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
||||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
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 {
|
fun getApkUrl(extension: Extension.Available): String {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
//import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to get the extension repo listing from GitHub.
|
* Used to get the extension repo listing from GitHub.
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.extension.util
|
package eu.kanade.tachiyomi.extension.util
|
||||||
|
|
||||||
//import android.annotation.SuppressLint
|
// import android.annotation.SuppressLint
|
||||||
//import android.content.Context
|
// import android.content.Context
|
||||||
//import android.content.pm.PackageInfo
|
// import android.content.pm.PackageInfo
|
||||||
//import android.content.pm.PackageManager
|
// import android.content.pm.PackageManager
|
||||||
//import dalvik.system.PathClassLoader
|
// import dalvik.system.PathClassLoader
|
||||||
import eu.kanade.tachiyomi.annoations.Nsfw
|
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
//import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
// import kotlinx.coroutines.async
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
// import kotlinx.coroutines.runBlocking
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
// import timber.log.Timber
|
||||||
import eu.kanade.tachiyomi.source.Source
|
// import uy.kohesive.injekt.injectLazy
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that handles the loading of the extensions installed in the system.
|
* Class that handles the loading of the extensions installed in the system.
|
||||||
*/
|
*/
|
||||||
//@SuppressLint("PackageManagerGetSignatures")
|
// @SuppressLint("PackageManagerGetSignatures")
|
||||||
internal object ExtensionLoader {
|
internal object ExtensionLoader {
|
||||||
|
|
||||||
// private val preferences: PreferencesHelper by injectLazy()
|
// private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
//import android.annotation.SuppressLint
|
// import android.annotation.SuppressLint
|
||||||
//import android.content.Context
|
// import android.content.Context
|
||||||
//import android.os.Build
|
// import android.os.Build
|
||||||
//import android.os.Handler
|
// import android.os.Handler
|
||||||
//import android.os.Looper
|
// import android.os.Looper
|
||||||
//import android.webkit.WebSettings
|
// import android.webkit.WebSettings
|
||||||
//import android.webkit.WebView
|
// import android.webkit.WebView
|
||||||
//import android.widget.Toast
|
// import android.widget.Toast
|
||||||
//import eu.kanade.tachiyomi.R
|
// import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
// import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
//import eu.kanade.tachiyomi.util.lang.launchUI
|
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
//import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
// import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
//import eu.kanade.tachiyomi.util.system.WebViewUtil
|
// import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
//import eu.kanade.tachiyomi.util.system.isOutdated
|
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
//import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
// import eu.kanade.tachiyomi.util.system.toast
|
||||||
//import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
//import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class CloudflareInterceptor() : Interceptor {
|
class CloudflareInterceptor() : Interceptor {
|
||||||
|
|
||||||
@@ -77,7 +70,7 @@ class CloudflareInterceptor() : Interceptor {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
//// @SuppressLint("SetJavaScriptEnabled")
|
// // @SuppressLint("SetJavaScriptEnabled")
|
||||||
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
||||||
// // We need to lock this thread until the WebView finds the challenge solution url, because
|
// // We need to lock this thread until the WebView finds the challenge solution url, because
|
||||||
// // OkHttp doesn't support asynchronous interceptors.
|
// // 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
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
//import android.content.Context
|
// import android.content.Context
|
||||||
//import eu.kanade.tachiyomi.BuildConfig
|
// import eu.kanade.tachiyomi.BuildConfig
|
||||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import okhttp3.Cache
|
// import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
//import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
//import okhttp3.dnsoverhttps.DnsOverHttps
|
// import okhttp3.dnsoverhttps.DnsOverHttps
|
||||||
//import okhttp3.logging.HttpLoggingInterceptor
|
// import okhttp3.logging.HttpLoggingInterceptor
|
||||||
//import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class NetworkHelper(context: Context) {
|
class NetworkHelper(context: Context) {
|
||||||
@@ -22,14 +19,17 @@ class NetworkHelper(context: Context) {
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
// val cookieManager = AndroidCookieJar()
|
val cookieManager = MemoryCookieJar()
|
||||||
|
|
||||||
val client by lazy {
|
val client by lazy {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
// .cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
// .cache(Cache(cacheDir, cacheSize))
|
// .cache(Cache(cacheDir, cacheSize))
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(5, TimeUnit.MINUTES)
|
||||||
|
.writeTimeout(5, TimeUnit.MINUTES)
|
||||||
|
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||||
|
|
||||||
// .addInterceptor(UserAgentInterceptor())
|
// .addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
// if (BuildConfig.DEBUG) {
|
// if (BuildConfig.DEBUG) {
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
//import kotlinx.coroutines.suspendCancellableCoroutine
|
// import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
@@ -38,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
call.cancel()
|
// call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean {
|
override fun isUnsubscribed(): Boolean {
|
||||||
@@ -52,7 +48,7 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
// 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 ->
|
// return suspendCancellableCoroutine { continuation ->
|
||||||
// enqueue(
|
// enqueue(
|
||||||
// object : Callback {
|
// object : Callback {
|
||||||
@@ -81,20 +77,21 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//}
|
// }
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable().doOnNext { response ->
|
return asObservable()
|
||||||
if (!response.isSuccessful) {
|
.doOnNext { response ->
|
||||||
response.close()
|
if (!response.isSuccessful) {
|
||||||
throw Exception("HTTP error ${response.code}")
|
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()
|
// val progressClient = newBuilder()
|
||||||
// .cache(null)
|
// .cache(nasObservableSuccessull)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
@@ -104,11 +101,11 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||||||
// .build()
|
// .build()
|
||||||
//
|
//
|
||||||
// return progressClient.newCall(request)
|
// return progressClient.newCall(request)
|
||||||
//}
|
// }
|
||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
// .cache(null)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
//import androidx.preference.PreferenceScreen
|
// import androidx.preference.PreferenceScreen
|
||||||
|
|
||||||
interface ConfigurableSource : Source {
|
interface ConfigurableSource : Source {
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
//import android.graphics.drawable.Drawable
|
// import android.graphics.drawable.Drawable
|
||||||
//import eu.kanade.tachiyomi.extension.ExtensionManager
|
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
//import uy.kohesive.injekt.Injekt
|
// import uy.kohesive.injekt.Injekt
|
||||||
//import uy.kohesive.injekt.api.get
|
// import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
* 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 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
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
//import android.content.Context
|
// import android.content.Context
|
||||||
//import eu.kanade.tachiyomi.R
|
// import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ open class Page(
|
|||||||
val url: String = "",
|
val url: String = "",
|
||||||
var imageUrl: String? = null,
|
var imageUrl: String? = null,
|
||||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||||
): ProgressListener {
|
) : ProgressListener {
|
||||||
|
|
||||||
val number: Int
|
val number: Int
|
||||||
get() = index + 1
|
get() = index + 1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface SChapter : Serializable {
|
|||||||
|
|
||||||
var chapter_number: Float
|
var chapter_number: Float
|
||||||
|
|
||||||
var scanlator: String?
|
var scanlator: String?
|
||||||
|
|
||||||
fun copyFrom(other: SChapter) {
|
fun copyFrom(other: SChapter) {
|
||||||
name = other.name
|
name = other.name
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import okhttp3.Request
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
//import uy.kohesive.injekt.injectLazy
|
// import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
*/
|
*/
|
||||||
protected val network: NetworkHelper by injectLazy()
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Preferences that a source may need.
|
// * 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
|
* @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)
|
return GET(page.imageUrl!!, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package ir.armor.tachidesk
|
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
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
|
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
}
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
|
val mangaRoot = "$dataRoot/manga"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
package ir.armor.tachidesk
|
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 eu.kanade.tachiyomi.App
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.util.*
|
import ir.armor.tachidesk.util.applicationSetup
|
||||||
|
import ir.armor.tachidesk.util.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.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
@@ -23,27 +41,34 @@ class Main {
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
// System.getProperties()["proxySet"] = "true"
|
||||||
|
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
|
||||||
|
// System.getProperties()["socksProxyPort"] = "2020"
|
||||||
|
|
||||||
// make sure everything we need exists
|
// make sure everything we need exists
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
|
|
||||||
registerConfigModules()
|
registerConfigModules()
|
||||||
|
|
||||||
//Load config API
|
// Load config API
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
//Load Android compatibility dependencies
|
// Load Android compatibility dependencies
|
||||||
AndroidCompatInitializer().init()
|
AndroidCompatInitializer().init()
|
||||||
// start app
|
// start app
|
||||||
androidCompat.startApp(App())
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
// Thread(getMangaUpdateQueueThread).start()
|
||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
// config.addSinglePageRoot("/", "")
|
try {
|
||||||
config.addStaticFiles("/react")
|
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)
|
}.start(4567)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.before() { ctx ->
|
app.before() { ctx ->
|
||||||
// allow the client which is running on another port
|
// allow the client which is running on another port
|
||||||
ctx.header("Access-Control-Allow-Origin", "*")
|
ctx.header("Access-Control-Allow-Origin", "*")
|
||||||
@@ -53,27 +78,40 @@ class Main {
|
|||||||
ctx.json(getExtensionList())
|
ctx.json(getExtensionList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
println(apkName)
|
println("installing $apkName")
|
||||||
|
|
||||||
ctx.status(
|
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 ->
|
app.get("/api/v1/source/list") { ctx ->
|
||||||
ctx.json(getSourceList())
|
ctx.json(getSourceList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||||
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
|
ctx.json(getSource(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId,pageNum,popular = true))
|
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||||
}
|
}
|
||||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||||
ctx.json(getMangaList(sourceId,pageNum,popular = false))
|
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||||
@@ -81,6 +119,14 @@ class Main {
|
|||||||
ctx.json(getManga(mangaId))
|
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 ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getChapterList(mangaId))
|
ctx.json(getChapterList(mangaId))
|
||||||
@@ -89,11 +135,38 @@ class Main {
|
|||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
val mangaId = ctx.pathParam("mangaId").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
|
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.Config
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
@@ -11,18 +16,21 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
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() {
|
fun makeDataBaseTables() {
|
||||||
// mention db object to connect
|
// mention db object to connect
|
||||||
DBMangaer.db
|
DBMangaer.db
|
||||||
|
// val db = DBMangaer.db
|
||||||
|
// db.useNestedTransactions = true
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(ExtensionsTable)
|
SchemaUtils.create(ExtensionsTable)
|
||||||
SchemaUtils.create(SourceTable)
|
SchemaUtils.create(SourceTable)
|
||||||
SchemaUtils.create(MangaTable)
|
SchemaUtils.create(MangaTable)
|
||||||
SchemaUtils.create(ChapterTable)
|
SchemaUtils.create(ChapterTable)
|
||||||
|
SchemaUtils.create(PageTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
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(
|
data class ChapterDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val date_upload: Long,
|
val date_upload: Long,
|
||||||
val chapter_number: Float,
|
val chapter_number: Float,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
)
|
val pageCount: Int? = null,
|
||||||
|
)
|
||||||
|
|||||||
+15
-11
@@ -1,14 +1,18 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
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(
|
data class ExtensionDataClass(
|
||||||
val name: String,
|
val name: String,
|
||||||
val pkgName: String,
|
val pkgName: String,
|
||||||
val versionName: String,
|
val versionName: String,
|
||||||
val versionCode: Int,
|
val versionCode: Int,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val isNsfw: Boolean,
|
val isNsfw: Boolean,
|
||||||
val apkName: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val installed: Boolean,
|
val installed: Boolean,
|
||||||
val classFQName: String,
|
val classFQName: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
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
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val sourceId: Long,
|
val sourceId: Long,
|
||||||
|
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val thumbnail_url: String? = null,
|
val thumbnailUrl: String? = null,
|
||||||
|
|
||||||
val initialized: Boolean = false,
|
val initialized: Boolean = false,
|
||||||
|
|
||||||
val artist: String? = null,
|
val artist: String? = null,
|
||||||
val author: String? = null,
|
val author: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val genre: String? = null,
|
val genre: String? = null,
|
||||||
val status: String = MangaStatus.UNKNOWN.name
|
val status: String = MangaStatus.UNKNOWN.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class PagedMangaListDataClass(
|
||||||
|
val mangaList: List<MangaDataClass>,
|
||||||
|
val hasNextPage: Boolean
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
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(
|
data class PageDataClass(
|
||||||
val index: Int,
|
val index: Int,
|
||||||
var imageUrl: String,
|
var imageUrl: String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package ir.armor.tachidesk.database.dataclass
|
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(
|
data class SourceDataClass(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val supportsLatest: Boolean
|
val supportsLatest: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
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 ir.armor.tachidesk.database.table.ExtensionsTable
|
||||||
import org.jetbrains.exposed.dao.IntEntity
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
import org.jetbrains.exposed.dao.IntEntityClass
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
@@ -18,4 +22,4 @@ class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
|||||||
var iconUrl by ExtensionsTable.iconUrl
|
var iconUrl by ExtensionsTable.iconUrl
|
||||||
var installed by ExtensionsTable.installed
|
var installed by ExtensionsTable.installed
|
||||||
var classFQName by ExtensionsTable.classFQName
|
var classFQName by ExtensionsTable.classFQName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
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 ir.armor.tachidesk.database.table.MangaTable
|
||||||
import org.jetbrains.exposed.dao.IntEntity
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
import org.jetbrains.exposed.dao.IntEntityClass
|
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 thumbnail_url by MangaTable.thumbnail_url
|
||||||
|
|
||||||
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package ir.armor.tachidesk.database.entity
|
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 ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.dao.*
|
import org.jetbrains.exposed.dao.EntityClass
|
||||||
|
import org.jetbrains.exposed.dao.LongEntity
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
|
||||||
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||||
@@ -13,4 +18,4 @@ class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
|||||||
var extension by ExtensionEntity referencedOn SourceTable.extension
|
var extension by ExtensionEntity referencedOn SourceTable.extension
|
||||||
var partOfFactorySource by SourceTable.partOfFactorySource
|
var partOfFactorySource by SourceTable.partOfFactorySource
|
||||||
var positionInFactorySource by SourceTable.positionInFactorySource
|
var positionInFactorySource by SourceTable.positionInFactorySource
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
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
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
@@ -8,7 +11,7 @@ object ChapterTable : IntIdTable() {
|
|||||||
val name = varchar("name", 512)
|
val name = varchar("name", 512)
|
||||||
val date_upload = long("date_upload").default(0)
|
val date_upload = long("date_upload").default(0)
|
||||||
val chapter_number = float("chapter_number").default(-1f)
|
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)
|
val manga = reference("manga", MangaTable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
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() {
|
object ExtensionsTable : IntIdTable() {
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
@@ -15,4 +18,4 @@ object ExtensionsTable : IntIdTable() {
|
|||||||
|
|
||||||
val installed = bool("installed").default(false)
|
val installed = bool("installed").default(false)
|
||||||
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
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 eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
@@ -30,4 +34,4 @@ enum class MangaStatus(val status: Int) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
|
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
|
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
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
|
||||||
object SourceTable : IdTable<Long>() {
|
object SourceTable : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name= varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val lang = varchar("lang", 10)
|
val lang = varchar("lang", 10)
|
||||||
val extension = reference("extension", ExtensionsTable)
|
val extension = reference("extension", ExtensionsTable)
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
package ir.armor.tachidesk.util
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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.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.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
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.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
|
||||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
val mangaDetails = getManga(mangaId)
|
||||||
val source = getHttpSource(mangaDetails.sourceId)
|
val source = getHttpSource(mangaDetails.sourceId)
|
||||||
|
|
||||||
val chapterList = source.fetchChapterList(
|
val chapterList = source.fetchChapterList(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDetails.title
|
title = mangaDetails.title
|
||||||
url = mangaDetails.url
|
url = mangaDetails.url
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
).toBlocking().first()
|
||||||
|
|
||||||
return transaction {
|
return transaction {
|
||||||
@@ -41,47 +43,59 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return@transaction chapterList.map {
|
return@transaction chapterList.map {
|
||||||
ChapterDataClass(
|
ChapterDataClass(
|
||||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||||
it.url,
|
it.url,
|
||||||
it.name,
|
it.name,
|
||||||
it.date_upload,
|
it.date_upload,
|
||||||
it.chapter_number,
|
it.chapter_number,
|
||||||
it.scanlator,
|
it.scanlator,
|
||||||
mangaId
|
mangaId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
|
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||||
return transaction {
|
return transaction {
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
|
||||||
val pagesList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = chapterEntry[ChapterTable.url]
|
url = chapterEntry[ChapterTable.url]
|
||||||
name = chapterEntry[ChapterTable.name]
|
name = chapterEntry[ChapterTable.name]
|
||||||
}
|
}
|
||||||
).toBlocking().first()
|
).toBlocking().first()
|
||||||
|
|
||||||
return@transaction pagesList.map {
|
val chapter = ChapterDataClass(
|
||||||
PageDataClass(
|
chapterEntry[ChapterTable.id].value,
|
||||||
it.index,
|
chapterEntry[ChapterTable.url],
|
||||||
getTrueImageUrl(it,source)
|
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
|
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.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||||
@@ -67,18 +71,17 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
return transaction {
|
return transaction {
|
||||||
return@transaction ExtensionsTable.selectAll().map {
|
return@transaction ExtensionsTable.selectAll().map {
|
||||||
ExtensionDataClass(
|
ExtensionDataClass(
|
||||||
it[ExtensionsTable.name],
|
it[ExtensionsTable.name],
|
||||||
it[ExtensionsTable.pkgName],
|
it[ExtensionsTable.pkgName],
|
||||||
it[ExtensionsTable.versionName],
|
it[ExtensionsTable.versionName],
|
||||||
it[ExtensionsTable.versionCode],
|
it[ExtensionsTable.versionCode],
|
||||||
it[ExtensionsTable.lang],
|
it[ExtensionsTable.lang],
|
||||||
it[ExtensionsTable.isNsfw],
|
it[ExtensionsTable.isNsfw],
|
||||||
it[ExtensionsTable.apkName],
|
it[ExtensionsTable.apkName],
|
||||||
it[ExtensionsTable.iconUrl],
|
it[ExtensionsTable.iconUrl],
|
||||||
it[ExtensionsTable.installed],
|
it[ExtensionsTable.installed],
|
||||||
it[ExtensionsTable.classFQName]
|
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
|
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 eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import ir.armor.tachidesk.Config
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
fun getManga(mangaId: Int): MangaDataClass {
|
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||||
return transaction {
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
|
||||||
|
|
||||||
return@transaction if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized]) {
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
mangaEntry[MangaTable.sourceReference].value,
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
mangaEntry[MangaTable.thumbnail_url],
|
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
mangaEntry[MangaTable.artist],
|
||||||
mangaEntry[MangaTable.author],
|
mangaEntry[MangaTable.author],
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[MangaTable.description],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
)
|
)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
val fetchedManga = source.fetchMangaDetails(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
url = mangaEntry[MangaTable.url]
|
url = mangaEntry[MangaTable.url]
|
||||||
title = mangaEntry[MangaTable.title]
|
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
|
|
||||||
}
|
}
|
||||||
|
).toBlocking().first()
|
||||||
|
|
||||||
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
|
||||||
MangaDataClass(
|
it[MangaTable.initialized] = true
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
|
||||||
|
|
||||||
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
mangaEntry[MangaTable.url],
|
it[MangaTable.author] = fetchedManga.author
|
||||||
mangaEntry[MangaTable.title],
|
it[MangaTable.description] = fetchedManga.description
|
||||||
mangaEntry[MangaTable.thumbnail_url],
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
|
it[MangaTable.status] = fetchedManga.status
|
||||||
true,
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
mangaEntry[MangaTable.artist],
|
}
|
||||||
mangaEntry[MangaTable.author],
|
|
||||||
mangaEntry[MangaTable.description],
|
|
||||||
mangaEntry[MangaTable.genre],
|
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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.MangaDataClass
|
||||||
|
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> {
|
fun 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 source = getHttpSource(sourceId.toLong())
|
||||||
val mangasPage = if (popular) {
|
val mangasPage = if (popular) {
|
||||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
source.fetchPopularManga(pageNum).toBlocking().first()
|
||||||
@@ -19,11 +27,16 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
|||||||
else
|
else
|
||||||
throw Exception("Source $source doesn't support latest")
|
throw Exception("Source $source doesn't support latest")
|
||||||
}
|
}
|
||||||
return transaction {
|
return mangasPage.processEntries(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||||
|
val mangasPage = this
|
||||||
|
val mangaList = transaction {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||||
var mangaEntityId = if (mangaEntry == null) { // create manga entry
|
if (mangaEntry == null) { // create manga entry
|
||||||
MangaTable.insertAndGetId {
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
it[url] = manga.url
|
it[url] = manga.url
|
||||||
it[title] = manga.title
|
it[title] = manga.title
|
||||||
|
|
||||||
@@ -32,21 +45,18 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
|||||||
it[description] = manga.description
|
it[description] = manga.description
|
||||||
it[genre] = manga.genre
|
it[genre] = manga.genre
|
||||||
it[status] = manga.status
|
it[status] = manga.status
|
||||||
it[thumbnail_url] = manga.genre
|
it[thumbnail_url] = manga.thumbnail_url
|
||||||
|
|
||||||
it[sourceReference] = sourceId
|
it[sourceReference] = sourceId
|
||||||
}.value
|
}.value
|
||||||
} else {
|
|
||||||
mangaEntry[MangaTable.id].value
|
|
||||||
}
|
|
||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaEntityId,
|
mangaId,
|
||||||
sourceId.toLong(),
|
sourceId,
|
||||||
|
|
||||||
manga.url,
|
manga.url,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.thumbnail_url,
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
manga.initialized,
|
manga.initialized,
|
||||||
|
|
||||||
@@ -55,7 +65,30 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
|||||||
manga.description,
|
manga.description,
|
||||||
manga.genre,
|
manga.genre,
|
||||||
MangaStatus.valueOf(manga.status).name,
|
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
|
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.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.Config
|
import ir.armor.tachidesk.Config
|
||||||
@@ -13,7 +17,7 @@ import org.jetbrains.exposed.sql.selectAll
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
||||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
||||||
@@ -39,16 +43,16 @@ fun getHttpSource(sourceId: Long): HttpSource {
|
|||||||
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
|
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
|
||||||
var usedCached = false
|
var usedCached = false
|
||||||
val instance =
|
val instance =
|
||||||
if (cachedExtensionPair != null) {
|
if (cachedExtensionPair != null) {
|
||||||
usedCached = true
|
usedCached = true
|
||||||
println("Used cached Extension")
|
println("Used cached Extension")
|
||||||
cachedExtensionPair.second
|
cachedExtensionPair.second
|
||||||
} else {
|
} else {
|
||||||
println("No Extension cache")
|
println("No Extension cache")
|
||||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
||||||
val classToLoad = Class.forName(className, true, child)
|
val classToLoad = Class.forName(className, true, child)
|
||||||
classToLoad.newInstance()
|
classToLoad.newInstance()
|
||||||
}
|
}
|
||||||
if (sourceRecord.partOfFactorySource) {
|
if (sourceRecord.partOfFactorySource) {
|
||||||
return@transaction if (usedCached) {
|
return@transaction if (usedCached) {
|
||||||
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
|
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
|
||||||
@@ -71,12 +75,26 @@ fun getSourceList(): List<SourceDataClass> {
|
|||||||
return transaction {
|
return transaction {
|
||||||
return@transaction SourceTable.selectAll().map {
|
return@transaction SourceTable.selectAll().map {
|
||||||
SourceDataClass(
|
SourceDataClass(
|
||||||
it[SourceTable.id].value.toString(),
|
it[SourceTable.id].value.toString(),
|
||||||
it[SourceTable.name],
|
it[SourceTable.name],
|
||||||
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
|
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
|
||||||
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
|
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
|
||||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
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
|
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.Config
|
||||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -8,7 +12,7 @@ fun applicationSetup() {
|
|||||||
// make dirs we need
|
// make dirs we need
|
||||||
File(Config.dataRoot).mkdirs()
|
File(Config.dataRoot).mkdirs()
|
||||||
File(Config.extensionsRoot).mkdirs()
|
File(Config.extensionsRoot).mkdirs()
|
||||||
|
File(Config.thumbnailsRoot).mkdirs()
|
||||||
|
|
||||||
makeDataBaseTables()
|
makeDataBaseTables()
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-7
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.util
|
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 com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
@@ -13,6 +17,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -28,8 +33,8 @@ fun installAPK(apkName: String): Int {
|
|||||||
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
|
||||||
// check if we don't have the dex file already downloaded
|
// check if we don't have the dex file already downloaded
|
||||||
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
if (!File(dexPath).exists()) {
|
if (!File(jarPath).exists()) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val api = ExtensionGithubApi()
|
val api = ExtensionGithubApi()
|
||||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
val apkToDownload = api.getApkUrl(extensionRecord)
|
||||||
@@ -41,7 +46,6 @@ fun installAPK(apkName: String): Int {
|
|||||||
// download apk file
|
// download apk file
|
||||||
downloadAPKFile(apkToDownload, apkFilePath)
|
downloadAPKFile(apkToDownload, apkFilePath)
|
||||||
|
|
||||||
|
|
||||||
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
||||||
println(className)
|
println(className)
|
||||||
// dex -> jar
|
// dex -> jar
|
||||||
@@ -60,7 +64,7 @@ fun installAPK(apkName: String): Int {
|
|||||||
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
|
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
|
val httpSource = instance as HttpSource
|
||||||
transaction {
|
transaction {
|
||||||
// SourceEntity.new {
|
// SourceEntity.new {
|
||||||
@@ -80,7 +84,6 @@ fun installAPK(apkName: String): Int {
|
|||||||
// println(httpSource.name)
|
// println(httpSource.name)
|
||||||
// println()
|
// println()
|
||||||
}
|
}
|
||||||
|
|
||||||
} else { // multi source
|
} else { // multi source
|
||||||
val sourceFactory = instance as SourceFactory
|
val sourceFactory = instance as SourceFactory
|
||||||
transaction {
|
transaction {
|
||||||
@@ -110,7 +113,6 @@ fun installAPK(apkName: String): Int {
|
|||||||
it[classFQName] = className
|
it[classFQName] = className
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return 201 // we downloaded successfully
|
return 201 // we downloaded successfully
|
||||||
} else {
|
} else {
|
||||||
@@ -122,10 +124,28 @@ val networkHelper: NetworkHelper by injectLazy()
|
|||||||
|
|
||||||
private fun downloadAPKFile(url: String, apkPath: String) {
|
private fun downloadAPKFile(url: String, apkPath: String) {
|
||||||
val request = Request.Builder().url(url).build()
|
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 downloadedFile = File(apkPath)
|
||||||
val sink = downloadedFile.sink().buffer()
|
val sink = downloadedFile.sink().buffer()
|
||||||
sink.writeAll(response.body!!.source())
|
sink.writeAll(response.body!!.source())
|
||||||
sink.close()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@ plugins {
|
|||||||
|
|
||||||
node {
|
node {
|
||||||
workDir = file("${project.projectDir}/react/")
|
workDir = file("${project.projectDir}/react/")
|
||||||
nodeModulesDir = file("${project.projectDir}/react/node_modules")
|
nodeModulesDir = file("${project.projectDir}/react/")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("yarn_build") {
|
tasks.named("yarn_build") {
|
||||||
dependsOn("yarn_install")
|
dependsOn("yarn") // install node_moduels
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyBuild") {
|
tasks.register<Copy>("copyBuild") {
|
||||||
|
|||||||
@@ -47,4 +47,4 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"typescript": "^4.1.0"
|
"typescript": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-25
@@ -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 {
|
import {
|
||||||
BrowserRouter as Router, Route, Switch,
|
BrowserRouter as Router, Route, Switch,
|
||||||
} from 'react-router-dom';
|
} 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 NavBar from './components/NavBar';
|
||||||
import Home from './screens/Home';
|
import Home from './screens/Home';
|
||||||
import Sources from './screens/Sources';
|
import Sources from './screens/Sources';
|
||||||
@@ -9,35 +17,79 @@ import Extensions from './screens/Extensions';
|
|||||||
import MangaList from './screens/MangaList';
|
import MangaList from './screens/MangaList';
|
||||||
import Manga from './screens/Manga';
|
import Manga from './screens/Manga';
|
||||||
import Reader from './screens/Reader';
|
import Reader from './screens/Reader';
|
||||||
|
import Search from './screens/SearchSingle';
|
||||||
|
import NavBarTitle from './context/NavbarTitle';
|
||||||
|
import DarkTheme from './context/DarkTheme';
|
||||||
|
|
||||||
export default function App() {
|
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 (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<NavBar />
|
|
||||||
|
|
||||||
<Switch>
|
<ThemeProvider theme={theme}>
|
||||||
<Route path="/extensions">
|
<NavBarTitle.Provider value={navTitleContext}>
|
||||||
<Extensions />
|
<CssBaseline />
|
||||||
</Route>
|
<DarkTheme.Provider value={darkThemeContext}>
|
||||||
<Route path="/sources/:sourceId/popular/">
|
<NavBar />
|
||||||
<MangaList popular />
|
</DarkTheme.Provider>
|
||||||
</Route>
|
<Container maxWidth={false} disableGutters>
|
||||||
<Route path="/sources/:sourceId/latest/">
|
<Switch>
|
||||||
<MangaList popular={false} />
|
<Route path="/sources/:sourceId/search/">
|
||||||
</Route>
|
<Search />
|
||||||
<Route path="/sources">
|
</Route>
|
||||||
<Sources />
|
<Route path="/extensions">
|
||||||
</Route>
|
<Extensions />
|
||||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
</Route>
|
||||||
<Reader />
|
<Route path="/sources/:sourceId/popular/">
|
||||||
</Route>
|
<MangaList popular />
|
||||||
<Route path="/manga/:id">
|
</Route>
|
||||||
<Manga />
|
<Route path="/sources/:sourceId/latest/">
|
||||||
</Route>
|
<MangaList popular={false} />
|
||||||
<Route path="/">
|
</Route>
|
||||||
<Home />
|
<Route path="/sources">
|
||||||
</Route>
|
<Sources />
|
||||||
</Switch>
|
</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>
|
</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 React from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
import Card from '@material-ui/core/Card';
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
import Card from '@material-ui/core/Card';
|
||||||
@@ -42,7 +46,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
name, lang, versionName, iconUrl, installed, apkName,
|
name, lang, versionName, iconUrl, installed, apkName,
|
||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
|
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||||
@@ -50,10 +54,25 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
function install() {
|
function install() {
|
||||||
setInstalledState('installing');
|
setInstalledState('installing');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className={classes.root}>
|
<CardContent className={classes.root}>
|
||||||
@@ -76,7 +95,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
|
<Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 React from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
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 CardMedia from '@material-ui/core/CardMedia';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Grid } from '@material-ui/core';
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -39,7 +44,7 @@ const useStyles = makeStyles({
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
manga: IManga
|
manga: IManga
|
||||||
}
|
}
|
||||||
export default function MangaCard(props: IProps) {
|
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||||
const {
|
const {
|
||||||
manga: {
|
manga: {
|
||||||
id, title, thumbnailUrl,
|
id, title, thumbnailUrl,
|
||||||
@@ -48,22 +53,26 @@ export default function MangaCard(props: IProps) {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/manga/${id}/`}>
|
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||||
<Card className={classes.root}>
|
<Link to={`/manga/${id}/`}>
|
||||||
<CardActionArea>
|
<Card className={classes.root} ref={ref}>
|
||||||
<div className={classes.wrapper}>
|
<CardActionArea>
|
||||||
<CardMedia
|
<div className={classes.wrapper}>
|
||||||
className={classes.image}
|
<CardMedia
|
||||||
component="img"
|
className={classes.image}
|
||||||
alt={title}
|
component="img"
|
||||||
image={thumbnailUrl}
|
alt={title}
|
||||||
title={title}
|
image={thumbnailUrl}
|
||||||
/>
|
title={title}
|
||||||
<div className={classes.gradient} />
|
/>
|
||||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
<div className={classes.gradient} />
|
||||||
</div>
|
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||||
</CardActionArea>
|
</div>
|
||||||
</Card>
|
</CardActionArea>
|
||||||
</Link>
|
</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';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
|
|||||||
@@ -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...',
|
||||||
|
};
|
||||||
@@ -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 { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import MoreIcon from '@material-ui/icons/MoreVert';
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import Menu from '@material-ui/core/Menu';
|
||||||
|
|
||||||
import TemporaryDrawer from './TemporaryDrawer';
|
import TemporaryDrawer from './TemporaryDrawer';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -19,13 +29,35 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// const theme = createMuiTheme({
|
||||||
|
// overrides: {
|
||||||
|
// MuiAppBar: {
|
||||||
|
// colorPrimary: { backgroundColor: '#FFC0CB' },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// palette: { type: 'dark' },
|
||||||
|
// });
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [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 (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<AppBar position="static">
|
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
@@ -38,8 +70,44 @@ export default function NavBar() {
|
|||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6" className={classes.title}>
|
<Typography variant="h6" className={classes.title}>
|
||||||
Tachidesk
|
{title}
|
||||||
</Typography>
|
</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>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
import Card from '@material-ui/core/Card';
|
||||||
@@ -65,8 +69,9 @@ export default function SourceCard(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/latest/`; }}>Latest</Button>}
|
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
|
||||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/popular/`; }}>Browse</Button>
|
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
|
||||||
|
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -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 React from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Drawer from '@material-ui/core/Drawer';
|
import Drawer from '@material-ui/core/Drawer';
|
||||||
@@ -48,6 +52,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<ListItemText primary="Sources" />
|
<ListItemText primary="Sources" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Search">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Global Search" />
|
||||||
|
</ListItem>
|
||||||
|
</Link> */}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -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 React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|||||||
Vendored
+4
@@ -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" />
|
/// <reference types="react-scripts" />
|
||||||
|
|||||||
@@ -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';
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
|||||||
@@ -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 ExtensionCard from '../components/ExtensionCard';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
export default function Extensions() {
|
export default function Extensions() {
|
||||||
let mapped;
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
setTitle('Extensions');
|
||||||
const [extensions, setExtensions] = useState<IExtension[]>([]);
|
const [extensions, setExtensions] = useState<IExtension[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,10 +18,7 @@ export default function Extensions() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (extensions.length === 0) {
|
if (extensions.length === 0) {
|
||||||
mapped = <h3>wait</h3>;
|
return <h3>wait</h3>;
|
||||||
} else {
|
|
||||||
mapped = extensions.map((it) => <ExtensionCard extension={it} />);
|
|
||||||
}
|
}
|
||||||
|
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
|
||||||
return <h2>{mapped}</h2>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 React from 'react';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
@@ -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 { useParams } from 'react-router-dom';
|
||||||
import ChapterCard from '../components/ChapterCard';
|
import ChapterCard from '../components/ChapterCard';
|
||||||
import MangaDetails from '../components/MangaDetails';
|
import MangaDetails from '../components/MangaDetails';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
const { id } = useParams<{id: string}>();
|
const { id } = useParams<{id: string}>();
|
||||||
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
|
||||||
const [manga, setManga] = useState<IManga>();
|
const [manga, setManga] = useState<IManga>();
|
||||||
const [chapters, setChapters] = useState<IChapter[]>([]);
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
@@ -12,7 +18,10 @@ export default function Manga() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => setManga(data));
|
.then((data: IManga) => {
|
||||||
|
setManga(data);
|
||||||
|
setTitle(data.title);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -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 { useParams } from 'react-router-dom';
|
||||||
import MangaCard from '../components/MangaCard';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
export default function MangaList(props: { popular: boolean }) {
|
export default function MangaList(props: { popular: boolean }) {
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
let mapped;
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||||
const [lastPageNum] = useState<number>(1);
|
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||||
|
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: { name: string }) => setTitle(data.name));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sourceType = props.popular ? 'popular' : 'latest';
|
const sourceType = props.popular ? 'popular' : 'latest';
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas(
|
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||||
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })),
|
setMangas([
|
||||||
));
|
...mangas,
|
||||||
}, []);
|
...data.mangaList.map((it) => ({
|
||||||
|
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
|
||||||
|
}))]);
|
||||||
|
setHasNextPage(data.hasNextPage);
|
||||||
|
});
|
||||||
|
}, [lastPageNum]);
|
||||||
|
|
||||||
if (mangas.length === 0) {
|
return (
|
||||||
mapped = <h3>wait</h3>;
|
<MangaGrid
|
||||||
} else {
|
mangas={mangas}
|
||||||
mapped = (
|
hasNextPage={hasNextPage}
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
|
lastPageNum={lastPageNum}
|
||||||
{mangas.map((it) => (
|
setLastPageNum={setLastPageNum}
|
||||||
<MangaCard manga={it} />
|
/>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapped;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { useParams } from 'react-router-dom';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -9,34 +14,36 @@ const style = {
|
|||||||
backgroundColor: '#343a40',
|
backgroundColor: '#343a40',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
interface IPage {
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
index: number
|
|
||||||
imageUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Reader() {
|
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}>();
|
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => setPages(data));
|
.then((data:IChapter) => {
|
||||||
|
setTitle(data.name);
|
||||||
|
setPageCount(data.pageCount);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
pages.sort((a, b) => (a.index - b.index));
|
if (pageCount === -1) {
|
||||||
|
return (
|
||||||
let mapped;
|
<div style={style}>
|
||||||
if (pages.length === 0) {
|
<h3>wait</h3>
|
||||||
mapped = <h3>wait</h3>;
|
|
||||||
} else {
|
|
||||||
mapped = pages.map(({ imageUrl }) => (
|
|
||||||
<div style={{ margin: '0 auto' }}>
|
|
||||||
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
|
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
{mapped}
|
{mapped}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 SourceCard from '../components/SourceCard';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
export default function Sources() {
|
export default function Sources() {
|
||||||
let mapped;
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
setTitle('Sources');
|
||||||
const [sources, setSources] = useState<ISource[]>([]);
|
const [sources, setSources] = useState<ISource[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -12,10 +18,7 @@ export default function Sources() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (sources.length === 0) {
|
if (sources.length === 0) {
|
||||||
mapped = <h3>wait</h3>;
|
return (<h3>wait</h3>);
|
||||||
} else {
|
|
||||||
mapped = sources.map((it) => <SourceCard source={it} />);
|
|
||||||
}
|
}
|
||||||
|
return <>{sources.map((it) => <SourceCard source={it} />)}</>;
|
||||||
return <h2>{mapped}</h2>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -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 {
|
interface IExtension {
|
||||||
name: string
|
name: string
|
||||||
lang: string
|
lang: string
|
||||||
@@ -30,4 +34,5 @@ interface IChapter {
|
|||||||
chapter_number: number
|
chapter_number: number
|
||||||
scanlator: String
|
scanlator: String
|
||||||
mangaId: number
|
mangaId: number
|
||||||
|
pageCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user