Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92ed48f7f6 | |||
| 13e84bc492 | |||
| 0ef86c34b7 | |||
| 7e1a4259d7 | |||
| c842c51fb6 | |||
| 6f2f228e08 | |||
| c78eaa8b96 | |||
| f9606526d2 | |||
| fe4cc9ea2c | |||
| 54d0c05fcc | |||
| 2f7df73a37 | |||
| cf19f3626b | |||
| ff2da5e59b | |||
| e03922e518 | |||
| 893fba5b8c | |||
| c1786f8e24 | |||
| a59f974537 | |||
| 7157e07328 | |||
| 954084bd82 | |||
| 0915ba40f6 | |||
| de30d55bcf | |||
| af1c34fba5 | |||
| 7b7d93786f | |||
| 7c1c504482 | |||
| 33b22fcab6 | |||
| ab0566dcba | |||
| c4f2cc7189 | |||
| 4626d99590 | |||
| 6465ca8a19 | |||
| 15b9d151df | |||
| dd1b6c86cd | |||
| 9613cda79a | |||
| 648b8e5960 | |||
| ce545b1fd5 | |||
| 9151034fbc | |||
| 312a8baa13 | |||
| 18b6168cd1 | |||
| 9a282c3bf4 | |||
| 2bbebe4c30 | |||
| 162961b560 | |||
| f1cc37d0db | |||
| 5a9d216fb7 | |||
| bf37d3be7c | |||
| 7fd57aaed8 | |||
| d996c44b24 | |||
| 6f3052dd1b | |||
| d2b1bfdcdd | |||
| 945fb99594 | |||
| 09d624a4e2 | |||
| eb90db7ce6 | |||
| b56f9391b8 | |||
| c181478909 | |||
| 76b31e734c | |||
| ed8bd76d95 | |||
| 3051a72d7f | |||
| 3a33bf3a5d | |||
| 7959ba2664 | |||
| fe6568b82c | |||
| c228648bb6 | |||
| fdaeb6d1fa | |||
| ba45e18399 | |||
| 3e2bf877d4 | |||
| c80d344046 | |||
| 2364f10d8d | |||
| 2602275c20 | |||
| d113311f4e | |||
| 8d95701e8e | |||
| 0d2c54a5ed | |||
| 6506c84b85 | |||
| 69bb38b487 | |||
| 95e17f2b50 | |||
| 9625da9221 | |||
| c1659f1cf2 | |||
| c46ee764ac | |||
| 7aada85f76 | |||
| 145cbe3e4f | |||
| cb8dd8259d | |||
| b8e721fd27 | |||
| 7917b5384c | |||
| 087b7554bf | |||
| fb5f851a2a | |||
| 7ac51f8c2a | |||
| e5e40a986c | |||
| 7a27436868 | |||
| a5bab7425d | |||
| 93d5ab3739 | |||
| 3146fefb55 | |||
| 1ea51bb9df | |||
| 98bd664ab6 | |||
| 61aee2e784 | |||
| 22bf49078f | |||
| 7284e0d4ae | |||
| d39d075b1a | |||
| 0f6749b0c1 | |||
| 771030b911 | |||
| 8d5744a2cf | |||
| 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 |
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "🐞 Bug report"
|
||||
title: "[Bug] <short description>"
|
||||
about: "Report a bug"
|
||||
labels: "bug"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## Device information
|
||||
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
||||
- Server Operating System: (Example: Ubuntu 20.04)
|
||||
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
||||
- Client Operating System: <usually the same as above Server Operating System>
|
||||
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
||||
|
||||
## Steps to reproduce
|
||||
1. First Step
|
||||
2. Second Step
|
||||
|
||||
### Expected behavior
|
||||
Describe what should have happened
|
||||
|
||||
### Actual behavior
|
||||
Describe what happens instead
|
||||
|
||||
## Other details
|
||||
Describe additional details If necessary
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "🌟 Feature request"
|
||||
title: "[Feature Request] <short description>"
|
||||
about: "Suggest a feature to improve the project"
|
||||
labels: "enhancement"
|
||||
---
|
||||
|
||||
**PLEASE READ THIS**
|
||||
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated to the latest version of the app.
|
||||
- I have tried the troubleshooting guide described in `README.md`
|
||||
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
|
||||
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
||||
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||
|
||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||
|
||||
---
|
||||
|
||||
## What feature should be added to Tachidesk?
|
||||
Explain What the feature is and how it should work in detail
|
||||
|
||||
## Why/Project's Benefit/Existing Problem
|
||||
Explain why this should be added
|
||||
@@ -1,10 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
cp ../master/repo/* .
|
||||
new_build=$(ls | tail -1)
|
||||
echo "New build file name: $new_build"
|
||||
git lfs install
|
||||
#git lfs track "*.zip"
|
||||
|
||||
cp -f $new_build Tachidesk-latest.jar
|
||||
cp ../master/repo/* .
|
||||
new_jar_build=$(ls *.jar| tail -1)
|
||||
echo "last jar build file name: $new_jar_build"
|
||||
|
||||
new_win32_build=$(ls *.zip| tail -1)
|
||||
echo "last win32 build file name: $new_win32_build"
|
||||
|
||||
cp -f $new_jar_build Tachidesk-latest.jar
|
||||
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
||||
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
#!/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 -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
||||
#echo "count is: $filter_count"
|
||||
|
||||
mkdir -p repo/
|
||||
cp server/build/Tachidesk-*.jar repo/
|
||||
cp server/build/Tachidesk-*.zip repo/
|
||||
|
||||
# Get last commit message
|
||||
last_commit_log=$(git log -1 --pretty=format:"%s")
|
||||
echo "last commit log: $last_commit_log"
|
||||
ls repo
|
||||
pwd
|
||||
|
||||
filter_count=$(echo "$last_commit_log" | grep -c "[RELEASE CI]" )
|
||||
|
||||
if [ "$filter_count" -gt 0 ]; then
|
||||
cp server/build/Tachidesk-*.jar repo/
|
||||
fi
|
||||
#if [ "$filter_count" -gt 0 ]; then
|
||||
# cp server/build/Tachidesk-*.jar repo/
|
||||
# cp server/build/Tachidesk-*.zip repo/
|
||||
#fi
|
||||
@@ -48,37 +48,24 @@ jobs:
|
||||
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'
|
||||
- name: Download android.jar
|
||||
run: |
|
||||
cd master
|
||||
./scripts/getAndroid.sh
|
||||
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Build the Jar
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:shadowJar --stacktrace
|
||||
arguments: :server:windowsPackage --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
|
||||
configuration-cache-enabled: true
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Issue closer
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.0
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "title",
|
||||
"regex": ".*<short description>*",
|
||||
"message": "You did not fill out the description in the title"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||
"message": "The requested information was not filled out"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
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
|
||||
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 android.jar
|
||||
run: |
|
||||
cd master
|
||||
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/react/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Build Jar and launch4j
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
build-root-directory: master
|
||||
wrapper-directory: master
|
||||
arguments: :server:windowsPackage --stacktrace
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
|
||||
- name: Create repo artifacts
|
||||
run: |
|
||||
cd master
|
||||
./.github/scripts/create-repo.sh
|
||||
|
||||
- name: Upload Release
|
||||
uses: xresloader/upload-to-github-release@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
file: "master/repo/*"
|
||||
tags: true
|
||||
draft: true
|
||||
verbose: true
|
||||
|
||||
# - name: Create Release
|
||||
# id: create_release
|
||||
# uses: actions/create-release@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# tag_name: ${{ github.ref }}
|
||||
# release_name: Release ${{ github.ref }}
|
||||
# body: |
|
||||
# Release body
|
||||
# draft: false
|
||||
# prerelease: true
|
||||
#
|
||||
# - name: Get the Ref
|
||||
# id: get-ref
|
||||
# uses: ankitvgupta/ref-to-tag-action@master
|
||||
# with:
|
||||
# ref: ${{ github.ref }}
|
||||
# head_ref: ${{ github.head_ref }}
|
||||
#
|
||||
# - name: Get the tag
|
||||
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
|
||||
#
|
||||
# - name: Upload Release
|
||||
# uses: AButler/upload-release-assets@v2.0
|
||||
# with:
|
||||
# files: 'master/repo/*'
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# release-tag: ${{ steps.get-ref.outputs.tag }}
|
||||
@@ -1,4 +1,4 @@
|
||||
dependencies {
|
||||
// Config API
|
||||
// Config API, moved to the global build.gradle
|
||||
// implementation("com.typesafe:config:1.4.0")
|
||||
}
|
||||
@@ -4,54 +4,55 @@ import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import mu.KotlinLogging
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Manages app config.
|
||||
*/
|
||||
open class ConfigManager {
|
||||
private val generatedModules
|
||||
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
|
||||
|
||||
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||
val config by lazy { loadConfigs() }
|
||||
|
||||
//Public read-only view of modules
|
||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||
get() = generatedModules
|
||||
|
||||
open val configFolder: String
|
||||
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
|
||||
open val appConfigFile: String = "$dataRoot/server.conf"
|
||||
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Get a config module
|
||||
*/
|
||||
inline fun <reified T : ConfigModule> module(): T
|
||||
= loadedModules[T::class.java] as T
|
||||
inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
|
||||
|
||||
/**
|
||||
* Get a config module (Java API)
|
||||
*/
|
||||
fun <T : ConfigModule> module(type: Class<T>): T
|
||||
= loadedModules[type] as T
|
||||
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
||||
|
||||
/**
|
||||
* Load configs
|
||||
*/
|
||||
fun loadConfigs(): Config {
|
||||
val configs = mutableListOf<Config>()
|
||||
//Load reference configs
|
||||
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||
|
||||
//Load reference config
|
||||
configs += ConfigFactory.parseResources("reference.conf")
|
||||
//Load user config
|
||||
val userConfig =
|
||||
File(appConfigFile).let{
|
||||
ConfigFactory.parseFile(it)
|
||||
}
|
||||
|
||||
//Load custom configs from dir
|
||||
File(configFolder).listFiles()?.map {
|
||||
ConfigFactory.parseFile(it)
|
||||
}?.filterNotNull()?.forEach {
|
||||
configs += it.withFallback(configs.last())
|
||||
}
|
||||
|
||||
val config = configs.last().resolve()
|
||||
val config = ConfigFactory.empty()
|
||||
.withFallback(userConfig)
|
||||
.withFallback(compatConfig)
|
||||
.withFallback(serverConfig)
|
||||
.resolve()
|
||||
|
||||
logger.debug {
|
||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import java.io.File
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip = config.getString("ip")
|
||||
val port = config.getInt("port")
|
||||
|
||||
val allowConfigChanges = config.getBoolean("allowConfigChanges")
|
||||
val enableWebUi = config.getBoolean("enableWebUi")
|
||||
val useOldWebUi = config.getBoolean("useOldWebUi")
|
||||
val prettyPrintApi = config.getBoolean("prettyPrintApi")
|
||||
// TODO Apply to operation IDs
|
||||
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
|
||||
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
|
||||
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
|
||||
|
||||
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
|
||||
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
|
||||
|
||||
val rootDir = registerFile(config.getString("rootDir"))
|
||||
val patchesDir = registerFile(config.getString("patchesDir"))
|
||||
|
||||
fun registerFile(file: String): File {
|
||||
return File(file).apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(config: Config)
|
||||
= ServerConfig(config.getConfig("ts.server"))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
# Server ip and port bindings
|
||||
ts.server.ip = 0.0.0.0
|
||||
ts.server.port = 4567
|
||||
|
||||
# Allow/disallow preference changes (useful for demos)
|
||||
ts.server.allowConfigChanges = true
|
||||
@@ -1,29 +1,70 @@
|
||||
|
||||

|
||||
# Tachidesk
|
||||
A free and open source manga reader than runs extensions built for [Tachiyomi](https://tachiyomi.org/) which runs on desktop operating systems.
|
||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
|
||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
||||
|
||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||
|
||||
## Is this application usable? Should I test it?
|
||||
Here is a list of current features:
|
||||
|
||||
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
||||
- A library to save your mangas and categories to put them into.
|
||||
- Searching and browsing installed sources.
|
||||
- A minimal chapter reader.
|
||||
- Ability to download Mangas for offline read(This partially works)
|
||||
|
||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
||||
|
||||
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 in more detail.
|
||||
|
||||
## Downloading and Running the app
|
||||
#### Prerequisites
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||
|
||||
#### Download the app
|
||||
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||
|
||||
#### Running pre-built jar packages
|
||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||
|
||||
#### Running pre-built Windows packages
|
||||
Windows specific builds have java bundled inside them, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`, the rest will work like the jar release.
|
||||
|
||||
#### Running on Docker
|
||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||
|
||||
## General troubleshooting
|
||||
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
|
||||
|
||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
||||
|
||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
||||
|
||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
|
||||
|
||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||
|
||||
## Support and help
|
||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
||||
|
||||
## 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.
|
||||
|
||||
## How do I run the thing?
|
||||
#### Running pre-built jar packages
|
||||
Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||
|
||||
Double click on the jar file or run `java -jar Tachidesk-latest.jar` or `java -jar Tachidesk-vX.Y.Z-rxxx.jar`
|
||||
|
||||
The server will be running on `http://localhost:4567` open this url in your browser.
|
||||
|
||||
## Building from source
|
||||
### Get Android stubs jar
|
||||
#### Manual download
|
||||
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)
|
||||
#### Automated download(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.
|
||||
### building the jar
|
||||
Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building the Windows package
|
||||
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||
## Running for development purposes
|
||||
### `server` module
|
||||
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||
@@ -34,14 +75,15 @@ How to do it is described in `webUI/react/README.md` but for short,
|
||||
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 this application usable? Should I test it?
|
||||
Checkout [the state of project](https://github.com/AriaMoradi/Tachidesk/issues/2) to see what's implemented.
|
||||
|
||||
## Credit
|
||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||
|
||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||
|
||||
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||
|
||||
@@ -76,5 +76,8 @@ configure(listOf(
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.0")
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.0")
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ plugins {
|
||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
||||
application
|
||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||
id("org.jmailen.kotlinter") version "3.3.0"
|
||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.0.2"
|
||||
val TachideskVersion = "v0.2.4"
|
||||
|
||||
|
||||
repositories {
|
||||
@@ -70,15 +72,16 @@ dependencies {
|
||||
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.0")
|
||||
|
||||
// Exposed ORM
|
||||
val exposed_version = "0.28.1"
|
||||
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||
implementation ("org.xerial:sqlite-jdbc:3.30.1")
|
||||
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||
implementation("com.h2database:h2:1.4.199")
|
||||
|
||||
// tray icon
|
||||
implementation("com.dorkbox:SystemTray:3.17")
|
||||
|
||||
|
||||
// AndroidCompat
|
||||
implementation(project(":AndroidCompat"))
|
||||
@@ -89,8 +92,8 @@ dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
}
|
||||
|
||||
val name = "ir.armor.tachidesk.Main"
|
||||
application {
|
||||
val name = "ir.armor.tachidesk.Main"
|
||||
mainClass.set(name)
|
||||
|
||||
// Required by ShadowJar.
|
||||
@@ -114,7 +117,7 @@ val TachideskRevision = Runtime
|
||||
it.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
process.destroy()
|
||||
"r"+output.trim()
|
||||
"r" + output.trim()
|
||||
|
||||
}
|
||||
|
||||
@@ -137,11 +140,75 @@ tasks {
|
||||
}
|
||||
}
|
||||
|
||||
launch4j { //used for windows
|
||||
mainClassName = name
|
||||
bundledJrePath = "jre"
|
||||
bundledJre64Bit = true
|
||||
jreMinVersion = "8"
|
||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
||||
}
|
||||
|
||||
tasks.register<Zip>("windowsPackage") {
|
||||
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
|
||||
destinationDirectory.set(File("$buildDir"))
|
||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
||||
dependsOn("windowsPackageWorkaround2")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||
)
|
||||
dependsOn("windowsPackageWorkaround")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
dependsOn("deleteUnwantedJreDir")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
||||
delete(
|
||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
||||
)
|
||||
dependsOn("addJreToDistributable")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("addJreToDistributable") {
|
||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
||||
eachFile {
|
||||
path = path.replace(".*-jre".toRegex(),"jre")
|
||||
}
|
||||
dependsOn("downloadJre")
|
||||
dependsOn("createExe")
|
||||
}
|
||||
|
||||
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
||||
dest(buildDir)
|
||||
overwrite(false)
|
||||
onlyIfModified(true)
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
destinationDir = File("$rootDir/server/build")
|
||||
dependsOn("lintKotlin")
|
||||
}
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(":webUI:copyBuild")
|
||||
}
|
||||
|
||||
tasks.named("run") {
|
||||
dependsOn("formatKotlin", "lintKotlin")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk;
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
@@ -2,9 +2,9 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
//import android.content.res.Configuration
|
||||
//import android.support.multidex.MultiDex
|
||||
//import timber.log.Timber
|
||||
// import android.content.res.Configuration
|
||||
// import android.support.multidex.MultiDex
|
||||
// import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektScope
|
||||
import uy.kohesive.injekt.registry.default.DefaultRegistrar
|
||||
|
||||
@@ -2,19 +2,22 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import com.google.gson.Gson
|
||||
//import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
//import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
//import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
//import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
//import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
|
||||
//import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
//import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
// import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
// import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
// import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
// import eu.kanade.tachiyomi.data.sync.LibrarySyncManager
|
||||
// import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import rx.Observable
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.api.*
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
@@ -56,11 +59,9 @@ class AppModule(val app: Application) : InjektModule {
|
||||
}
|
||||
|
||||
// rxAsync { get<DatabaseHelper>() }
|
||||
|
||||
}
|
||||
|
||||
private fun rxAsync(block: () -> Unit) {
|
||||
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
//import android.content.Context
|
||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
//import kotlinx.coroutines.Dispatchers
|
||||
//import kotlinx.coroutines.withContext
|
||||
// import kotlinx.coroutines.Dispatchers
|
||||
// import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
internal class ExtensionGithubApi {
|
||||
|
||||
@@ -27,7 +27,7 @@ internal class ExtensionGithubApi {
|
||||
// suspend fun checkForUpdates(): List<Extension.Installed> {
|
||||
// val extensions = fin dExtensions()
|
||||
//
|
||||
//// preferences.lastExtCheck().set(Date().time)
|
||||
// // preferences.lastExtCheck().set(Date().time)
|
||||
//
|
||||
// val installedExtensions = ExtensionLoader.loadExtensions(context)
|
||||
// .filterIsInstance<LoadResult.Success>()
|
||||
@@ -49,23 +49,23 @@ internal class ExtensionGithubApi {
|
||||
|
||||
private fun parseResponse(json: JsonArray): List<Extension.Available> {
|
||||
return json
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
.filter { element ->
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map { element ->
|
||||
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
|
||||
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
|
||||
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
|
||||
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
|
||||
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
|
||||
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
|
||||
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
|
||||
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
|
||||
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
|
||||
@@ -9,7 +9,7 @@ import retrofit2.Retrofit
|
||||
import retrofit2.http.GET
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Used to get the extension repo listing from GitHub.
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
//import android.annotation.SuppressLint
|
||||
//import android.content.Context
|
||||
//import android.content.pm.PackageInfo
|
||||
//import android.content.pm.PackageManager
|
||||
//import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.annoations.Nsfw
|
||||
//import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
//import eu.kanade.tachiyomi.util.lang.Hash
|
||||
//import kotlinx.coroutines.async
|
||||
//import kotlinx.coroutines.runBlocking
|
||||
//import timber.log.Timber
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
// import android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.content.pm.PackageInfo
|
||||
// import android.content.pm.PackageManager
|
||||
// import dalvik.system.PathClassLoader
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
// import eu.kanade.tachiyomi.util.lang.Hash
|
||||
// import kotlinx.coroutines.async
|
||||
// import kotlinx.coroutines.runBlocking
|
||||
// import timber.log.Timber
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* Class that handles the loading of the extensions installed in the system.
|
||||
*/
|
||||
//@SuppressLint("PackageManagerGetSignatures")
|
||||
// @SuppressLint("PackageManagerGetSignatures")
|
||||
internal object ExtensionLoader {
|
||||
|
||||
// private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
@@ -1,30 +1,23 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
//import android.annotation.SuppressLint
|
||||
//import android.content.Context
|
||||
//import android.os.Build
|
||||
//import android.os.Handler
|
||||
//import android.os.Looper
|
||||
//import android.webkit.WebSettings
|
||||
//import android.webkit.WebView
|
||||
//import android.widget.Toast
|
||||
//import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
//import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
//import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
//import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
//import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
//import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
//import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
// import android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.os.Build
|
||||
// import android.os.Handler
|
||||
// import android.os.Looper
|
||||
// import android.webkit.WebSettings
|
||||
// import android.webkit.WebView
|
||||
// import android.widget.Toast
|
||||
// import eu.kanade.tachiyomi.R
|
||||
// import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
// import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
// import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
// import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
// import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
// import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class CloudflareInterceptor() : Interceptor {
|
||||
|
||||
@@ -77,7 +70,7 @@ class CloudflareInterceptor() : Interceptor {
|
||||
// }
|
||||
}
|
||||
//
|
||||
//// @SuppressLint("SetJavaScriptEnabled")
|
||||
// // @SuppressLint("SetJavaScriptEnabled")
|
||||
// private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
||||
// // We need to lock this thread until the WebView finds the challenge solution url, because
|
||||
// // OkHttp doesn't support asynchronous interceptors.
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class MemoryCookieJar : CookieJar {
|
||||
private val cache = mutableSetOf<WrappedCookie>()
|
||||
|
||||
@Synchronized
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val cookiesToRemove = mutableSetOf<WrappedCookie>()
|
||||
val validCookies = mutableSetOf<WrappedCookie>()
|
||||
|
||||
cache.forEach { cookie ->
|
||||
if (cookie.isExpired()) {
|
||||
cookiesToRemove.add(cookie)
|
||||
} else if (cookie.matches(url)) {
|
||||
validCookies.add(cookie)
|
||||
}
|
||||
}
|
||||
|
||||
cache.removeAll(cookiesToRemove)
|
||||
|
||||
return validCookies.toList().map(WrappedCookie::unwrap)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
|
||||
|
||||
cache.removeAll(cookiesToAdd)
|
||||
cache.addAll(cookiesToAdd)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||
fun unwrap() = cookie
|
||||
|
||||
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||
|
||||
fun matches(url: HttpUrl) = cookie.matches(url)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is WrappedCookie) return false
|
||||
|
||||
return other.cookie.name == cookie.name &&
|
||||
other.cookie.domain == cookie.domain &&
|
||||
other.cookie.path == cookie.path &&
|
||||
other.cookie.secure == cookie.secure &&
|
||||
other.cookie.hostOnly == cookie.hostOnly
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var hash = 17
|
||||
hash = 31 * hash + cookie.name.hashCode()
|
||||
hash = 31 * hash + cookie.domain.hashCode()
|
||||
hash = 31 * hash + cookie.path.hashCode()
|
||||
hash = 31 * hash + if (cookie.secure) 0 else 1
|
||||
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
|
||||
return hash
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
//import android.content.Context
|
||||
//import eu.kanade.tachiyomi.BuildConfig
|
||||
//import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.BuildConfig
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import android.content.Context
|
||||
import okhttp3.Cache
|
||||
//import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
// import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
//import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
//import okhttp3.logging.HttpLoggingInterceptor
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.net.InetAddress
|
||||
// import okhttp3.dnsoverhttps.DnsOverHttps
|
||||
// import okhttp3.logging.HttpLoggingInterceptor
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
@@ -22,14 +19,17 @@ class NetworkHelper(context: Context) {
|
||||
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
|
||||
// val cookieManager = AndroidCookieJar()
|
||||
val cookieManager = MemoryCookieJar()
|
||||
|
||||
val client by lazy {
|
||||
val builder = OkHttpClient.Builder()
|
||||
// .cookieJar(cookieManager)
|
||||
.cookieJar(cookieManager)
|
||||
// .cache(Cache(cacheDir, cacheSize))
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.MINUTES)
|
||||
.writeTimeout(5, TimeUnit.MINUTES)
|
||||
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||
|
||||
// .addInterceptor(UserAgentInterceptor())
|
||||
|
||||
// if (BuildConfig.DEBUG) {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
//import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
// import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
fun Call.asObservable(): Observable<Response> {
|
||||
return Observable.unsafeCreate { subscriber ->
|
||||
@@ -38,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
|
||||
override fun unsubscribe() {
|
||||
call.cancel()
|
||||
// call.cancel()
|
||||
}
|
||||
|
||||
override fun isUnsubscribed(): Boolean {
|
||||
@@ -52,7 +48,7 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
//suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||
// suspend fun Call.await(assertSuccess: Boolean = false): Response {
|
||||
// return suspendCancellableCoroutine { continuation ->
|
||||
// enqueue(
|
||||
// object : Callback {
|
||||
@@ -81,20 +77,21 @@ fun Call.asObservable(): Observable<Response> {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
return asObservable()
|
||||
.doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
// val progressClient = newBuilder()
|
||||
// .cache(null)
|
||||
// .cache(nasObservableSuccessull)
|
||||
// .addNetworkInterceptor { chain ->
|
||||
// val originalResponse = chain.proceed(chain.request())
|
||||
// originalResponse.newBuilder()
|
||||
@@ -104,11 +101,11 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
||||
// .build()
|
||||
//
|
||||
// return progressClient.newCall(request)
|
||||
//}
|
||||
// }
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
// .cache(null)
|
||||
// .addNetworkInterceptor { chain ->
|
||||
// val originalResponse = chain.proceed(chain.request())
|
||||
// originalResponse.newBuilder()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
//import androidx.preference.PreferenceScreen
|
||||
// import androidx.preference.PreferenceScreen
|
||||
|
||||
interface ConfigurableSource : Source {
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
//import android.graphics.drawable.Drawable
|
||||
//import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
// import android.graphics.drawable.Drawable
|
||||
// import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
//import uy.kohesive.injekt.Injekt
|
||||
//import uy.kohesive.injekt.api.get
|
||||
// import uy.kohesive.injekt.Injekt
|
||||
// import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* A basic interface for creating a source. It could be an online source, a local source, etc...
|
||||
@@ -46,6 +46,6 @@ interface Source {
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>>
|
||||
}
|
||||
|
||||
//fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
//fun Source.getPreferenceKey(): String = "source_$id"
|
||||
// fun Source.getPreferenceKey(): String = "source_$id"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package eu.kanade.tachiyomi.source
|
||||
|
||||
//import android.content.Context
|
||||
//import eu.kanade.tachiyomi.R
|
||||
// import android.content.Context
|
||||
// import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
@@ -9,7 +9,7 @@ open class Page(
|
||||
val url: String = "",
|
||||
var imageUrl: String? = null,
|
||||
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
|
||||
): ProgressListener {
|
||||
) : ProgressListener {
|
||||
|
||||
val number: Int
|
||||
get() = index + 1
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SChapter : Serializable {
|
||||
|
||||
var chapter_number: Float
|
||||
|
||||
var scanlator: String?
|
||||
var scanlator: String?
|
||||
|
||||
fun copyFrom(other: SChapter) {
|
||||
name = other.name
|
||||
|
||||
@@ -16,7 +16,7 @@ import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
//import uy.kohesive.injekt.injectLazy
|
||||
// import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
/**
|
||||
* Network service.
|
||||
*/
|
||||
protected val network: NetworkHelper by injectLazy()
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
// /**
|
||||
// * Preferences that a source may need.
|
||||
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
|
||||
*
|
||||
* @param page the chapter whose page list has to be fetched
|
||||
*/
|
||||
protected open fun imageRequest(page: Page): Request {
|
||||
open fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
|
||||
object Config {
|
||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk",null, null)
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
}
|
||||
@@ -1,74 +1,100 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
import eu.kanade.tachiyomi.App
|
||||
/* 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 io.javalin.Javalin
|
||||
import ir.armor.tachidesk.util.*
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import xyz.nulldev.androidcompat.AndroidCompat
|
||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import ir.armor.tachidesk.util.addMangaToCategory
|
||||
import ir.armor.tachidesk.util.addMangaToLibrary
|
||||
import ir.armor.tachidesk.util.createCategory
|
||||
import ir.armor.tachidesk.util.getCategoryList
|
||||
import ir.armor.tachidesk.util.getCategoryMangaList
|
||||
import ir.armor.tachidesk.util.getChapter
|
||||
import ir.armor.tachidesk.util.getChapterList
|
||||
import ir.armor.tachidesk.util.getExtensionIcon
|
||||
import ir.armor.tachidesk.util.getExtensionList
|
||||
import ir.armor.tachidesk.util.getLibraryMangas
|
||||
import ir.armor.tachidesk.util.getManga
|
||||
import ir.armor.tachidesk.util.getMangaCategories
|
||||
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.openInBrowser
|
||||
import ir.armor.tachidesk.util.removeCategory
|
||||
import ir.armor.tachidesk.util.removeExtension
|
||||
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
||||
import ir.armor.tachidesk.util.reorderCategory
|
||||
import ir.armor.tachidesk.util.sourceFilters
|
||||
import ir.armor.tachidesk.util.sourceGlobalSearch
|
||||
import ir.armor.tachidesk.util.sourceSearch
|
||||
import ir.armor.tachidesk.util.updateCategory
|
||||
|
||||
class Main {
|
||||
companion object {
|
||||
val androidCompat by lazy { AndroidCompat() }
|
||||
|
||||
fun registerConfigModules() {
|
||||
GlobalConfigManager.registerModules(
|
||||
// ServerConfig.register(GlobalConfigManager.config),
|
||||
// SyncConfigModule.register(GlobalConfigManager.config)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
// make sure everything we need exists
|
||||
applicationSetup()
|
||||
|
||||
registerConfigModules()
|
||||
|
||||
//Load config API
|
||||
DI.global.addImport(ConfigKodeinModule().create())
|
||||
//Load Android compatibility dependencies
|
||||
AndroidCompatInitializer().init()
|
||||
// start app
|
||||
androidCompat.startApp(App())
|
||||
serverSetup()
|
||||
|
||||
var hasWebUiBundled: Boolean = false
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
this::class.java.classLoader.getResource("/react/index.html")
|
||||
hasWebUiBundled = true
|
||||
config.addStaticFiles("/react")
|
||||
config.addSinglePageRoot("/","/react/index.html")
|
||||
config.addSinglePageRoot("/", "/react/index.html")
|
||||
} catch (e: RuntimeException) {
|
||||
println("Warning: react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
}
|
||||
}.start(4567)
|
||||
|
||||
|
||||
|
||||
app.before() { ctx ->
|
||||
// allow the client which is running on another port
|
||||
ctx.header("Access-Control-Allow-Origin", "*")
|
||||
config.enableCorsForAllOrigins()
|
||||
}.start(serverConfig.ip, serverConfig.port)
|
||||
if (hasWebUiBundled) {
|
||||
openInBrowser()
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
ctx.json(getExtensionList())
|
||||
}
|
||||
|
||||
|
||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
println(apkName)
|
||||
println("installing $apkName")
|
||||
|
||||
ctx.status(
|
||||
installAPK(apkName)
|
||||
installAPK(apkName)
|
||||
)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
println("uninstalling $apkName")
|
||||
removeExtension(apkName)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
val result = getExtensionIcon(apkName)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/list") { ctx ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
@@ -85,6 +111,50 @@ class Main {
|
||||
ctx.json(getManga(mangaId))
|
||||
}
|
||||
|
||||
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val result = getThumbnail(mangaId)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// adds the manga to library
|
||||
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
addMangaToLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the library
|
||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
removeMangaFromLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
addMangaToCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// removes the manga from the category
|
||||
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeMangaFromCategory(mangaId, categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapterList(mangaId))
|
||||
@@ -93,7 +163,17 @@ class Main {
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getPages(chapterId, mangaId))
|
||||
ctx.json(getChapter(chapterId, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterId, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
@@ -103,10 +183,11 @@ class Main {
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm") { ctx ->
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceSearch(sourceId, searchTerm))
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||
}
|
||||
|
||||
// source filter list
|
||||
@@ -115,10 +196,53 @@ class Main {
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// lists mangas that have no category assigned
|
||||
app.get("/api/v1/library/") { ctx ->
|
||||
ctx.json(getLibraryMangas())
|
||||
}
|
||||
|
||||
// category list
|
||||
app.get("/api/v1/category/") { ctx ->
|
||||
ctx.json(getCategoryList())
|
||||
}
|
||||
|
||||
// category create
|
||||
app.post("/api/v1/category/") { ctx ->
|
||||
val name = ctx.formParam("name")!!
|
||||
createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||
updateCategory(categoryId, name, isLanding)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category re-ordering
|
||||
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
reorderCategory(categoryId, from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category delete
|
||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
removeCategory(categoryId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// returns the manga list associated with a category
|
||||
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
ctx.json(getCategoryMangaList(categoryId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
import java.io.File
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip = config.getString("ip")
|
||||
val port = config.getInt("port")
|
||||
|
||||
// proxy
|
||||
val socksProxy = config.getBoolean("socksProxy")
|
||||
val socksProxyHost = config.getString("socksProxyHost")
|
||||
val socksProxyPort = config.getString("socksProxyPort")
|
||||
|
||||
fun registerFile(file: String): File {
|
||||
return File(file).apply {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(config: Config) = ServerConfig(config.getConfig("server"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
import eu.kanade.tachiyomi.App
|
||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||
import ir.armor.tachidesk.util.systemTray
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
import xyz.nulldev.androidcompat.AndroidCompat
|
||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import java.io.File
|
||||
|
||||
object applicationDirs {
|
||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||
val mangaRoot = "$dataRoot/manga"
|
||||
}
|
||||
|
||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||
|
||||
val systemTray by lazy { systemTray() }
|
||||
|
||||
val androidCompat by lazy { AndroidCompat() }
|
||||
|
||||
fun serverSetup() {
|
||||
// register server config
|
||||
GlobalConfigManager.registerModule(
|
||||
ServerConfig.register(GlobalConfigManager.config)
|
||||
)
|
||||
|
||||
// make dirs we need
|
||||
listOf(
|
||||
applicationDirs.dataRoot,
|
||||
applicationDirs.extensionsRoot,
|
||||
"${applicationDirs.extensionsRoot}/icon",
|
||||
applicationDirs.thumbnailsRoot
|
||||
).forEach {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
|
||||
makeDataBaseTables()
|
||||
|
||||
// create system tray
|
||||
systemTray
|
||||
|
||||
// Load config API
|
||||
DI.global.addImport(ConfigKodeinModule().create())
|
||||
// Load Android compatibility dependencies
|
||||
AndroidCompatInitializer().init()
|
||||
// start app
|
||||
androidCompat.startApp(App())
|
||||
|
||||
// socks proxy settings
|
||||
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
|
||||
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
package ir.armor.tachidesk.database
|
||||
|
||||
import ir.armor.tachidesk.Config
|
||||
/* 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.applicationDirs
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
@@ -11,18 +18,24 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
object DBMangaer {
|
||||
val db by lazy {
|
||||
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
|
||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||
}
|
||||
}
|
||||
|
||||
fun makeDataBaseTables() {
|
||||
// mention db object to connect
|
||||
DBMangaer.db
|
||||
// must mention db object so the lazy block executes
|
||||
val db = DBMangaer.db
|
||||
db.useNestedTransactions = true
|
||||
|
||||
transaction {
|
||||
SchemaUtils.create(ExtensionsTable)
|
||||
SchemaUtils.create(SourceTable)
|
||||
SchemaUtils.create(MangaTable)
|
||||
SchemaUtils.create(ChapterTable)
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
ExtensionTable,
|
||||
SourceTable,
|
||||
MangaTable,
|
||||
ChapterTable,
|
||||
PageTable,
|
||||
CategoryTable,
|
||||
CategoryMangaTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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 CategoryDataClass(
|
||||
val id: Int,
|
||||
val order: Int,
|
||||
val name: String,
|
||||
val isLanding: Boolean
|
||||
)
|
||||
@@ -1,11 +1,16 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class ChapterDataClass(
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val date_upload: Long,
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
)
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val date_upload: Long,
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class ExtensionDataClass(
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val versionName: String,
|
||||
val versionCode: Int,
|
||||
val lang: String,
|
||||
val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val installed: Boolean,
|
||||
val classFQName: String,
|
||||
)
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val versionName: String,
|
||||
val versionCode: Int,
|
||||
val lang: String,
|
||||
val isNsfw: Boolean,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val installed: Boolean,
|
||||
val classFQName: String,
|
||||
)
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
|
||||
data class MangaDataClass(
|
||||
val id: Int,
|
||||
val sourceId: Long,
|
||||
val id: Int,
|
||||
val sourceId: Long,
|
||||
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnail_url: String? = null,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val thumbnailUrl: String? = null,
|
||||
|
||||
val initialized: Boolean = false,
|
||||
val initialized: Boolean = false,
|
||||
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: String? = null,
|
||||
val status: String = MangaStatus.UNKNOWN.name
|
||||
)
|
||||
val artist: String? = null,
|
||||
val author: String? = null,
|
||||
val description: String? = null,
|
||||
val genre: String? = null,
|
||||
val status: String = MangaStatus.UNKNOWN.name,
|
||||
val inLibrary: Boolean = false
|
||||
)
|
||||
|
||||
data class PagedMangaListDataClass(
|
||||
val mangaList: List<MangaDataClass>,
|
||||
val hasNextPage: Boolean
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class PageDataClass(
|
||||
val index: Int,
|
||||
var imageUrl: String,
|
||||
)
|
||||
val index: Int,
|
||||
var imageUrl: String,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class SourceDataClass(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val iconUrl: String,
|
||||
val supportsLatest: Boolean
|
||||
)
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val iconUrl: String,
|
||||
val supportsLatest: Boolean
|
||||
)
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
/* 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.ExtensionTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ExtensionEntity>(ExtensionsTable)
|
||||
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
|
||||
|
||||
var name by ExtensionsTable.name
|
||||
var pkgName by ExtensionsTable.pkgName
|
||||
var versionName by ExtensionsTable.versionName
|
||||
var versionCode by ExtensionsTable.versionCode
|
||||
var lang by ExtensionsTable.lang
|
||||
var isNsfw by ExtensionsTable.isNsfw
|
||||
var apkName by ExtensionsTable.apkName
|
||||
var iconUrl by ExtensionsTable.iconUrl
|
||||
var installed by ExtensionsTable.installed
|
||||
var classFQName by ExtensionsTable.classFQName
|
||||
}
|
||||
var name by ExtensionTable.name
|
||||
var pkgName by ExtensionTable.pkgName
|
||||
var versionName by ExtensionTable.versionName
|
||||
var versionCode by ExtensionTable.versionCode
|
||||
var lang by ExtensionTable.lang
|
||||
var isNsfw by ExtensionTable.isNsfw
|
||||
var apkName by ExtensionTable.apkName
|
||||
var iconUrl by ExtensionTable.iconUrl
|
||||
var installed by ExtensionTable.installed
|
||||
var classFQName by ExtensionTable.classFQName
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.dao.IntEntity
|
||||
import org.jetbrains.exposed.dao.IntEntityClass
|
||||
@@ -20,4 +24,4 @@ class MangaEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||
var thumbnail_url by MangaTable.thumbnail_url
|
||||
|
||||
var sourceReference by MangaEntity referencedOn MangaTable.sourceReference
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.dao.*
|
||||
import org.jetbrains.exposed.dao.EntityClass
|
||||
import org.jetbrains.exposed.dao.LongEntity
|
||||
import org.jetbrains.exposed.dao.id.EntityID
|
||||
|
||||
class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||
@@ -13,4 +18,4 @@ class SourceEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||
var extension by ExtensionEntity referencedOn SourceTable.extension
|
||||
var partOfFactorySource by SourceTable.partOfFactorySource
|
||||
var positionInFactorySource by SourceTable.positionInFactorySource
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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/. */
|
||||
|
||||
object CategoryMangaTable : IntIdTable() {
|
||||
val category = reference("category", CategoryTable)
|
||||
val manga = reference("manga", MangaTable)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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 ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
object CategoryTable : IntIdTable() {
|
||||
val name = varchar("name", 64)
|
||||
val isLanding = bool("is_landing").default(false)
|
||||
val order = integer("order").default(0)
|
||||
}
|
||||
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||
categoryEntry[CategoryTable.id].value,
|
||||
categoryEntry[CategoryTable.order],
|
||||
categoryEntry[CategoryTable.name],
|
||||
categoryEntry[CategoryTable.isLanding],
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ChapterTable : IntIdTable() {
|
||||
@@ -8,7 +11,7 @@ object ChapterTable : IntIdTable() {
|
||||
val name = varchar("name", 512)
|
||||
val date_upload = long("date_upload").default(0)
|
||||
val chapter_number = float("chapter_number").default(-1f)
|
||||
val scanlator = varchar("scanlator",128).nullable()
|
||||
val scanlator = varchar("scanlator", 128).nullable()
|
||||
|
||||
val manga = reference("manga", MangaTable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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 ExtensionsTable : IntIdTable() {
|
||||
object ExtensionTable : IntIdTable() {
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
val versionName = varchar("version_name", 16)
|
||||
@@ -15,4 +18,4 @@ object ExtensionsTable : IntIdTable() {
|
||||
|
||||
val installed = bool("installed").default(false)
|
||||
val classFQName = varchar("class_name", 256).default("") // fully qualified name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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 ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.util.proxyThumbnailUrl
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
object MangaTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
@@ -17,10 +24,32 @@ object MangaTable : IntIdTable() {
|
||||
val status = integer("status").default(SManga.UNKNOWN)
|
||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||
|
||||
val inLibrary = bool("in_library").default(false)
|
||||
val defaultCategory = bool("default_category").default(true)
|
||||
|
||||
// source is used by some ancestor of IntIdTable
|
||||
val sourceReference = reference("source", SourceTable)
|
||||
}
|
||||
|
||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
MangaDataClass(
|
||||
mangaEntry[MangaTable.id].value,
|
||||
mangaEntry[sourceReference].value,
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
proxyThumbnailUrl(mangaEntry[MangaTable.id].value),
|
||||
|
||||
mangaEntry[MangaTable.initialized],
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
)
|
||||
|
||||
enum class MangaStatus(val status: Int) {
|
||||
UNKNOWN(0),
|
||||
ONGOING(1),
|
||||
@@ -30,4 +59,4 @@ enum class MangaStatus(val status: Int) {
|
||||
companion object {
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object PageTable : IntIdTable() {
|
||||
val index = integer("index")
|
||||
val url = varchar("url", 2048)
|
||||
val imageUrl = varchar("imageUrl", 2048).nullable()
|
||||
|
||||
val chapter = reference("chapter", ChapterTable)
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.dao.id.IdTable
|
||||
|
||||
object SourceTable : IdTable<Long>() {
|
||||
override val id = long("id").entityId()
|
||||
val name= varchar("name", 128)
|
||||
val name = varchar("name", 128)
|
||||
val lang = varchar("lang", 10)
|
||||
val extension = reference("extension", ExtensionsTable)
|
||||
val extension = reference("extension", ExtensionTable)
|
||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
/* 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/. */
|
||||
|
||||
fun createCategory(name: String) {
|
||||
transaction {
|
||||
val count = CategoryTable.selectAll().count()
|
||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||
CategoryTable.insert {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.order] = count.toInt() + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||
transaction {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
if (name != null) it[CategoryTable.name] = name
|
||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||
transaction {
|
||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
categories.forEachIndexed { index, cat ->
|
||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||
it[CategoryTable.order] = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCategory(categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||
}
|
||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryList(): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
/* 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/. */
|
||||
|
||||
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||
CategoryMangaTable.insert {
|
||||
it[CategoryMangaTable.category] = categoryId
|
||||
it[CategoryMangaTable.manga] = mangaId
|
||||
}
|
||||
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,30 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.PageDataClass
|
||||
import ir.armor.tachidesk.database.entity.MangaEntity
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
|
||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
val mangaDetails = getManga(mangaId)
|
||||
val source = getHttpSource(mangaDetails.sourceId)
|
||||
|
||||
val chapterList = source.fetchChapterList(
|
||||
SManga.create().apply {
|
||||
title = mangaDetails.title
|
||||
url = mangaDetails.url
|
||||
}
|
||||
SManga.create().apply {
|
||||
title = mangaDetails.title
|
||||
url = mangaDetails.url
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
return transaction {
|
||||
@@ -41,47 +43,59 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return@transaction chapterList.map {
|
||||
ChapterDataClass(
|
||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||
it.url,
|
||||
it.name,
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId
|
||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||
it.url,
|
||||
it.name,
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPages(chapterId: Int, mangaId: Int): List<PageDataClass> {
|
||||
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
return transaction {
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
|
||||
val pagesList = source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
}
|
||||
val pageList = source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
url = chapterEntry[ChapterTable.url]
|
||||
name = chapterEntry[ChapterTable.name]
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
return@transaction pagesList.map {
|
||||
PageDataClass(
|
||||
it.index,
|
||||
getTrueImageUrl(it,source)
|
||||
)
|
||||
val chapter = ChapterDataClass(
|
||||
chapterEntry[ChapterTable.id].value,
|
||||
chapterEntry[ChapterTable.url],
|
||||
chapterEntry[ChapterTable.name],
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId,
|
||||
pageList.count()
|
||||
)
|
||||
|
||||
pageList.forEach { page ->
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||
if (pageEntry == null) {
|
||||
transaction {
|
||||
PageTable.insert {
|
||||
it[index] = page.index
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
it[this.chapter] = chapterId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@transaction chapter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
return if ( page.imageUrl == null){
|
||||
source.fetchImageUrl(page).toBlocking().first()!!
|
||||
} else page.imageUrl!!
|
||||
}
|
||||
|
||||
@@ -1,35 +1,42 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.APKExtractor
|
||||
import ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
import ir.armor.tachidesk.applicationDirs
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
|
||||
fun installAPK(apkName: String): Int {
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||
|
||||
// check if we don't have the dex file already downloaded
|
||||
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
if (!File(dexPath).exists()) {
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
if (!File(jarPath).exists()) {
|
||||
runBlocking {
|
||||
val api = ExtensionGithubApi()
|
||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
||||
@@ -41,7 +48,6 @@ fun installAPK(apkName: String): Int {
|
||||
// download apk file
|
||||
downloadAPKFile(apkToDownload, apkFilePath)
|
||||
|
||||
|
||||
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
||||
println(className)
|
||||
// dex -> jar
|
||||
@@ -57,10 +63,10 @@ fun installAPK(apkName: String): Int {
|
||||
val instance = classToLoad.newInstance()
|
||||
|
||||
val extensionId = transaction {
|
||||
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
|
||||
return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||
}
|
||||
|
||||
if (instance is HttpSource) {// single source
|
||||
if (instance is HttpSource) { // single source
|
||||
val httpSource = instance as HttpSource
|
||||
transaction {
|
||||
// SourceEntity.new {
|
||||
@@ -80,7 +86,6 @@ fun installAPK(apkName: String): Int {
|
||||
// println(httpSource.name)
|
||||
// println()
|
||||
}
|
||||
|
||||
} else { // multi source
|
||||
val sourceFactory = instance as SourceFactory
|
||||
transaction {
|
||||
@@ -105,12 +110,11 @@ fun installAPK(apkName: String): Int {
|
||||
|
||||
// update extension info
|
||||
transaction {
|
||||
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
|
||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||
it[installed] = true
|
||||
it[classFQName] = className
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return 201 // we downloaded successfully
|
||||
} else {
|
||||
@@ -122,10 +126,47 @@ val networkHelper: NetworkHelper by injectLazy()
|
||||
|
||||
private fun downloadAPKFile(url: String, apkPath: String) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
val response = networkHelper.client.newCall(request).execute()
|
||||
|
||||
val downloadedFile = File(apkPath)
|
||||
val sink = downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
}
|
||||
|
||||
fun removeExtension(pkgName: String) {
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
|
||||
val fileNameWithoutType = pkgName.substringBefore(".apk")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
transaction {
|
||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||
|
||||
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||
it[ExtensionTable.installed] = false
|
||||
}
|
||||
}
|
||||
|
||||
if (File(jarPath).exists()) {
|
||||
File(jarPath).delete()
|
||||
}
|
||||
}
|
||||
|
||||
val network: NetworkHelper by injectLazy()
|
||||
|
||||
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||
|
||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
val fileName = apkName
|
||||
|
||||
return getCachedResponse(saveDir, fileName) {
|
||||
network.client.newCall(
|
||||
GET(iconUrl)
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionIconUrl(apkName: String): String {
|
||||
return "/api/v1/extension/icon/$apkName"
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
@@ -18,7 +21,7 @@ private object Data {
|
||||
|
||||
private fun extensionDatabaseIsEmtpy(): Boolean {
|
||||
return transaction {
|
||||
return@transaction ExtensionsTable.selectAll().count() == 0L
|
||||
return@transaction ExtensionTable.selectAll().count() == 0L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +36,10 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
foundExtensions = api.findExtensions()
|
||||
transaction {
|
||||
foundExtensions.forEach { foundExtension ->
|
||||
val extensionRecord = ExtensionsTable.select { ExtensionsTable.name eq foundExtension.name }.firstOrNull()
|
||||
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
|
||||
if (extensionRecord != null) {
|
||||
// update the record
|
||||
ExtensionsTable.update({ ExtensionsTable.name eq foundExtension.name }) {
|
||||
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
|
||||
it[name] = foundExtension.name
|
||||
it[pkgName] = foundExtension.pkgName
|
||||
it[versionName] = foundExtension.versionName
|
||||
@@ -48,7 +51,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
}
|
||||
} else {
|
||||
// insert new record
|
||||
ExtensionsTable.insert {
|
||||
ExtensionTable.insert {
|
||||
it[name] = foundExtension.name
|
||||
it[pkgName] = foundExtension.pkgName
|
||||
it[versionName] = foundExtension.versionName
|
||||
@@ -62,23 +65,24 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("used cached extension list")
|
||||
}
|
||||
|
||||
return transaction {
|
||||
return@transaction ExtensionsTable.selectAll().map {
|
||||
return@transaction ExtensionTable.selectAll().map {
|
||||
ExtensionDataClass(
|
||||
it[ExtensionsTable.name],
|
||||
it[ExtensionsTable.pkgName],
|
||||
it[ExtensionsTable.versionName],
|
||||
it[ExtensionsTable.versionCode],
|
||||
it[ExtensionsTable.lang],
|
||||
it[ExtensionsTable.isNsfw],
|
||||
it[ExtensionsTable.apkName],
|
||||
it[ExtensionsTable.iconUrl],
|
||||
it[ExtensionsTable.installed],
|
||||
it[ExtensionsTable.classFQName]
|
||||
it[ExtensionTable.name],
|
||||
it[ExtensionTable.pkgName],
|
||||
it[ExtensionTable.versionName],
|
||||
it[ExtensionTable.versionCode],
|
||||
it[ExtensionTable.lang],
|
||||
it[ExtensionTable.isNsfw],
|
||||
it[ExtensionTable.apkName],
|
||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||
it[ExtensionTable.installed],
|
||||
it[ExtensionTable.classFQName]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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 okhttp3.Response
|
||||
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.
|
||||
*/
|
||||
private fun BufferedSource.saveTo(stream: OutputStream) {
|
||||
use { input ->
|
||||
stream.sink().buffer().use {
|
||||
it.writeAll(input)
|
||||
it.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||
val filePath = "$saveDir/$fileName"
|
||||
if (cachedFile != null) {
|
||||
val fileType = cachedFile.substringAfter(filePath)
|
||||
return Pair(
|
||||
pathToInputStream(cachedFile),
|
||||
"image/$fileType"
|
||||
)
|
||||
}
|
||||
|
||||
val response = fetcher()
|
||||
|
||||
if (response.code == 200) {
|
||||
val contentType = response.headers["content-type"]!!
|
||||
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||
|
||||
Files.newOutputStream(Paths.get(fullPath)).use { os ->
|
||||
response.body!!.source().saveTo(os)
|
||||
}
|
||||
return Pair(
|
||||
pathToInputStream(fullPath),
|
||||
contentType
|
||||
)
|
||||
} else {
|
||||
throw Exception("request error! ${response.code}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
/* 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/. */
|
||||
|
||||
fun addMangaToLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMangaFromLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = false
|
||||
it[defaultCategory] = true
|
||||
}
|
||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryMangas(): List<MangaDataClass> {
|
||||
return transaction {
|
||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,103 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import ir.armor.tachidesk.applicationDirs
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import java.io.InputStream
|
||||
|
||||
fun getManga(mangaId: Int): MangaDataClass {
|
||||
return transaction {
|
||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
|
||||
return@transaction if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
return if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
mangaEntry[MangaTable.thumbnail_url],
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||
|
||||
true,
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
)
|
||||
} else { // initialize manga
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
val fetchedManga = source.fetchMangaDetails(
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
// update database
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
// it[url] = fetchedManga.url
|
||||
// it[title] = fetchedManga.title
|
||||
it[initialized] = true
|
||||
|
||||
it[artist] = fetchedManga.artist
|
||||
it[author] = fetchedManga.author
|
||||
it[description] = fetchedManga.description
|
||||
it[genre] = fetchedManga.genre
|
||||
it[status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||
it[thumbnail_url] = fetchedManga.thumbnail_url
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
)
|
||||
} else { // initialize manga
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
val fetchedManga = source.fetchMangaDetails(
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
it[MangaTable.initialized] = true
|
||||
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
mangaEntry[MangaTable.thumbnail_url],
|
||||
|
||||
true,
|
||||
|
||||
mangaEntry[MangaTable.artist],
|
||||
mangaEntry[MangaTable.author],
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
)
|
||||
it[MangaTable.artist] = fetchedManga.artist
|
||||
it[MangaTable.author] = fetchedManga.author
|
||||
it[MangaTable.description] = fetchedManga.description
|
||||
it[MangaTable.genre] = fetchedManga.genre
|
||||
it[MangaTable.status] = fetchedManga.status
|
||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||
}
|
||||
}
|
||||
|
||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
||||
|
||||
true,
|
||||
|
||||
fetchedManga.artist,
|
||||
fetchedManga.author,
|
||||
fetchedManga.description,
|
||||
fetchedManga.genre,
|
||||
MangaStatus.valueOf(fetchedManga.status).name,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
return getCachedResponse(saveDir, fileName) {
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||
val source = getHttpSource(sourceId)
|
||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
||||
}
|
||||
|
||||
source.client.newCall(
|
||||
GET(thumbnailUrl, source.headers)
|
||||
).execute()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.URLEncoder
|
||||
|
||||
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
||||
|
||||
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
||||
|
||||
private fun clientBuilder(
|
||||
r18Toggle: Int,
|
||||
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
||||
): OkHttpClient = okHttpClient.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalCookies = chain.request().header("Cookie") ?: ""
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
||||
.build()
|
||||
chain.proceed(newReq)
|
||||
}.build()
|
||||
|
||||
private fun cookiesHeader(r18Toggle: Int): String {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
||||
return buildCookies(cookies)
|
||||
}
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) =
|
||||
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
// fun isLogged(): Boolean {
|
||||
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
||||
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||
// }
|
||||
|
||||
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
||||
val formBody = FormBody.Builder()
|
||||
.add("login_username", username)
|
||||
.add("login_password", password)
|
||||
.add("no_js", "1")
|
||||
.add("remember_me", "1")
|
||||
|
||||
twoFactorCode.let {
|
||||
formBody.add("two_factor", it)
|
||||
}
|
||||
|
||||
val response = clientBuilder().newCall(
|
||||
POST(
|
||||
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
mangaDexSource.headers,
|
||||
formBody.build()
|
||||
)
|
||||
).execute()
|
||||
return response.body!!.string().isEmpty()
|
||||
}
|
||||
//
|
||||
// fun logout(): Boolean {
|
||||
// return withContext(Dispatchers.IO) {
|
||||
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
||||
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||
// val token = cookie?.value
|
||||
// if (token.isNullOrEmpty()) {
|
||||
// return@withContext true
|
||||
// }
|
||||
// val result = clientBuilder().newCall(
|
||||
// POSTWithCookie(
|
||||
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
||||
// REMEMBER_ME,
|
||||
// token,
|
||||
// headers
|
||||
// )
|
||||
// ).execute()
|
||||
// val resultStr = result.body!!.string()
|
||||
// if (resultStr.contains("success", true)) {
|
||||
// network.cookieManager.remove(httpUrl)
|
||||
// return@withContext true
|
||||
// }
|
||||
//
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
|
||||
import ir.armor.tachidesk.database.table.MangaStatus
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.insertAndGetId
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> {
|
||||
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||
return "/api/v1/manga/$mangaId/thumbnail"
|
||||
}
|
||||
|
||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId.toLong())
|
||||
val mangasPage = if (popular) {
|
||||
source.fetchPopularManga(pageNum).toBlocking().first()
|
||||
@@ -19,11 +27,16 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
||||
else
|
||||
throw Exception("Source $source doesn't support latest")
|
||||
}
|
||||
return transaction {
|
||||
return mangasPage.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangasPage = this
|
||||
val mangaList = transaction {
|
||||
return@transaction mangasPage.mangas.map { manga ->
|
||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||
var mangaEntityId = if (mangaEntry == null) { // create manga entry
|
||||
MangaTable.insertAndGetId {
|
||||
if (mangaEntry == null) { // create manga entry
|
||||
val mangaId = MangaTable.insertAndGetId {
|
||||
it[url] = manga.url
|
||||
it[title] = manga.title
|
||||
|
||||
@@ -32,21 +45,18 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
||||
it[description] = manga.description
|
||||
it[genre] = manga.genre
|
||||
it[status] = manga.status
|
||||
it[thumbnail_url] = manga.genre
|
||||
it[thumbnail_url] = manga.thumbnail_url
|
||||
|
||||
it[sourceReference] = sourceId
|
||||
}.value
|
||||
} else {
|
||||
mangaEntry[MangaTable.id].value
|
||||
}
|
||||
|
||||
MangaDataClass(
|
||||
mangaEntityId,
|
||||
sourceId.toLong(),
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId,
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
manga.thumbnail_url,
|
||||
proxyThumbnailUrl(mangaId),
|
||||
|
||||
manga.initialized,
|
||||
|
||||
@@ -54,8 +64,32 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
|
||||
manga.author,
|
||||
manga.description,
|
||||
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,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return PagedMangaListDataClass(
|
||||
mangaList,
|
||||
mangasPage.hasNextPage
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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.applicationDirs
|
||||
import ir.armor.tachidesk.database.table.ChapterTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.PageTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.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
|
||||
|
||||
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 = getChapterDir(mangaId, chapterId)
|
||||
File(saveDir).mkdirs()
|
||||
val fileName = index.toString()
|
||||
|
||||
return getCachedResponse(saveDir, fileName) {
|
||||
source.fetchImage(tachiPage).toBlocking().first()
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||
val source = getHttpSource(sourceId)
|
||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||
|
||||
val chapterDir = when {
|
||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||
else -> chapterEntry[ChapterTable.name]
|
||||
}
|
||||
|
||||
val mangaTitle = mangaEntry[MangaTable.title]
|
||||
val sourceName = source.toString()
|
||||
|
||||
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||
// make sure dirs exist
|
||||
File(mangaDir).mkdirs()
|
||||
return mangaDir
|
||||
}
|
||||
@@ -1,28 +1,31 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
/* 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()
|
||||
// source.getFilterList().toItems()
|
||||
}
|
||||
|
||||
fun sourceSearch(sourceId: Long, searchTerm: String) {
|
||||
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
|
||||
val source = getHttpSource(sourceId)
|
||||
//source.fetchSearchManga()
|
||||
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
|
||||
return searchManga.processEntries(sourceId)
|
||||
}
|
||||
|
||||
fun sourceGlobalSearch(searchTerm: String) {
|
||||
|
||||
}
|
||||
|
||||
data class FilterWrapper(
|
||||
val type: String,
|
||||
val filter: Any
|
||||
val type: String,
|
||||
val filter: Any
|
||||
)
|
||||
|
||||
//private fun FilterList.toItems(): List<FilterWrapper> {
|
||||
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
|
||||
// return mapNotNull { filter ->
|
||||
// when (filter) {
|
||||
// is Filter.Header -> FilterWrapper("Header",filter)
|
||||
@@ -56,4 +59,4 @@ data class FilterWrapper(
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.applicationDirs
|
||||
import ir.armor.tachidesk.database.dataclass.SourceDataClass
|
||||
import ir.armor.tachidesk.database.entity.ExtensionEntity
|
||||
import ir.armor.tachidesk.database.entity.SourceEntity
|
||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
||||
@@ -32,23 +36,23 @@ fun getHttpSource(sourceId: Long): HttpSource {
|
||||
val apkName = extensionRecord.apkName
|
||||
val className = extensionRecord.classFQName
|
||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||
val jarPath = "${Config.extensionsRoot}/$jarName"
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||
|
||||
println(jarName)
|
||||
|
||||
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
|
||||
var usedCached = false
|
||||
val instance =
|
||||
if (cachedExtensionPair != null) {
|
||||
usedCached = true
|
||||
println("Used cached Extension")
|
||||
cachedExtensionPair.second
|
||||
} else {
|
||||
println("No Extension cache")
|
||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
||||
val classToLoad = Class.forName(className, true, child)
|
||||
classToLoad.newInstance()
|
||||
}
|
||||
if (cachedExtensionPair != null) {
|
||||
usedCached = true
|
||||
println("Used cached Extension")
|
||||
cachedExtensionPair.second
|
||||
} else {
|
||||
println("No Extension cache")
|
||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
||||
val classToLoad = Class.forName(className, true, child)
|
||||
classToLoad.newInstance()
|
||||
}
|
||||
if (sourceRecord.partOfFactorySource) {
|
||||
return@transaction if (usedCached) {
|
||||
(instance as List<HttpSource>)[sourceRecord.positionInFactorySource!!]
|
||||
@@ -71,12 +75,26 @@ fun getSourceList(): List<SourceDataClass> {
|
||||
return transaction {
|
||||
return@transaction SourceTable.selectAll().map {
|
||||
SourceDataClass(
|
||||
it[SourceTable.id].value.toString(),
|
||||
it[SourceTable.name],
|
||||
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
|
||||
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
|
||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||
it[SourceTable.id].value.toString(),
|
||||
it[SourceTable.name],
|
||||
it[SourceTable.lang],
|
||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||
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])),
|
||||
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl],
|
||||
getHttpSource(source[SourceTable.id].value).supportsLatest
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,67 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.Config
|
||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||
import java.io.File
|
||||
/* 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/. */
|
||||
|
||||
fun applicationSetup() {
|
||||
// make dirs we need
|
||||
File(Config.dataRoot).mkdirs()
|
||||
File(Config.extensionsRoot).mkdirs()
|
||||
import dorkbox.systemTray.MenuItem
|
||||
import dorkbox.systemTray.SystemTray
|
||||
import dorkbox.systemTray.SystemTray.TrayType
|
||||
import dorkbox.util.CacheUtil
|
||||
import dorkbox.util.Desktop
|
||||
import ir.armor.tachidesk.Main
|
||||
import java.awt.event.ActionListener
|
||||
import java.io.IOException
|
||||
|
||||
fun openInBrowser() {
|
||||
try {
|
||||
Desktop.browseURL("http://127.0.0.1:4567")
|
||||
} catch (e1: IOException) {
|
||||
e1.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
makeDataBaseTables()
|
||||
}
|
||||
fun systemTray(): SystemTray? {
|
||||
try {
|
||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
|
||||
if (System.getProperty("os.name").startsWith("Windows"))
|
||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||
|
||||
CacheUtil.clear()
|
||||
|
||||
val systemTray = SystemTray.get() ?: return null
|
||||
val mainMenu = systemTray.menu
|
||||
|
||||
mainMenu.add(
|
||||
MenuItem(
|
||||
"Open Tachidesk",
|
||||
ActionListener {
|
||||
try {
|
||||
Desktop.browseURL("http://127.0.0.1:4567")
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
||||
|
||||
// systemTray.setTooltip("Tachidesk")
|
||||
systemTray.setImage(icon)
|
||||
// systemTray.status = "No Mail"
|
||||
|
||||
systemTray.getMenu().add(
|
||||
MenuItem("Quit") {
|
||||
systemTray.shutdown()
|
||||
System.exit(0)
|
||||
}
|
||||
)
|
||||
|
||||
return systemTray
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 579 KiB |
@@ -0,0 +1,8 @@
|
||||
# Server ip and port bindings
|
||||
server.ip = 0.0.0.0
|
||||
server.port = 4567
|
||||
|
||||
# Socks5 proxy
|
||||
server.socksProxy = false
|
||||
server.socksProxyHost = ""
|
||||
server.socksProxyPort = ""
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"systemParams": "linux-x64-88",
|
||||
"modulesFolders": [],
|
||||
"flags": [],
|
||||
"linkedModules": [],
|
||||
"topLevelPatterns": [],
|
||||
"lockfileEntries": {},
|
||||
"files": [],
|
||||
"artifacts": {}
|
||||
}
|
||||
@@ -13,5 +13,7 @@ module.exports = {
|
||||
|
||||
// Indent props with 4 spaces
|
||||
'react/jsx-indent-props': ['error', 4],
|
||||
|
||||
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
.eslintcache
|
||||
.vscode
|
||||
.env
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.1",
|
||||
"fontsource-roboto": "^4.0.0",
|
||||
"react": "^17.0.1",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 579 KiB |
@@ -7,9 +7,9 @@
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="A manga reader that runs tachiyomi's extensions"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Tachidesk</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "Tachidesk",
|
||||
"name": "Tachidesk",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
@@ -8,18 +8,24 @@
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
"theme_color": "#ff2323",
|
||||
"background_color": "#ff2323"
|
||||
}
|
||||
|
||||
@@ -1,47 +1,113 @@
|
||||
import React from 'react';
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router, Route, Switch,
|
||||
BrowserRouter as Router, Redirect, Route, Switch,
|
||||
} from 'react-router-dom';
|
||||
import { Container } from '@material-ui/core';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
|
||||
import NavBar from './components/NavBar';
|
||||
import Home from './screens/Home';
|
||||
import Sources from './screens/Sources';
|
||||
import Extensions from './screens/Extensions';
|
||||
import MangaList from './screens/MangaList';
|
||||
import SourceMangas from './screens/SourceMangas';
|
||||
import Manga from './screens/Manga';
|
||||
import Reader from './screens/Reader';
|
||||
import Search from './screens/Search';
|
||||
import Search from './screens/SearchSingle';
|
||||
import NavbarContext from './context/NavbarContext';
|
||||
import DarkTheme from './context/DarkTheme';
|
||||
import Library from './screens/Library';
|
||||
import Settings from './screens/Settings';
|
||||
import Categories from './screens/settings/Categories';
|
||||
import useLocalStorage from './util/useLocalStorage';
|
||||
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
const [action, setAction] = useState<any>(<div />);
|
||||
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||
const navBarContext = {
|
||||
title, setTitle, action, setAction,
|
||||
};
|
||||
const darkThemeContext = { darkTheme, setDarkTheme };
|
||||
|
||||
const theme = React.useMemo(
|
||||
() => createMuiTheme({
|
||||
palette: {
|
||||
type: darkTheme ? 'dark' : 'light',
|
||||
},
|
||||
overrides: {
|
||||
MuiCssBaseline: {
|
||||
'@global': {
|
||||
'*::-webkit-scrollbar': {
|
||||
width: '10px',
|
||||
background: darkTheme ? '#222' : '#e1e1e1',
|
||||
|
||||
},
|
||||
'*::-webkit-scrollbar-thumb': {
|
||||
background: darkTheme ? '#111' : '#aaa',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[darkTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<NavBar />
|
||||
|
||||
<Switch>
|
||||
<Route path="/search">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route path="/extensions">
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<MangaList popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<MangaList popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
<ThemeProvider theme={theme}>
|
||||
<NavbarContext.Provider value={navBarContext}>
|
||||
<CssBaseline />
|
||||
<NavBar />
|
||||
<Container maxWidth={false} disableGutters>
|
||||
<Switch>
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<Search />
|
||||
</Route>
|
||||
<Route path="/extensions">
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<SourceMangas popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<SourceMangas popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
</Route>
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
<Route path="/settings/categories">
|
||||
<Categories />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<Settings />
|
||||
</DarkTheme.Provider>
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => (
|
||||
<Redirect to="/library" />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Container>
|
||||
</NavbarContext.Provider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* 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 } from 'react';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import client from '../util/client';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
paper: {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
open: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
mangaId: number
|
||||
}
|
||||
|
||||
interface ICategoryInfo {
|
||||
category: ICategory
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export default function CategorySelect(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { open, setOpen, mangaId } = props;
|
||||
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||
|
||||
useEffect(() => {
|
||||
let tmpCategoryInfos: ICategoryInfo[] = [];
|
||||
client.get('/api/v1/category/')
|
||||
.then((response) => response.data)
|
||||
.then((data: ICategory[]) => {
|
||||
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
||||
})
|
||||
.then(() => {
|
||||
client.get(`/api/v1/manga/${mangaId}/category/`)
|
||||
.then((response) => response.data)
|
||||
.then((data: ICategory[]) => {
|
||||
data.forEach((category) => {
|
||||
tmpCategoryInfos[category.order - 1].selected = true;
|
||||
});
|
||||
setCategoryInfos(tmpCategoryInfos);
|
||||
});
|
||||
});
|
||||
}, [updateTriggerHolder, open]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
const method = checked ? client.get : client.delete;
|
||||
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
||||
.then(() => triggerUpdate());
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={classes}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Set categories</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{categoryInfos.map((categoryInfo) => (
|
||||
<FormControlLabel
|
||||
control={(
|
||||
<Checkbox
|
||||
checked={categoryInfo.selected}
|
||||
onChange={(e) => handleChange(e, categoryInfo.category.id)}
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
label={categoryInfo.category.name}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
@@ -5,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import client from '../util/client';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -34,24 +40,47 @@ const useStyles = makeStyles((theme) => ({
|
||||
|
||||
interface IProps {
|
||||
extension: IExtension
|
||||
notifyInstall: () => void
|
||||
}
|
||||
|
||||
export default function ExtensionCard(props: IProps) {
|
||||
const {
|
||||
extension: {
|
||||
name, lang, versionName, iconUrl, installed, apkName,
|
||||
name, lang, versionName, installed, apkName, iconUrl,
|
||||
},
|
||||
notifyInstall,
|
||||
} = props;
|
||||
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
|
||||
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles();
|
||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||
|
||||
function install() {
|
||||
setInstalledState('installing');
|
||||
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
|
||||
setInstalledState('installed');
|
||||
});
|
||||
client.get(`/api/v1/extension/install/${apkName}`)
|
||||
.then(() => {
|
||||
setInstalledState('uninstall');
|
||||
notifyInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function uninstall() {
|
||||
setInstalledState('uninstalling');
|
||||
client.get(`/api/v1/extension/uninstall/${apkName}`)
|
||||
.then(() => {
|
||||
// setInstalledState('install');
|
||||
notifyInstall();
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
if (installedState === 'install') {
|
||||
install();
|
||||
} else {
|
||||
uninstall();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,7 +91,7 @@ export default function ExtensionCard(props: IProps) {
|
||||
variant="rounded"
|
||||
className={classes.icon}
|
||||
alt={name}
|
||||
src={iconUrl}
|
||||
src={serverAddress + iconUrl}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
@@ -76,7 +105,7 @@ export default function ExtensionCard(props: IProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
|
||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import { langCodeToName } from '../util/language';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
paper: {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
shownLangs: string[]
|
||||
setShownLangs: (arg0: string[]) => void
|
||||
allLangs: string[]
|
||||
}
|
||||
|
||||
export default function ExtensionLangSelect(props: IProps) {
|
||||
const { shownLangs, setShownLangs, allLangs } = props;
|
||||
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||
const [mShownLangs, setMShownLangs] = useState(shownLangs);
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
setShownLangs(mShownLangs);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
if (checked) {
|
||||
setMShownLangs([...mShownLangs, lang]);
|
||||
} else {
|
||||
const clone = JSON.parse(JSON.stringify(mShownLangs));
|
||||
clone.splice(clone.indexOf(lang), 1);
|
||||
setMShownLangs(clone);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<Dialog
|
||||
classes={classes}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Enabled Languages</DialogTitle>
|
||||
<DialogContent dividers style={{ padding: 0 }}>
|
||||
<List>
|
||||
{allLangs.map((lang) => (
|
||||
<ListItem key={lang}>
|
||||
<ListItemText primary={langCodeToName(lang)} />
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
checked={mShownLangs.indexOf(lang) !== -1}
|
||||
onChange={(e) => handleChange(e, lang)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
@@ -5,6 +9,8 @@ import CardActionArea from '@material-ui/core/CardActionArea';
|
||||
import CardMedia from '@material-ui/core/CardMedia';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
@@ -39,31 +45,36 @@ const useStyles = makeStyles({
|
||||
interface IProps {
|
||||
manga: IManga
|
||||
}
|
||||
export default function MangaCard(props: IProps) {
|
||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
manga: {
|
||||
id, title, thumbnailUrl,
|
||||
},
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
return (
|
||||
<Link to={`/manga/${id}/`}>
|
||||
<Card className={classes.root}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
component="img"
|
||||
alt={title}
|
||||
image={thumbnailUrl}
|
||||
title={title}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||
<Link to={`/manga/${id}/`}>
|
||||
<Card className={classes.root} ref={ref}>
|
||||
<CardActionArea>
|
||||
<div className={classes.wrapper}>
|
||||
<CardMedia
|
||||
className={classes.image}
|
||||
component="img"
|
||||
alt={title}
|
||||
image={serverAddress + thumbnailUrl}
|
||||
title={title}
|
||||
/>
|
||||
<div className={classes.gradient} />
|
||||
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
|
||||
</div>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default MangaCard;
|
||||
|
||||
@@ -1,17 +1,72 @@
|
||||
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 { Button, createStyles, makeStyles } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import client from '../util/client';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
'& button': {
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
manga: IManga | undefined
|
||||
manga: IManga
|
||||
}
|
||||
|
||||
export default function MangaDetails(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { manga } = props;
|
||||
const [inLibrary, setInLibrary] = useState<string>(
|
||||
manga.inLibrary ? 'In Library' : 'Not In Library',
|
||||
);
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||
|
||||
function addToLibrary() {
|
||||
setInLibrary('adding');
|
||||
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('In Library');
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromLibrary() {
|
||||
setInLibrary('removing');
|
||||
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('Not In Library');
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
if (inLibrary === 'Not In Library') {
|
||||
addToLibrary();
|
||||
} else {
|
||||
removeFromLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1>
|
||||
{manga && manga.title}
|
||||
</h1>
|
||||
</>
|
||||
<div className={classes.root}>
|
||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
|
||||
{inLibrary === 'In Library'
|
||||
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
|
||||
|
||||
</div>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,59 @@
|
||||
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, { 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 } = props;
|
||||
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 !== undefined ? message : 'loading...'}</h3>;
|
||||
mapped = <h3>{message}</h3>;
|
||||
} else {
|
||||
mapped = (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
|
||||
{mangas.map((it) => (
|
||||
<MangaCard manga={it} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
mapped = mangas.map((it, idx) => {
|
||||
if (idx === mangas.length - 1) {
|
||||
return <MangaCard manga={it} ref={lastManga} />;
|
||||
}
|
||||
return <MangaCard manga={it} />;
|
||||
});
|
||||
}
|
||||
|
||||
return mapped;
|
||||
return (
|
||||
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
|
||||
{mapped}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
MangaGrid.defaultProps = {
|
||||
message: 'loading...',
|
||||
};
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// TODO: remove above!
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import MoreIcon from '@material-ui/icons/MoreVert';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
|
||||
import TemporaryDrawer from './TemporaryDrawer';
|
||||
import NavBarContext from '../context/NavbarContext';
|
||||
import DarkTheme from '../context/DarkTheme';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -19,13 +31,35 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// const theme = createMuiTheme({
|
||||
// overrides: {
|
||||
// MuiAppBar: {
|
||||
// colorPrimary: { backgroundColor: '#FFC0CB' },
|
||||
// },
|
||||
// },
|
||||
// palette: { type: 'dark' },
|
||||
// });
|
||||
|
||||
export default function NavBar() {
|
||||
const classes = useStyles();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const { title, action } = useContext(NavBarContext);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const { darkTheme } = useContext(DarkTheme);
|
||||
|
||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="static">
|
||||
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
@@ -38,8 +72,45 @@ export default function NavBar() {
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
Tachidesk
|
||||
{title}
|
||||
</Typography>
|
||||
{action}
|
||||
{/* <IconButton
|
||||
onClick={handleMenu}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton> */}
|
||||
{/* <Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => { setDarkTheme(true); handleClose(); }}
|
||||
>
|
||||
Dark Theme
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => { setDarkTheme(false); handleClose(); }}
|
||||
>
|
||||
Light Theme
|
||||
|
||||
</MenuItem>
|
||||
</Menu> */}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Card from '@material-ui/core/Card';
|
||||
@@ -5,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
import { langCodeToName } from '../util/language';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -43,6 +49,8 @@ export default function SourceCard(props: IProps) {
|
||||
},
|
||||
} = props;
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
@@ -53,20 +61,21 @@ export default function SourceCard(props: IProps) {
|
||||
variant="rounded"
|
||||
className={classes.icon}
|
||||
alt={name}
|
||||
src={iconUrl}
|
||||
src={serverAddress + iconUrl}
|
||||
/>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
{lang}
|
||||
{langCodeToName(lang)}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/latest/`; }}>Latest</Button>}
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `sources/${id}/popular/`; }}>Browse</Button>
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/search/`; }}>Search</Button>
|
||||
{supportsLatest && <Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/latest/`; }}>Latest</Button>}
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/sources/${id}/popular/`; }}>Browse</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
@@ -32,6 +36,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
onKeyDown={() => setDrawerOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Library">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Library" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Extensions">
|
||||
<ListItemIcon>
|
||||
@@ -48,14 +60,22 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="settings">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Search">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Global Search" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
</Link> */}
|
||||
</List>
|
||||
</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,21 @@
|
||||
/* 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>>
|
||||
action: any
|
||||
setAction: React.Dispatch<React.SetStateAction<any>>
|
||||
};
|
||||
|
||||
const NavBarContext = React.createContext<ContextType>({
|
||||
title: 'Tachidesk',
|
||||
setTitle: ():void => {},
|
||||
action: <div />,
|
||||
setAction: ():void => {},
|
||||
});
|
||||
|
||||
export default NavBarContext;
|
||||
@@ -1,3 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import './index.css';
|
||||
// roboto font
|
||||
import 'fontsource-roboto';
|
||||
@@ -12,8 +15,3 @@ ReactDOM.render(
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry) {
|
||||
import('web-vitals').then(({
|
||||
getCLS, getFID, getFCP, getLCP, getTTFB,
|
||||
}) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,21 +1,100 @@
|
||||
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 NavbarContext from '../context/NavbarContext';
|
||||
import client from '../util/client';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
import ExtensionLangSelect from '../components/ExtensionLangSelect';
|
||||
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language';
|
||||
|
||||
const allLangs: string[] = [];
|
||||
|
||||
function groupExtensions(extensions: IExtension[]) {
|
||||
allLangs.length = 0; // empty the array
|
||||
const result = { installed: [] } as any;
|
||||
extensions.sort((a, b) => ((a.apkName > b.apkName) ? 1 : -1));
|
||||
|
||||
extensions.forEach((extension) => {
|
||||
if (result[extension.lang] === undefined) {
|
||||
result[extension.lang] = [];
|
||||
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
|
||||
}
|
||||
if (extension.installed) {
|
||||
result.installed.push(extension);
|
||||
} else {
|
||||
result[extension.lang].push(extension);
|
||||
}
|
||||
});
|
||||
|
||||
// put english first for convience
|
||||
allLangs.sort(langSortCmp);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function Extensions() {
|
||||
let mapped;
|
||||
const [extensions, setExtensions] = useState<IExtension[]>([]);
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:4567/api/v1/extension/list')
|
||||
.then((response) => response.json())
|
||||
.then((data) => setExtensions(data));
|
||||
}, []);
|
||||
setTitle('Extensions');
|
||||
setAction(
|
||||
<ExtensionLangSelect
|
||||
shownLangs={shownLangs}
|
||||
setShownLangs={setShownLangs}
|
||||
allLangs={allLangs}
|
||||
/>,
|
||||
);
|
||||
}, [shownLangs]);
|
||||
|
||||
if (extensions.length === 0) {
|
||||
mapped = <h3>wait</h3>;
|
||||
} else {
|
||||
mapped = extensions.map((it) => <ExtensionCard extension={it} />);
|
||||
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
|
||||
const [extensions, setExtensions] = useState<any>({});
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||
|
||||
useEffect(() => {
|
||||
client.get('/api/v1/extension/list')
|
||||
.then((response) => response.data)
|
||||
.then((data) => setExtensionsRaw(data));
|
||||
}, [updateTriggerHolder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (extensionsRaw.length > 0) {
|
||||
const groupedExtension = groupExtensions(extensionsRaw);
|
||||
setExtensions(groupedExtension);
|
||||
}
|
||||
}, [extensionsRaw]);
|
||||
|
||||
if (Object.entries(extensions).length === 0) {
|
||||
return <h3>loading...</h3>;
|
||||
}
|
||||
|
||||
return <h2>{mapped}</h2>;
|
||||
return (
|
||||
<>
|
||||
{
|
||||
Object.entries(extensions).map(([lang, list]) => (
|
||||
(['installed', ...shownLangs].indexOf(lang) !== -1
|
||||
&& (
|
||||
<React.Fragment key={lang}>
|
||||
<h1 key={lang} style={{ marginLeft: 25 }}>
|
||||
{langCodeToName(lang)}
|
||||
</h1>
|
||||
{(list as IExtension[]).map((it) => (
|
||||
<ExtensionCard
|
||||
key={it.apkName}
|
||||
extension={it}
|
||||
notifyInstall={() => {
|
||||
triggerUpdate();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<h1>
|
||||
Home
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* 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 { Tab, Tabs } from '@material-ui/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
import NavbarContext from '../context/NavbarContext';
|
||||
import client from '../util/client';
|
||||
|
||||
interface IMangaCategory {
|
||||
category: ICategory
|
||||
mangas: IManga[]
|
||||
isFetched: boolean
|
||||
}
|
||||
|
||||
interface TabPanelProps {
|
||||
children: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const {
|
||||
children, value, index,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Library() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Library'); setAction(<></>); }, []);
|
||||
|
||||
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
|
||||
const [tabNum, setTabNum] = useState<number>(0);
|
||||
|
||||
// a hack so MangaGrid doesn't stop working. I won't change it in case
|
||||
// if I do manga pagination for library..
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
|
||||
const handleTabChange = (newTab: number) => {
|
||||
setTabNum(newTab);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all<IManga[], ICategory[]>([
|
||||
client.get('/api/v1/library').then((response) => response.data),
|
||||
client.get('/api/v1/category').then((response) => response.data),
|
||||
])
|
||||
.then(
|
||||
([libraryMangas, categories]) => {
|
||||
const categoryTabs = categories.map((category) => ({
|
||||
category,
|
||||
mangas: [] as IManga[],
|
||||
isFetched: false,
|
||||
}));
|
||||
|
||||
if (libraryMangas.length > 0 || categoryTabs.length === 0) {
|
||||
const defaultCategoryTab = {
|
||||
category: {
|
||||
name: 'Default',
|
||||
isLanding: true,
|
||||
order: 0,
|
||||
id: -1,
|
||||
},
|
||||
mangas: libraryMangas,
|
||||
isFetched: true,
|
||||
};
|
||||
setTabs(
|
||||
[defaultCategoryTab, ...categoryTabs],
|
||||
);
|
||||
} else {
|
||||
setTabs(categoryTabs);
|
||||
setTabNum(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
// console.log(client.defaults.baseURL);
|
||||
// fetch the current tab
|
||||
useEffect(() => {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.category.order === tabNum && !tab.isFetched) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
client.get(`/api/v1/category/${tab.category.id}`)
|
||||
.then((response) => response.data)
|
||||
.then((data: IManga[]) => {
|
||||
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
||||
tabsClone[index].mangas = data;
|
||||
tabsClone[index].isFetched = true;
|
||||
|
||||
setTabs(tabsClone); // clone the object
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [tabNum]);
|
||||
|
||||
let toRender;
|
||||
if (tabs.length > 1) {
|
||||
// eslint-disable-next-line max-len
|
||||
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));
|
||||
|
||||
const tabBodies = tabs.map((tab) => (
|
||||
<TabPanel value={tabNum} index={tab.category.order}>
|
||||
<MangaGrid
|
||||
mangas={tab.mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={tab.isFetched ? 'Category is Empty' : 'Loading...'}
|
||||
/>
|
||||
</TabPanel>
|
||||
));
|
||||
|
||||
// Visual Hack: 160px is min-width for viewport width of >600
|
||||
const scrollableTabs = window.innerWidth < tabs.length * 160;
|
||||
toRender = (
|
||||
<>
|
||||
<Tabs
|
||||
value={tabNum}
|
||||
onChange={(e, newTab) => handleTabChange(newTab)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
centered={!scrollableTabs}
|
||||
variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
|
||||
scrollButtons="on"
|
||||
>
|
||||
{tabDefines}
|
||||
</Tabs>
|
||||
{tabBodies}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
|
||||
toRender = (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
message={tabs.length > 0 ? 'Library is Empty' : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return toRender;
|
||||
}
|
||||