Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a1e1e01dc | |||
| a567701639 | |||
| 1802271358 | |||
| 9e649eef79 | |||
| 1eb4a9c216 | |||
| e3f65d2192 | |||
| bb09cfddb3 | |||
| d383939c9f | |||
| 32dd543562 | |||
| 5a75f26791 | |||
| 95c437efd5 | |||
| ec877f632f | |||
| 8666cbf8bc | |||
| 84b0c26450 | |||
| 64e5bbabb3 | |||
| cc1a15e5ba | |||
| d29e942a72 | |||
| 8d86c88c38 | |||
| c7dc7421aa | |||
| 34ed3e5c68 | |||
| 1a4a8af384 | |||
| 62b1e99bbf | |||
| 1aa3b76934 | |||
| 3e53c50f64 | |||
| 430386bc84 | |||
| 30049e8152 | |||
| 34d9a7a233 | |||
| 183972475b | |||
| fd46727f8e | |||
| f6ce010aa2 | |||
| d0ff30df9f | |||
| 8e449abd67 | |||
| 2986130268 | |||
| 1c0c09f2f2 | |||
| 44100cb5b6 | |||
| cfc6e5cd2a | |||
| c067d14c2c | |||
| aded854a2b | |||
| e79d0b9dd2 | |||
| a0115d88b0 | |||
| 85ec2ed367 | |||
| bf908c4d17 | |||
| f41c5c9428 | |||
| 04837983fa | |||
| 5d484b012c | |||
| 436a8d0585 | |||
| 28cc0a6f84 | |||
| 26cc2f2c96 | |||
| 149107e749 | |||
| a74936c5f5 | |||
| ff8c8913d4 | |||
| 83426e1302 | |||
| 9cd93d467c | |||
| 257f8a5a27 | |||
| 79bab08cae | |||
| 4e699e4f5a | |||
| 1128f40bac | |||
| 53ef836326 | |||
| b8df0e89e5 | |||
| 472bfec6bf | |||
| c1b86cedd2 | |||
| 428c65f075 |
+26
-5
@@ -1,6 +1,27 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
|
||||
# Windows forced line-endings
|
||||
/.idea/* text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Gradle wrapper
|
||||
*.jar binary
|
||||
|
||||
# Images
|
||||
*.webp binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.gz binary
|
||||
*.zip binary
|
||||
*.7z binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
*.woff binary
|
||||
*.pyc binary
|
||||
*.swp binary
|
||||
*.pdf binary
|
||||
@@ -25,6 +25,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
||||
## Device information
|
||||
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
||||
- Server Operating System: (Example: Ubuntu 20.04)
|
||||
- Server Desktop Environment: N/A or (Example: Gnome 40)
|
||||
- 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)
|
||||
@@ -34,10 +35,10 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
||||
2. Second Step
|
||||
|
||||
### Expected behavior
|
||||
Describe what should have happened
|
||||
Describe what should have happened. Remove this line after you are done.
|
||||
|
||||
### Actual behavior
|
||||
Describe what happens instead
|
||||
Describe what happens instead. Remove this line after you are done.
|
||||
|
||||
## Other details
|
||||
Describe additional details If necessary
|
||||
Describe additional details If necessary. Remove this line after you are done.
|
||||
@@ -23,7 +23,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
|
||||
---
|
||||
|
||||
## What feature should be added to Tachidesk?
|
||||
Explain What the feature is and how it should work in detail
|
||||
Explain What the feature is and how it should work in detail. Remove this line after you are done.
|
||||
|
||||
## Why/Project's Benefit/Existing Problem
|
||||
Explain why this should be added
|
||||
Explain why this should be added. Remove this line after you are done.
|
||||
@@ -26,7 +26,12 @@ jobs:
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||
"message": "The requested information was not filled out"
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*Remove this line after you are done.*",
|
||||
"message": "The lines requesting to be removed were not removed."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package xyz.nulldev.ts.config
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
|
||||
@@ -18,9 +18,7 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
// Android stub library
|
||||
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
|
||||
implementation(fileTree("lib/"))
|
||||
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
|
||||
|
||||
|
||||
// Android JAR libs
|
||||
@@ -41,10 +39,10 @@ dependencies {
|
||||
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
|
||||
|
||||
// Config API
|
||||
implementation( project(":AndroidCompat:Config"))
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
// dex2jar
|
||||
// compileOnly( "dex2jar:dex-translator")
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
// APK parser
|
||||
compileOnly("net.dongliu:apk-parser:2.6.10")
|
||||
@@ -55,7 +53,11 @@ dependencies {
|
||||
// AndroidX annotations
|
||||
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
|
||||
|
||||
// compileOnly("io.reactivex:rxjava:1.3.8")
|
||||
// substitute for duktape-android
|
||||
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
|
||||
implementation("org.mozilla:rhino-runtime:1.7.13")
|
||||
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
|
||||
implementation("org.mozilla:rhino-engine:1.7.13")
|
||||
}
|
||||
|
||||
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# 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/.
|
||||
|
||||
# This is a windows only PowerShell script to create android.jar stubs
|
||||
|
||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||
if ($(Split-Path -Path (Get-Location) -Leaf) -eq "AndroidCompat" ) {
|
||||
Set-Location ..
|
||||
}
|
||||
|
||||
Write-Output "Getting required Android.jar..."
|
||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||
|
||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
|
||||
|
||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||
|
||||
[IO.File]::WriteAllBytes($android_jar, [Convert]::FromBase64String($androidEncoded))
|
||||
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
Write-Output "Patching JAR..."
|
||||
|
||||
function Remove-Files-Zip($zipfile, $path)
|
||||
{
|
||||
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
|
||||
|
||||
$stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
|
||||
$mode = [IO.Compression.ZipArchiveMode]::Update
|
||||
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
|
||||
|
||||
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
|
||||
|
||||
$zip.Dispose()
|
||||
$stream.Close()
|
||||
$stream.Dispose()
|
||||
}
|
||||
|
||||
Write-Output "Removing org.json..."
|
||||
Remove-Files-Zip $android_jar 'org/json/*'
|
||||
|
||||
Write-Output "Removing org.apache..."
|
||||
Remove-Files-Zip $android_jar 'org/apache/*'
|
||||
|
||||
Write-Output "Removing org.w3c..."
|
||||
Remove-Files-Zip $android_jar 'org/w3c/*'
|
||||
|
||||
Write-Output "Removing org.xml..."
|
||||
Remove-Files-Zip $android_jar 'org/xml/*'
|
||||
|
||||
Write-Output "Removing org.xmlpull..."
|
||||
Remove-Files-Zip $android_jar 'org/xmlpull/*'
|
||||
|
||||
Write-Output "Removing junit..."
|
||||
Remove-Files-Zip $android_jar 'junit/*'
|
||||
|
||||
Write-Output "Removing javax..."
|
||||
Remove-Files-Zip $android_jar 'javax/*'
|
||||
|
||||
Write-Output "Removing java..."
|
||||
Remove-Files-Zip $android_jar 'java/*'
|
||||
|
||||
Write-Output "Removing overriden classes..."
|
||||
Remove-Files-Zip $android_jar 'android/app/Application.class'
|
||||
Remove-Files-Zip $android_jar 'android/app/Service.class'
|
||||
Remove-Files-Zip $android_jar 'android/net/Uri.class'
|
||||
Remove-Files-Zip $android_jar 'android/net/Uri$Builder.class'
|
||||
Remove-Files-Zip $android_jar 'android/os/Environment.class'
|
||||
Remove-Files-Zip $android_jar 'android/text/format/Formatter.class'
|
||||
Remove-Files-Zip $android_jar 'android/text/Html.class'
|
||||
|
||||
function Dedupe($path)
|
||||
{
|
||||
Push-Location $path
|
||||
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
|
||||
$classes | ForEach-Object {
|
||||
"Processing class: $($_.FullName)"
|
||||
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
|
||||
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
|
||||
}
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
Dedupe "AndroidCompat/src/main/java"
|
||||
Dedupe "server/src/main/java"
|
||||
Dedupe "server/src/main/kotlin"
|
||||
|
||||
Write-Output "Copying Android.jar to library folder..."
|
||||
Move-Item -Force $android_jar "AndroidCompat/lib/android.jar"
|
||||
|
||||
Write-Output "Cleaning up..."
|
||||
Remove-Item -Recurse -Force "tmp"
|
||||
|
||||
Write-Output "Done!"
|
||||
@@ -1,4 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (C) Contributors to the Suwayomi project
|
||||
#
|
||||
# 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/.
|
||||
|
||||
# This is a bash script to create android.jar stubs
|
||||
|
||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
||||
cd ..
|
||||
fi
|
||||
|
||||
|
||||
echo "Getting required Android.jar..."
|
||||
rm -rf "tmp"
|
||||
mkdir -p "tmp"
|
||||
@@ -6,7 +21,7 @@ pushd "tmp"
|
||||
|
||||
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
|
||||
|
||||
# We need to remove any stub classes that we might use
|
||||
# We need to remove any stub classes that we have implementations for
|
||||
echo "Patching JAR..."
|
||||
|
||||
echo "Removing org.json..."
|
||||
@@ -1,20 +1,12 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.duktape;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 kotlin.NotImplementedError;
|
||||
|
||||
import javax.script.ScriptEngine;
|
||||
@@ -22,11 +14,18 @@ import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
import java.io.Closeable;
|
||||
|
||||
/** A simple EMCAScript (Javascript) interpreter. */
|
||||
/* Note (March 2021):
|
||||
* The old implementation for duktape-android used the nashorn engine which is deprecated.
|
||||
* This new implementation uses Mozilla's Rhino: https://github.com/mozilla/rhino
|
||||
*/
|
||||
|
||||
/**
|
||||
* A simple EMCAScript (Javascript) interpreter.
|
||||
*/
|
||||
public final class Duktape implements Closeable, AutoCloseable {
|
||||
|
||||
private ScriptEngineManager factory = new ScriptEngineManager();
|
||||
private ScriptEngine engine = factory.getEngineByName("JavaScript");
|
||||
private ScriptEngine engine = factory.getEngineByName("rhino");
|
||||
|
||||
/**
|
||||
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
|
||||
@@ -38,17 +37,6 @@ public final class Duktape implements Closeable, AutoCloseable {
|
||||
|
||||
private Duktape() {}
|
||||
|
||||
/**
|
||||
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
|
||||
* reporting. Note that the result must be one of the supported Java types or the call will
|
||||
* return null.
|
||||
*
|
||||
* @throws DuktapeException if there is an error evaluating the script.
|
||||
*/
|
||||
public synchronized Object evaluate(String script, String fileName) {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate {@code script} and return a result. Note that the result must be one of the
|
||||
* supported Java types or the call will return null.
|
||||
@@ -76,18 +64,18 @@ public final class Duktape implements Closeable, AutoCloseable {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||
* any overloaded methods.
|
||||
* <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||
* {@link Double}, {@link String}.
|
||||
*/
|
||||
public synchronized <T> T get(final String name, final Class<T> type) {
|
||||
throw new NotImplementedError("Not implemented!");
|
||||
}
|
||||
// /**
|
||||
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||
// * any overloaded methods.
|
||||
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||
// * {@link Double}, {@link String}.
|
||||
// */
|
||||
// public synchronized <T> T get(final String name, final Class<T> type) {
|
||||
// throw new NotImplementedError("Not implemented!");
|
||||
// }
|
||||
|
||||
/**
|
||||
* Release the native resources associated with this object. You <strong>must</strong> call this
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.squareup.duktape;
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
|
||||
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/** This is the reference Duktape stub that tachiyomi's extensions depend on.
|
||||
* Intended to be used as a reference.
|
||||
*/
|
||||
public class DuktapeStub implements Closeable {
|
||||
|
||||
public static Duktape create() {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public synchronized Object evaluate(String script) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
public synchronized <T> void set(String name, Class<T> type, T object) {
|
||||
throw new RuntimeException("Stub!");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||

|
||||

|
||||
# Tachidesk
|
||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||
|
||||
@@ -18,22 +18,28 @@ Here is a list of current features:
|
||||
|
||||
**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.
|
||||
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/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.
|
||||
### All Operating Systems
|
||||
You should have The Java Runtime Environment(JRE) 8 or newer 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).
|
||||
Download the latest jar release from [the releases section](https://github.com/Suwayomi/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.
|
||||
### Windows
|
||||
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||
|
||||
#### Running on Docker
|
||||
The Windows specific build has java bundled inside, 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 works like the previous section.
|
||||
|
||||
### Arch Linux
|
||||
You can install Tachidesk from the AUR
|
||||
```
|
||||
yay -S tachidesk
|
||||
```
|
||||
|
||||
### Docker
|
||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||
|
||||
## General troubleshooting
|
||||
@@ -43,7 +49,7 @@ 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 Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
||||
|
||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||
|
||||
@@ -56,18 +62,25 @@ This project has two components:
|
||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||
|
||||
## Building from source
|
||||
### Get Android stubs jar
|
||||
### Prerequisite: 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`.
|
||||
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||
#### 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 `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||
### Prerequisite: Software dependencies
|
||||
You need this software packages installed in order to build this project:
|
||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||
- Nodejs LTS or latest
|
||||
- Yarn
|
||||
### building the full-blown jar
|
||||
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||
### building without `webUI` bundled
|
||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar -x :webUI:copyBuild`, 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
|
||||
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||
### `webUI` module
|
||||
How to do it is described in `webUI/react/README.md` but for short,
|
||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||
@@ -76,7 +89,9 @@ How to do it is described in `webUI/react/README.md` but for short,
|
||||
and supports HMR and all the other goodies you'll need.
|
||||
|
||||
## 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`.
|
||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
||||
|
||||
The `AndroidCompat` module 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`.
|
||||
|
||||
@@ -86,7 +101,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
|
||||
|
||||
## License
|
||||
|
||||
Copyright (C) 2020-2021 Aria Moradi and contributors
|
||||
Copyright (C) Contributors to the Suwayomi project
|
||||
|
||||
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
|
||||
|
||||
+3
-1
@@ -61,7 +61,7 @@ configure(listOf(
|
||||
|
||||
// Logging
|
||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||
implementation("org.slf4j:slf4j-simple:1.7.30")
|
||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
||||
|
||||
// RxJava
|
||||
@@ -76,6 +76,8 @@ configure(listOf(
|
||||
|
||||
// dependency of :AndroidCompat:Config
|
||||
implementation("com.typesafe:config:1.4.0")
|
||||
implementation("io.github.config4k:config4k:0.4.2")
|
||||
|
||||
|
||||
// to get application content root
|
||||
implementation("net.harawata:appdirs:1.2.0")
|
||||
|
||||
@@ -9,7 +9,7 @@ plugins {
|
||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.2.4"
|
||||
val TachideskVersion = "v0.2.6"
|
||||
|
||||
|
||||
repositories {
|
||||
@@ -57,19 +57,17 @@ dependencies {
|
||||
|
||||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||
|
||||
|
||||
val coroutinesVersion = "1.3.9"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
|
||||
// dex2jar
|
||||
implementation(fileTree("lib/dex2jar/"))
|
||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||
|
||||
|
||||
// api
|
||||
implementation("io.javalin:javalin:3.12.0")
|
||||
implementation("org.slf4j:slf4j-simple:1.8.0-beta4")
|
||||
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||
|
||||
// Exposed ORM
|
||||
@@ -87,9 +85,8 @@ dependencies {
|
||||
implementation(project(":AndroidCompat"))
|
||||
implementation(project(":AndroidCompat:Config"))
|
||||
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
// uncomment to test extensions directly
|
||||
// implementation(fileTree("lib/"))
|
||||
}
|
||||
|
||||
val name = "ir.armor.tachidesk.Main"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.app.Application
|
||||
import android.content.Context
|
||||
// import android.content.res.Configuration
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.app.Application
|
||||
import com.google.gson.Gson
|
||||
// import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi.extension.api
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.content.Context
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi.extension.util
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.content.pm.PackageInfo
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.annotation.SuppressLint
|
||||
// import android.content.Context
|
||||
// import android.os.Build
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 android.content.Context
|
||||
// import eu.kanade.tachiyomi.BuildConfig
|
||||
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
|
||||
@@ -1,248 +1,22 @@
|
||||
package ir.armor.tachidesk
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.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
|
||||
import ir.armor.tachidesk.server.applicationSetup
|
||||
import ir.armor.tachidesk.server.javalinSetup
|
||||
|
||||
class Main {
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
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")
|
||||
} catch (e: RuntimeException) {
|
||||
println("Warning: react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
}
|
||||
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("installing $apkName")
|
||||
|
||||
ctx.status(
|
||||
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()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||
}
|
||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
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))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapter(chapterId, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterId, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||
}
|
||||
|
||||
// source filter list
|
||||
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
applicationSetup()
|
||||
javalinSetup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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"))
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package ir.armor.tachidesk.database
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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
|
||||
@@ -12,6 +14,7 @@ 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 ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -12,5 +15,7 @@ data class ChapterDataClass(
|
||||
val chapter_number: Float,
|
||||
val scanlator: String?,
|
||||
val mangaId: Int,
|
||||
val chapterIndex: Int,
|
||||
val chapterCount: Int,
|
||||
val pageCount: Int? = null,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -8,7 +11,7 @@ import ir.armor.tachidesk.database.table.MangaStatus
|
||||
|
||||
data class MangaDataClass(
|
||||
val id: Int,
|
||||
val sourceId: Long,
|
||||
val sourceId: String,
|
||||
|
||||
val url: String,
|
||||
val title: String,
|
||||
@@ -21,7 +24,8 @@ data class MangaDataClass(
|
||||
val description: String? = null,
|
||||
val genre: String? = null,
|
||||
val status: String = MangaStatus.UNKNOWN.name,
|
||||
val inLibrary: Boolean = false
|
||||
val inLibrary: Boolean = false,
|
||||
val source: SourceDataClass? = null
|
||||
)
|
||||
|
||||
data class PagedMangaListDataClass(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package ir.armor.tachidesk.database.dataclass
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 name: String?,
|
||||
val lang: String?,
|
||||
val iconUrl: String?,
|
||||
val supportsLatest: Boolean?
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.entity
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 CategoryMangaTable : IntIdTable() {
|
||||
val category = reference("category", CategoryTable)
|
||||
val manga = reference("manga", MangaTable)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -13,5 +16,7 @@ object ChapterTable : IntIdTable() {
|
||||
val chapter_number = float("chapter_number").default(-1f)
|
||||
val scanlator = varchar("scanlator", 128).nullable()
|
||||
|
||||
val chapterIndex = integer("number_in_list")
|
||||
|
||||
val manga = reference("manga", MangaTable)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ir.armor.tachidesk.impl.proxyThumbnailUrl
|
||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
|
||||
@@ -28,13 +31,13 @@ object MangaTable : IntIdTable() {
|
||||
val defaultCategory = bool("default_category").default(true)
|
||||
|
||||
// source is used by some ancestor of IntIdTable
|
||||
val sourceReference = reference("source", SourceTable)
|
||||
val sourceReference = long("source")
|
||||
}
|
||||
|
||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
MangaDataClass(
|
||||
mangaEntry[MangaTable.id].value,
|
||||
mangaEntry[sourceReference].value,
|
||||
mangaEntry[sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.database.table
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
+8
-5
@@ -1,4 +1,11 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
@@ -12,10 +19,6 @@ 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()
|
||||
+8
-5
@@ -1,4 +1,11 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
@@ -14,10 +21,6 @@ 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) {
|
||||
+51
-11
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -14,11 +17,13 @@ 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.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
val mangaDetails = getManga(mangaId)
|
||||
val source = getHttpSource(mangaDetails.sourceId)
|
||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||
|
||||
val chapterList = source.fetchChapterList(
|
||||
SManga.create().apply {
|
||||
@@ -27,8 +32,10 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
val chapterCount = chapterList.count()
|
||||
|
||||
return transaction {
|
||||
chapterList.forEach { fetchedChapter ->
|
||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||
if (chapterEntry == null) {
|
||||
ChapterTable.insertAndGetId {
|
||||
@@ -38,12 +45,29 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
it[scanlator] = fetchedChapter.scanlator
|
||||
|
||||
it[chapterIndex] = index + 1
|
||||
it[manga] = mangaId
|
||||
}
|
||||
} else {
|
||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||
it[name] = fetchedChapter.name
|
||||
it[date_upload] = fetchedChapter.date_upload
|
||||
it[chapter_number] = fetchedChapter.chapter_number
|
||||
it[scanlator] = fetchedChapter.scanlator
|
||||
|
||||
it[chapterIndex] = index + 1
|
||||
it[manga] = mangaId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@transaction chapterList.map {
|
||||
// clear any orphaned chapters
|
||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||
// TODO
|
||||
}
|
||||
|
||||
return@transaction chapterList.mapIndexed { index, it ->
|
||||
ChapterDataClass(
|
||||
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
|
||||
it.url,
|
||||
@@ -51,18 +75,21 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
||||
it.date_upload,
|
||||
it.chapter_number,
|
||||
it.scanlator,
|
||||
mangaId
|
||||
mangaId,
|
||||
chapterCount - index,
|
||||
chapterCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
|
||||
return transaction {
|
||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||
val chapterEntry = ChapterTable.select {
|
||||
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
|
||||
}.firstOrNull()!!
|
||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val pageList = source.fetchPageList(
|
||||
SChapter.create().apply {
|
||||
@@ -71,14 +98,20 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
}
|
||||
).toBlocking().first()
|
||||
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
||||
|
||||
val chapter = ChapterDataClass(
|
||||
chapterEntry[ChapterTable.id].value,
|
||||
chapterId,
|
||||
chapterEntry[ChapterTable.url],
|
||||
chapterEntry[ChapterTable.name],
|
||||
chapterEntry[ChapterTable.date_upload],
|
||||
chapterEntry[ChapterTable.chapter_number],
|
||||
chapterEntry[ChapterTable.scanlator],
|
||||
mangaId,
|
||||
chapterEntry[ChapterTable.chapterIndex],
|
||||
chapterCount.toInt(),
|
||||
|
||||
pageList.count()
|
||||
)
|
||||
|
||||
@@ -93,6 +126,13 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||
it[this.chapter] = chapterId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
transaction {
|
||||
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
|
||||
it[url] = page.url
|
||||
it[imageUrl] = page.imageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+58
-23
@@ -1,20 +1,26 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||
import com.googlecode.d2j.dex.Dex2jar
|
||||
import com.googlecode.d2j.reader.MultiDexFileReader
|
||||
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
|
||||
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.applicationDirs
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.impl.util.APKExtractor
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
@@ -28,8 +34,45 @@ import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
|
||||
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
|
||||
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
|
||||
|
||||
val jarFilePath = File(jarFile).toPath()
|
||||
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
|
||||
val handler = BaksmaliBaseDexExceptionHandler()
|
||||
Dex2jar
|
||||
.from(reader)
|
||||
.withExceptionHandler(handler)
|
||||
.reUseReg(false)
|
||||
.topoLogicalSort()
|
||||
.skipDebug(true)
|
||||
.optimizeSynchronized(false)
|
||||
.printIR(false)
|
||||
.noCode(false)
|
||||
.skipExceptions(false)
|
||||
.to(jarFilePath)
|
||||
if (handler.hasException()) {
|
||||
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||
logger.error(
|
||||
"Detail Error Information in File $errorFile\n" +
|
||||
"Please report this file to one of following link if possible (any one).\n" +
|
||||
" https://sourceforge.net/p/dex2jar/tickets/\n" +
|
||||
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
|
||||
" https://github.com/pxb1988/dex2jar/issues\n" +
|
||||
" dex2jar@googlegroups.com"
|
||||
)
|
||||
handler.dump(errorFile, emptyArray<String>())
|
||||
}
|
||||
}
|
||||
|
||||
fun installAPK(apkName: String): Int {
|
||||
logger.debug("Installing $apkName")
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
|
||||
@@ -49,9 +92,9 @@ fun installAPK(apkName: String): Int {
|
||||
downloadAPKFile(apkToDownload, apkFilePath)
|
||||
|
||||
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
|
||||
println(className)
|
||||
logger.debug(className)
|
||||
// dex -> jar
|
||||
Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force")
|
||||
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
|
||||
|
||||
// clean up
|
||||
File(apkFilePath).delete()
|
||||
@@ -69,11 +112,6 @@ fun installAPK(apkName: String): Int {
|
||||
if (instance is HttpSource) { // single source
|
||||
val httpSource = instance as HttpSource
|
||||
transaction {
|
||||
// SourceEntity.new {
|
||||
// sourceId = httpSource.id
|
||||
// name = httpSource.name
|
||||
// this.extension = ExtensionEntity.find { ExtensionsTable.name eq extension.name }.first().id
|
||||
// }
|
||||
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
|
||||
SourceTable.insert {
|
||||
it[this.id] = httpSource.id
|
||||
@@ -82,9 +120,7 @@ fun installAPK(apkName: String): Int {
|
||||
it[extension] = extensionId
|
||||
}
|
||||
}
|
||||
// println(httpSource.id)
|
||||
// println(httpSource.name)
|
||||
// println()
|
||||
logger.debug("Installed source ${httpSource.name} with id {httpSource.id}")
|
||||
}
|
||||
} else { // multi source
|
||||
val sourceFactory = instance as SourceFactory
|
||||
@@ -101,9 +137,7 @@ fun installAPK(apkName: String): Int {
|
||||
it[positionInFactorySource] = index
|
||||
}
|
||||
}
|
||||
// println(httpSource.id)
|
||||
// println(httpSource.name)
|
||||
// println()
|
||||
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,9 +168,11 @@ private fun downloadAPKFile(url: String, apkPath: String) {
|
||||
sink.close()
|
||||
}
|
||||
|
||||
fun removeExtension(pkgName: String) {
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
|
||||
val fileNameWithoutType = pkgName.substringBefore(".apk")
|
||||
fun removeExtension(apkName: String) {
|
||||
logger.debug("Uninstalling $apkName")
|
||||
|
||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
transaction {
|
||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||
@@ -158,9 +194,8 @@ 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) {
|
||||
return getCachedImageResponse(saveDir, apkName) {
|
||||
network.client.newCall(
|
||||
GET(iconUrl)
|
||||
).execute()
|
||||
+10
-4
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -9,12 +12,15 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mu.KotlinLogging
|
||||
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
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private object Data {
|
||||
var lastExtensionCheck: Long = 0
|
||||
}
|
||||
@@ -28,7 +34,7 @@ private fun extensionDatabaseIsEmtpy(): Boolean {
|
||||
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
// update if 60 seconds has passed or requested offline and database is empty
|
||||
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
|
||||
println("Getting extensions list from the internet")
|
||||
logger.debug("Getting extensions list from the internet")
|
||||
Data.lastExtensionCheck = System.currentTimeMillis()
|
||||
var foundExtensions: List<Extension.Available>
|
||||
runBlocking {
|
||||
@@ -66,7 +72,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("used cached extension list")
|
||||
logger.debug("used cached extension list")
|
||||
}
|
||||
|
||||
return transaction {
|
||||
+8
-6
@@ -1,20 +1,22 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.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) {
|
||||
+15
-10
@@ -1,15 +1,18 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
@@ -21,7 +24,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
return if (mangaEntry[MangaTable.initialized]) {
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
@@ -34,10 +37,11 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
mangaEntry[MangaTable.description],
|
||||
mangaEntry[MangaTable.genre],
|
||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||
mangaEntry[MangaTable.inLibrary]
|
||||
mangaEntry[MangaTable.inLibrary],
|
||||
getSource(mangaEntry[MangaTable.sourceReference])
|
||||
)
|
||||
} else { // initialize manga
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val fetchedManga = source.fetchMangaDetails(
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
@@ -65,7 +69,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
mangaEntry[MangaTable.sourceReference].value,
|
||||
mangaEntry[MangaTable.sourceReference].toString(),
|
||||
|
||||
mangaEntry[MangaTable.url],
|
||||
mangaEntry[MangaTable.title],
|
||||
@@ -78,7 +82,8 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||
fetchedManga.description,
|
||||
fetchedManga.genre,
|
||||
MangaStatus.valueOf(fetchedManga.status).name,
|
||||
false
|
||||
false,
|
||||
getSource(mangaEntry[MangaTable.sourceReference])
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -88,8 +93,8 @@ fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
return getCachedResponse(saveDir, fileName) {
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||
return getCachedImageResponse(saveDir, fileName) {
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||
+5
-2
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
+7
-4
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -52,7 +55,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
@@ -70,7 +73,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
||||
val mangaId = mangaEntry[MangaTable.id].value
|
||||
MangaDataClass(
|
||||
mangaId,
|
||||
sourceId,
|
||||
sourceId.toString(),
|
||||
|
||||
manga.url,
|
||||
manga.title,
|
||||
+17
-8
@@ -1,16 +1,19 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ir.armor.tachidesk.server.applicationDirs
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
@@ -25,10 +28,16 @@ fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||
return page.imageUrl!!
|
||||
}
|
||||
|
||||
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||
fun getPageImage(mangaId: Int, chapterIndex: 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 source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||
val chapterEntry = transaction {
|
||||
ChapterTable.select {
|
||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||
}.firstOrNull()!!
|
||||
}
|
||||
val chapterId = chapterEntry[ChapterTable.id].value
|
||||
|
||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||
|
||||
val tachiPage = Page(
|
||||
@@ -49,14 +58,14 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
|
||||
File(saveDir).mkdirs()
|
||||
val fileName = index.toString()
|
||||
|
||||
return getCachedResponse(saveDir, fileName) {
|
||||
return getCachedImageResponse(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 sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
val source = getHttpSource(sourceId)
|
||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||
+6
-2
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -18,6 +21,7 @@ fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaLi
|
||||
}
|
||||
|
||||
fun sourceGlobalSearch(searchTerm: String) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
data class FilterWrapper(
|
||||
+23
-16
@@ -1,36 +1,45 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.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.ExtensionTable
|
||||
import ir.armor.tachidesk.database.table.SourceTable
|
||||
import ir.armor.tachidesk.server.applicationDirs
|
||||
import mu.KotlinLogging
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.selectAll
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import java.lang.NullPointerException
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.util.Locale
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
|
||||
private val extensionCache = mutableListOf<Pair<String, Any>>()
|
||||
|
||||
fun getHttpSource(sourceId: Long): HttpSource {
|
||||
val sourceRecord = transaction {
|
||||
SourceEntity.findById(sourceId)
|
||||
} ?: throw NullPointerException("Source with id $sourceId is not installed")
|
||||
|
||||
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
|
||||
if (cachedResult != null) {
|
||||
println("used cached HttpSource: ${cachedResult.second.name}")
|
||||
logger.debug("used cached HttpSource: ${cachedResult.second.name}")
|
||||
return cachedResult.second
|
||||
}
|
||||
|
||||
val result: HttpSource = transaction {
|
||||
val sourceRecord = SourceEntity.findById(sourceId)!!
|
||||
val extensionId = sourceRecord.extension.id.value
|
||||
val extensionRecord = ExtensionEntity.findById(extensionId)!!
|
||||
val apkName = extensionRecord.apkName
|
||||
@@ -38,17 +47,15 @@ fun getHttpSource(sourceId: Long): HttpSource {
|
||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||
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")
|
||||
logger.debug("Used cached Extension")
|
||||
cachedExtensionPair.second
|
||||
} else {
|
||||
println("No Extension cache")
|
||||
logger.debug("No Extension cache")
|
||||
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
|
||||
val classToLoad = Class.forName(className, true, child)
|
||||
classToLoad.newInstance()
|
||||
@@ -87,14 +94,14 @@ fun getSourceList(): List<SourceDataClass> {
|
||||
|
||||
fun getSource(sourceId: Long): SourceDataClass {
|
||||
return transaction {
|
||||
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
|
||||
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
|
||||
sourceId.toString(),
|
||||
source?.get(SourceTable.name),
|
||||
source?.get(SourceTable.lang),
|
||||
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
|
||||
source?.let { getHttpSource(sourceId).supportsLatest }
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk;
|
||||
package ir.armor.tachidesk.impl.util;
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -16,7 +19,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public class APKExtractor {
|
||||
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
|
||||
+6
-3
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.impl
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -52,7 +55,7 @@ private fun BufferedSource.saveTo(stream: OutputStream) {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||
val filePath = "$saveDir/$fileName"
|
||||
if (cachedFile != null) {
|
||||
@@ -0,0 +1,260 @@
|
||||
package ir.armor.tachidesk.server
|
||||
|
||||
import io.javalin.Javalin
|
||||
import ir.armor.tachidesk.Main
|
||||
import ir.armor.tachidesk.impl.addMangaToCategory
|
||||
import ir.armor.tachidesk.impl.addMangaToLibrary
|
||||
import ir.armor.tachidesk.impl.createCategory
|
||||
import ir.armor.tachidesk.impl.getCategoryList
|
||||
import ir.armor.tachidesk.impl.getCategoryMangaList
|
||||
import ir.armor.tachidesk.impl.getChapter
|
||||
import ir.armor.tachidesk.impl.getChapterList
|
||||
import ir.armor.tachidesk.impl.getExtensionIcon
|
||||
import ir.armor.tachidesk.impl.getExtensionList
|
||||
import ir.armor.tachidesk.impl.getLibraryMangas
|
||||
import ir.armor.tachidesk.impl.getManga
|
||||
import ir.armor.tachidesk.impl.getMangaCategories
|
||||
import ir.armor.tachidesk.impl.getMangaList
|
||||
import ir.armor.tachidesk.impl.getPageImage
|
||||
import ir.armor.tachidesk.impl.getSource
|
||||
import ir.armor.tachidesk.impl.getSourceList
|
||||
import ir.armor.tachidesk.impl.getThumbnail
|
||||
import ir.armor.tachidesk.impl.installAPK
|
||||
import ir.armor.tachidesk.impl.removeCategory
|
||||
import ir.armor.tachidesk.impl.removeExtension
|
||||
import ir.armor.tachidesk.impl.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.impl.removeMangaFromLibrary
|
||||
import ir.armor.tachidesk.impl.reorderCategory
|
||||
import ir.armor.tachidesk.impl.sourceFilters
|
||||
import ir.armor.tachidesk.impl.sourceGlobalSearch
|
||||
import ir.armor.tachidesk.impl.sourceSearch
|
||||
import ir.armor.tachidesk.impl.updateCategory
|
||||
import ir.armor.tachidesk.server.util.openInBrowser
|
||||
import mu.KotlinLogging
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun javalinSetup() {
|
||||
var hasWebUiBundled = false
|
||||
|
||||
val app = Javalin.create { config ->
|
||||
try {
|
||||
Main::class.java.getResource("/react/index.html")
|
||||
hasWebUiBundled = true
|
||||
config.addStaticFiles("/react")
|
||||
config.addSinglePageRoot("/", "/react/index.html")
|
||||
} catch (e: RuntimeException) {
|
||||
logger.warn("react build files are missing.")
|
||||
hasWebUiBundled = false
|
||||
}
|
||||
config.enableCorsForAllOrigins()
|
||||
}.start(serverConfig.ip, serverConfig.port)
|
||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||
openInBrowser()
|
||||
}
|
||||
|
||||
app.exception(NullPointerException::class.java) { e, ctx ->
|
||||
logger.error("NullPointerException while handling the request", e)
|
||||
ctx.status(404)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/list") { ctx ->
|
||||
ctx.json(getExtensionList())
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
ctx.status(
|
||||
installAPK(apkName)
|
||||
)
|
||||
}
|
||||
|
||||
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||
val apkName = ctx.pathParam("apkName")
|
||||
|
||||
removeExtension(apkName)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// icon for extension named `apkName`
|
||||
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)
|
||||
}
|
||||
|
||||
// list of sources
|
||||
app.get("/api/v1/source/list") { ctx ->
|
||||
ctx.json(getSourceList())
|
||||
}
|
||||
|
||||
// fetch source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(getSource(sourceId))
|
||||
}
|
||||
|
||||
// popular mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = true))
|
||||
}
|
||||
|
||||
// latest mangas from source with id `sourceId`
|
||||
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(getMangaList(sourceId, pageNum, popular = false))
|
||||
}
|
||||
|
||||
// get manga info
|
||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getManga(mangaId))
|
||||
}
|
||||
|
||||
// manga thumbnail
|
||||
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)
|
||||
}
|
||||
|
||||
// list manga's categories
|
||||
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))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getChapter(chapterIndex, mangaId))
|
||||
}
|
||||
|
||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||
val index = ctx.pathParam("index").toInt()
|
||||
val result = getPageImage(mangaId, chapterIndex, index)
|
||||
|
||||
ctx.result(result.first)
|
||||
ctx.header("content-type", result.second)
|
||||
}
|
||||
|
||||
// global search
|
||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
ctx.json(sourceGlobalSearch(searchTerm))
|
||||
}
|
||||
|
||||
// single source search
|
||||
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
val searchTerm = ctx.pathParam("searchTerm")
|
||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
||||
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
|
||||
}
|
||||
|
||||
// source filter list
|
||||
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
|
||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||
ctx.json(sourceFilters(sourceId))
|
||||
}
|
||||
|
||||
// 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,31 @@
|
||||
package ir.armor.tachidesk.server
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import xyz.nulldev.ts.config.ConfigModule
|
||||
|
||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||
val ip: String by config
|
||||
val port: Int by config
|
||||
|
||||
// proxy
|
||||
val socksProxy: Boolean by config
|
||||
val socksProxyHost: String by config
|
||||
val socksProxyPort: String by config
|
||||
|
||||
// misc
|
||||
val debugLogsEnabled: Boolean by config
|
||||
val systemTrayEnabled: Boolean by config
|
||||
val initialOpenInBrowserEnabled: Boolean by config
|
||||
|
||||
companion object {
|
||||
fun register(config: Config) = ServerConfig(config.getConfig("server"))
|
||||
}
|
||||
}
|
||||
+45
-4
@@ -1,8 +1,18 @@
|
||||
package ir.armor.tachidesk
|
||||
package ir.armor.tachidesk.server
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 ch.qos.logback.classic.Level
|
||||
import eu.kanade.tachiyomi.App
|
||||
import ir.armor.tachidesk.Main
|
||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||
import ir.armor.tachidesk.util.systemTray
|
||||
import ir.armor.tachidesk.server.util.systemTray
|
||||
import mu.KotlinLogging
|
||||
import net.harawata.appdirs.AppDirsFactory
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.conf.global
|
||||
@@ -12,6 +22,8 @@ import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||
import java.io.File
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
object applicationDirs {
|
||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
@@ -25,12 +37,17 @@ val systemTray by lazy { systemTray() }
|
||||
|
||||
val androidCompat by lazy { AndroidCompat() }
|
||||
|
||||
fun serverSetup() {
|
||||
fun applicationSetup() {
|
||||
// register server config
|
||||
GlobalConfigManager.registerModule(
|
||||
ServerConfig.register(GlobalConfigManager.config)
|
||||
)
|
||||
|
||||
// set application wide logging level
|
||||
if (serverConfig.debugLogsEnabled) {
|
||||
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
|
||||
}
|
||||
|
||||
// make dirs we need
|
||||
listOf(
|
||||
applicationDirs.dataRoot,
|
||||
@@ -41,10 +58,29 @@ fun serverSetup() {
|
||||
File(it).mkdirs()
|
||||
}
|
||||
|
||||
// create conf file if doesn't exist
|
||||
try {
|
||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||
if (!dataConfFile.exists()) {
|
||||
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
||||
dataConfFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Exception while creating initial server.conf:\n", e)
|
||||
}
|
||||
|
||||
makeDataBaseTables()
|
||||
|
||||
// create system tray
|
||||
systemTray
|
||||
if (serverConfig.systemTrayEnabled)
|
||||
try {
|
||||
systemTray
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
// Load config API
|
||||
DI.global.addImport(ConfigKodeinModule().create())
|
||||
@@ -53,6 +89,11 @@ fun serverSetup() {
|
||||
// start app
|
||||
androidCompat.startApp(App())
|
||||
|
||||
// Disable jetty's logging
|
||||
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
|
||||
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
|
||||
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
|
||||
|
||||
// socks proxy settings
|
||||
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
|
||||
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||
+7
-3
@@ -1,6 +1,9 @@
|
||||
package ir.armor.tachidesk.util
|
||||
package ir.armor.tachidesk.server.util
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -10,6 +13,7 @@ import dorkbox.systemTray.SystemTray.TrayType
|
||||
import dorkbox.util.CacheUtil
|
||||
import dorkbox.util.Desktop
|
||||
import ir.armor.tachidesk.Main
|
||||
import ir.armor.tachidesk.server.serverConfig
|
||||
import java.awt.event.ActionListener
|
||||
import java.io.IOException
|
||||
|
||||
@@ -24,7 +28,7 @@ fun openInBrowser() {
|
||||
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
|
||||
SystemTray.DEBUG = serverConfig.debugLogsEnabled
|
||||
if (System.getProperty("os.name").startsWith("Windows"))
|
||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="Exposed" level="ERROR"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -1,8 +1,13 @@
|
||||
# Server ip and port bindings
|
||||
server.ip = 0.0.0.0
|
||||
server.ip = "0.0.0.0"
|
||||
server.port = 4567
|
||||
|
||||
# Socks5 proxy
|
||||
server.socksProxy = false
|
||||
server.socksProxyHost = ""
|
||||
server.socksProxyPort = ""
|
||||
server.socksProxyPort = ""
|
||||
|
||||
# misc
|
||||
server.debugLogsEnabled = false
|
||||
server.systemTrayEnabled = true
|
||||
server.initialOpenInBrowserEnabled = true
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* This Kotlin source file was generated by the Gradle 'init' task.
|
||||
*/
|
||||
package ir.armor.tachidesk
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class AppTest {
|
||||
@Test fun testAppHasAGreeting() {
|
||||
assertTrue(true)
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/react-lazyload": "^3.1.0",
|
||||
"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-lazyload": "^3.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"web-vitals": "^0.2.4"
|
||||
|
||||
+21
-5
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -27,9 +30,12 @@ import useLocalStorage from './util/useLocalStorage';
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
const [action, setAction] = useState<any>(<div />);
|
||||
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
|
||||
|
||||
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||
|
||||
const navBarContext = {
|
||||
title, setTitle, action, setAction,
|
||||
title, setTitle, action, setAction, override, setOverride,
|
||||
};
|
||||
const darkThemeContext = { darkTheme, setDarkTheme };
|
||||
|
||||
@@ -63,7 +69,12 @@ export default function App() {
|
||||
<NavbarContext.Provider value={navBarContext}>
|
||||
<CssBaseline />
|
||||
<NavBar />
|
||||
<Container maxWidth={false} disableGutters>
|
||||
<Container
|
||||
id="appMainContainer"
|
||||
maxWidth={false}
|
||||
disableGutters
|
||||
style={{ paddingTop: '64px' }}
|
||||
>
|
||||
<Switch>
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
<Search />
|
||||
@@ -80,8 +91,8 @@ export default function App() {
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
</Route>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterId">
|
||||
<Reader />
|
||||
<Route path="/manga/:mangaId/chapter/:chapterNum">
|
||||
<></>
|
||||
</Route>
|
||||
<Route path="/manga/:id">
|
||||
<Manga />
|
||||
@@ -106,6 +117,11 @@ export default function App() {
|
||||
/>
|
||||
</Switch>
|
||||
</Container>
|
||||
<Switch>
|
||||
<Route path="/manga/:mangaId/chapter/:chapterIndex">
|
||||
<Reader />
|
||||
</Route>
|
||||
</Switch>
|
||||
</NavbarContext.Provider>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -85,6 +88,14 @@ export default function CategorySelect(props: IProps) {
|
||||
<DialogTitle>Set categories</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{categoryInfos.length === 0
|
||||
&& (
|
||||
<span>
|
||||
No categories found!
|
||||
<br />
|
||||
You should make some from settings.
|
||||
</span>
|
||||
)}
|
||||
{categoryInfos.map((categoryInfo) => (
|
||||
<FormControlLabel
|
||||
control={(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -8,6 +12,7 @@ import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -41,6 +46,7 @@ interface IProps{
|
||||
|
||||
export default function ChapterCard(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const history = useHistory();
|
||||
const { chapter } = props;
|
||||
|
||||
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
|
||||
@@ -63,9 +69,19 @@ export default function ChapterCard(props: IProps) {
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button>
|
||||
</div>
|
||||
<Link
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
style={{ marginLeft: 20 }}
|
||||
>
|
||||
open
|
||||
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/require-default-props */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
loading: {
|
||||
margin: '10px auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
shouldRender: boolean | (() => boolean)
|
||||
children?: React.ReactNode
|
||||
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
|
||||
componentProps?: any
|
||||
}
|
||||
|
||||
export default function LoadingPlaceholder(props: IProps) {
|
||||
const {
|
||||
children, shouldRender, component, componentProps,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const condition = shouldRender instanceof Function ? shouldRender() : shouldRender;
|
||||
|
||||
if (condition) {
|
||||
if (component) {
|
||||
return React.createElement(component, componentProps);
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -43,7 +46,7 @@ const useStyles = makeStyles({
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
manga: IManga
|
||||
manga: IMangaCard
|
||||
}
|
||||
const MangaCard = React.forwardRef((props: IProps, ref) => {
|
||||
const {
|
||||
|
||||
@@ -1,18 +1,117 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 { makeStyles } from '@material-ui/core';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { Theme } from '@material-ui/core/styles';
|
||||
import FavoriteIcon from '@material-ui/icons/Favorite';
|
||||
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import PublicIcon from '@material-ui/icons/Public';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import NavbarContext from '../context/NavbarContext';
|
||||
import client from '../util/client';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
width: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
position: 'sticky',
|
||||
top: '64px',
|
||||
left: '0px',
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
alignSelf: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
top: {
|
||||
padding: '10px',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '50%',
|
||||
// },
|
||||
},
|
||||
leftRight: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
leftSide: {
|
||||
'& img': {
|
||||
borderRadius: 4,
|
||||
maxWidth: '100%',
|
||||
minWidth: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
maxWidth: '50%',
|
||||
// [theme.breakpoints.up('md')]: {
|
||||
// minWidth: '100px',
|
||||
// },
|
||||
},
|
||||
rightSide: {
|
||||
marginLeft: 15,
|
||||
maxWidth: '100%',
|
||||
'& span': {
|
||||
fontWeight: '400',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
'& button': {
|
||||
marginLeft: 10,
|
||||
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
|
||||
},
|
||||
'& span': {
|
||||
display: 'block',
|
||||
fontSize: '0.85em',
|
||||
},
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: '#858585',
|
||||
'& button': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
},
|
||||
bottom: {
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
fontSize: '1.2em',
|
||||
// maxWidth: '50%',
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
fontSize: '1.3em',
|
||||
},
|
||||
},
|
||||
description: {
|
||||
'& h4': {
|
||||
marginTop: '1em',
|
||||
marginBottom: 0,
|
||||
},
|
||||
'& p': {
|
||||
textAlign: 'justify',
|
||||
textJustify: 'inter-word',
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& h5': {
|
||||
border: '2px solid #2196f3',
|
||||
borderRadius: '1.13em',
|
||||
marginRight: '1em',
|
||||
marginTop: 0,
|
||||
marginBottom: '10px',
|
||||
padding: '0.3em',
|
||||
color: '#2196f3',
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -21,30 +120,70 @@ interface IProps{
|
||||
manga: IManga
|
||||
}
|
||||
|
||||
function getSourceName(source: ISource) {
|
||||
if (source.name !== null) {
|
||||
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
|
||||
}
|
||||
return source.id;
|
||||
}
|
||||
|
||||
function getValueOrUnknown(val: string) {
|
||||
return val || 'UNKNOWN';
|
||||
}
|
||||
|
||||
export default function MangaDetails(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { setAction } = useContext(NavbarContext);
|
||||
|
||||
const { manga } = props;
|
||||
const [inLibrary, setInLibrary] = useState<string>(
|
||||
manga.inLibrary ? 'In Library' : 'Not In Library',
|
||||
manga.inLibrary ? 'In Library' : 'Add To Library',
|
||||
);
|
||||
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inLibrary === 'In Library') {
|
||||
setAction(
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => setCategoryDialogOpen(true)}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</>,
|
||||
|
||||
);
|
||||
} else { setAction(<></>); }
|
||||
}, [inLibrary, categoryDialogOpen]);
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const classes = useStyles(inLibrary)();
|
||||
|
||||
function addToLibrary() {
|
||||
setInLibrary('adding');
|
||||
// setInLibrary('adding');
|
||||
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('In Library');
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromLibrary() {
|
||||
setInLibrary('removing');
|
||||
// setInLibrary('removing');
|
||||
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||
setInLibrary('Not In Library');
|
||||
setInLibrary('Add To Library');
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick() {
|
||||
if (inLibrary === 'Not In Library') {
|
||||
if (inLibrary === 'Add To Library') {
|
||||
addToLibrary();
|
||||
} else {
|
||||
removeFromLibrary();
|
||||
@@ -52,21 +191,64 @@ export default function MangaDetails(props: IProps) {
|
||||
}
|
||||
|
||||
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 className={classes.root}>
|
||||
<div className={classes.top}>
|
||||
<div className={classes.leftRight}>
|
||||
<div className={classes.leftSide}>
|
||||
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
|
||||
</div>
|
||||
<div className={classes.rightSide}>
|
||||
<h1>
|
||||
{manga.title}
|
||||
</h1>
|
||||
<h3>
|
||||
Author:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.author)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Artist:
|
||||
{' '}
|
||||
<span>{getValueOrUnknown(manga.artist)}</span>
|
||||
</h3>
|
||||
<h3>
|
||||
Status:
|
||||
{' '}
|
||||
{manga.status}
|
||||
</h3>
|
||||
<h3>
|
||||
Source:
|
||||
{' '}
|
||||
{getSourceName(manga.source)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.buttons}>
|
||||
<div>
|
||||
<IconButton onClick={() => handleButtonClick()}>
|
||||
{inLibrary === 'In Library' && <FavoriteIcon />}
|
||||
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
|
||||
<span>{inLibrary}</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
|
||||
<a href={manga.url} target="_blank">
|
||||
<IconButton>
|
||||
<PublicIcon />
|
||||
<span>Open Site</span>
|
||||
</IconButton>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.bottom}>
|
||||
<div className={classes.description}>
|
||||
<h4>About</h4>
|
||||
<p>{manga.description}</p>
|
||||
</div>
|
||||
<div className={classes.genre}>
|
||||
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
|
||||
</div>
|
||||
</div>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -7,7 +10,7 @@ import Grid from '@material-ui/core/Grid';
|
||||
import MangaCard from './MangaCard';
|
||||
|
||||
interface IProps{
|
||||
mangas: IManga[]
|
||||
mangas: IMangaCard[]
|
||||
message?: string
|
||||
hasNextPage: boolean
|
||||
lastPageNum: number
|
||||
@@ -48,7 +51,7 @@ export default function MangaGrid(props: IProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
|
||||
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
|
||||
{mapped}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// TODO: remove above!
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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';
|
||||
import TemporaryDrawer from './TemporaryDrawer';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -31,89 +28,40 @@ 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 { title, action, override } = useContext(NavBarContext);
|
||||
|
||||
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" color={darkTheme ? 'default' : 'primary'}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
className={classes.menuButton}
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
{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(); }}
|
||||
<>
|
||||
{override.status && override.value}
|
||||
{!override.status
|
||||
&& (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
className={classes.menuButton}
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
Dark Theme
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => { setDarkTheme(false); handleClose(); }}
|
||||
>
|
||||
Light Theme
|
||||
|
||||
</MenuItem>
|
||||
</Menu> */}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||
</div>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
{action}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import { IReaderSettings } from './ReaderNavBar';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
loading: {
|
||||
margin: '100px auto',
|
||||
height: '100vh',
|
||||
},
|
||||
loadingImage: {
|
||||
padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)',
|
||||
height: '100vh',
|
||||
width: '200px',
|
||||
backgroundColor: '#525252',
|
||||
marginBottom: 10,
|
||||
},
|
||||
image: {
|
||||
display: 'block',
|
||||
marginBottom: settings.continuesPageGap ? '15px' : 0,
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
src: string
|
||||
index: number
|
||||
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||
settings: IReaderSettings
|
||||
}
|
||||
|
||||
function LazyImage(props: IProps) {
|
||||
const {
|
||||
src, index, setCurPage, settings,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
const [imageSrc, setImagsrc] = useState<string>('');
|
||||
const ref = useRef<HTMLImageElement>(null);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (ref.current) {
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
if (rect.y < 0 && rect.y + rect.height > 0) {
|
||||
setCurPage(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
|
||||
img.onload = () => setImagsrc(src);
|
||||
}, [src]);
|
||||
|
||||
if (imageSrc.length === 0) {
|
||||
return (
|
||||
<div className={classes.loadingImage}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={classes.image}
|
||||
ref={ref}
|
||||
src={imageSrc}
|
||||
alt={`Page #${index}`}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page(props: IProps) {
|
||||
const {
|
||||
src, index, setCurPage, settings,
|
||||
} = props;
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
return (
|
||||
<div style={{ margin: '0 auto' }}>
|
||||
<LazyLoad
|
||||
offset={window.innerHeight}
|
||||
placeholder={(
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
)}
|
||||
once
|
||||
>
|
||||
<LazyImage
|
||||
src={src}
|
||||
index={index}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
/>
|
||||
</LazyLoad>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 IconButton from '@material-ui/core/IconButton';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { useHistory, Link } from 'react-router-dom';
|
||||
import Slide from '@material-ui/core/Slide';
|
||||
import Fade from '@material-ui/core/Fade';
|
||||
import Zoom from '@material-ui/core/Zoom';
|
||||
import { Switch } from '@material-ui/core';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||
import DarkTheme from '../context/DarkTheme';
|
||||
import NavBarContext from '../context/NavbarContext';
|
||||
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
||||
// main container and root div need to change classes...
|
||||
AppMainContainer: {
|
||||
display: 'none',
|
||||
},
|
||||
AppRootElment: {
|
||||
display: 'flex',
|
||||
},
|
||||
|
||||
root: {
|
||||
position: settings.staticNav ? 'sticky' : 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
minWidth: '300px',
|
||||
height: '100vh',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: '#0a0b0b',
|
||||
|
||||
'& header': {
|
||||
backgroundColor: '#363b3d',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: '64px',
|
||||
paddingLeft: '24px',
|
||||
paddingRight: '24px',
|
||||
|
||||
transition: 'left 2s ease',
|
||||
|
||||
'& button': {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
'& button:nth-child(1)': {
|
||||
marginRight: '16px',
|
||||
},
|
||||
|
||||
'& button:nth-child(3)': {
|
||||
marginRight: '-12px',
|
||||
},
|
||||
|
||||
'& h1': {
|
||||
fontSize: '1.25rem',
|
||||
flexGrow: 1,
|
||||
},
|
||||
},
|
||||
'& hr': {
|
||||
margin: '0 16px',
|
||||
height: '1px',
|
||||
border: '0',
|
||||
backgroundColor: 'rgb(38, 41, 43)',
|
||||
},
|
||||
},
|
||||
|
||||
navigation: {
|
||||
margin: '0 16px',
|
||||
|
||||
'& > span:nth-child(1)': {
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
marginTop: '16px',
|
||||
},
|
||||
|
||||
'& $navigationChapters': {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gridTemplateAreas: '"prev next"',
|
||||
gridColumnGap: '5px',
|
||||
margin: '10px 0',
|
||||
|
||||
'& a': {
|
||||
flexGrow: 1,
|
||||
textDecoration: 'none',
|
||||
|
||||
'& button': {
|
||||
width: '100%',
|
||||
padding: '5px 8px',
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
navigationChapters: {}, // dummy rule
|
||||
|
||||
settingsCollapsseHeader: {
|
||||
'& span': {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
|
||||
openDrawerButton: {
|
||||
position: 'fixed',
|
||||
top: 0 + 20,
|
||||
left: 10 + 20,
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
borderRadius: 5,
|
||||
backgroundColor: 'black',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: 'black',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export interface IReaderSettings{
|
||||
staticNav: boolean
|
||||
showPageNumber: boolean
|
||||
continuesPageGap: boolean
|
||||
}
|
||||
|
||||
export const defaultReaderSettings = () => ({
|
||||
staticNav: false,
|
||||
showPageNumber: true,
|
||||
continuesPageGap: false,
|
||||
} as IReaderSettings);
|
||||
|
||||
interface IProps {
|
||||
settings: IReaderSettings
|
||||
setSettings: React.Dispatch<React.SetStateAction<IReaderSettings>>
|
||||
manga: IManga | IMangaCard
|
||||
chapter: IChapter | IPartialChpter
|
||||
curPage: number
|
||||
}
|
||||
|
||||
export default function ReaderNavBar(props: IProps) {
|
||||
const { title } = useContext(NavBarContext);
|
||||
const { darkTheme } = useContext(DarkTheme);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
settings, setSettings, manga, chapter, curPage,
|
||||
} = props;
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useState(false || settings.staticNav);
|
||||
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
|
||||
const [hideOpenButton, setHideOpenButton] = useState(false);
|
||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
const setSettingValue = (key: string, value: any) => setSettings({ ...settings, [key]: value });
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentScrollPos = window.pageYOffset;
|
||||
|
||||
if (Math.abs(currentScrollPos - prevScrollPos) > 20) {
|
||||
setHideOpenButton(currentScrollPos > prevScrollPos);
|
||||
setPrevScrollPos(currentScrollPos);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
const rootEl = document.querySelector('#root')!;
|
||||
const mainContainer = document.querySelector('#appMainContainer')!;
|
||||
|
||||
rootEl.classList.add(classes.AppRootElment);
|
||||
mainContainer.classList.add(classes.AppMainContainer);
|
||||
|
||||
return () => {
|
||||
rootEl.classList.remove(classes.AppRootElment);
|
||||
mainContainer.classList.remove(classes.AppMainContainer);
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [handleScroll]);// handleScroll changes on every render
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClickAwayListener onClickAway={() => (drawerVisible && setDrawerOpen(false))}>
|
||||
<Slide
|
||||
direction="right"
|
||||
in={drawerOpen}
|
||||
timeout={200}
|
||||
appear={false}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
onEntered={() => setDrawerVisible(true)}
|
||||
onExited={() => setDrawerVisible(false)}
|
||||
>
|
||||
<div className={classes.root}>
|
||||
<header>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
onClick={() => history.push(`/manga/${manga.id}`)}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h1">
|
||||
{title}
|
||||
</Typography>
|
||||
{!settings.staticNav
|
||||
&& (
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
>
|
||||
<KeyboardArrowLeftIcon />
|
||||
</IconButton>
|
||||
) }
|
||||
</header>
|
||||
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
|
||||
<ListItemText primary="Reader Settings" />
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
||||
>
|
||||
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
||||
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary="Static Navigation" />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.staticNav}
|
||||
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Show page number" />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.showPageNumber}
|
||||
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Continues Page gap" />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.continuesPageGap}
|
||||
onChange={(e) => setSettingValue('continuesPageGap', e.target.checked)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Collapse>
|
||||
<hr />
|
||||
<div className={classes.navigation}>
|
||||
<span>
|
||||
Currently on page
|
||||
{' '}
|
||||
{curPage + 1}
|
||||
{' '}
|
||||
of
|
||||
{' '}
|
||||
{chapter.pageCount}
|
||||
</span>
|
||||
<div className={classes.navigationChapters}>
|
||||
{chapter.chapterIndex > 1
|
||||
&& (
|
||||
<Link
|
||||
style={{ gridArea: 'prev' }}
|
||||
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex - 1}`}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<KeyboardArrowLeftIcon />}
|
||||
>
|
||||
Chapter
|
||||
{' '}
|
||||
{chapter.chapterIndex - 1}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{chapter.chapterIndex < chapter.chapterCount
|
||||
&& (
|
||||
<Link
|
||||
style={{ gridArea: 'next' }}
|
||||
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex + 1}`}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
endIcon={<KeyboardArrowRightIcon />}
|
||||
>
|
||||
Chapter
|
||||
{' '}
|
||||
{chapter.chapterIndex + 1}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Slide>
|
||||
</ClickAwayListener>
|
||||
<Zoom in={!drawerOpen}>
|
||||
<Fade in={!hideOpenButton}>
|
||||
<IconButton
|
||||
className={classes.openDrawerButton}
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
</Fade>
|
||||
</Zoom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -27,68 +30,54 @@ interface IProps {
|
||||
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const sideList = (side: 'left') => (
|
||||
<div
|
||||
className={classes.list}
|
||||
role="presentation"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
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>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Extensions" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Sources">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<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> */}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
BackdropProps={{ invisible: true }}
|
||||
open={drawerOpen}
|
||||
anchor="left"
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
{sideList('left')}
|
||||
<div
|
||||
className={classes.list}
|
||||
role="presentation"
|
||||
onClick={() => setDrawerOpen(false)}
|
||||
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>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Extensions" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Sources">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="settings">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
</List>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -9,6 +12,8 @@ type ContextType = {
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>
|
||||
action: any
|
||||
setAction: React.Dispatch<React.SetStateAction<any>>
|
||||
override: INavbarOverride
|
||||
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
|
||||
};
|
||||
|
||||
const NavBarContext = React.createContext<ContextType>({
|
||||
@@ -16,6 +21,8 @@ const NavBarContext = React.createContext<ContextType>({
|
||||
setTitle: ():void => {},
|
||||
action: <div />,
|
||||
setAction: ():void => {},
|
||||
override: { status: false, value: <div /> },
|
||||
setOverride: ():void => {},
|
||||
});
|
||||
|
||||
export default NavBarContext;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
Vendored
+4
-1
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -35,9 +38,13 @@ function groupExtensions(extensions: IExtension[]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function extensionDefaultLangs() {
|
||||
return [...defualtLangs(), 'all'];
|
||||
}
|
||||
|
||||
export default function Extensions() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Extensions');
|
||||
@@ -76,7 +83,7 @@ export default function Extensions() {
|
||||
<>
|
||||
{
|
||||
Object.entries(extensions).map(([lang, list]) => (
|
||||
(['installed', ...shownLangs].indexOf(lang) !== -1
|
||||
((['installed', ...shownLangs].indexOf(lang) !== -1 && (list as []).length > 0)
|
||||
&& (
|
||||
<React.Fragment key={lang}>
|
||||
<h1 key={lang} style={{ marginLeft: 25 }}>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,17 +1,51 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import ChapterCard from '../components/ChapterCard';
|
||||
import MangaDetails from '../components/MangaDetails';
|
||||
import NavbarContext from '../context/NavbarContext';
|
||||
import client from '../util/client';
|
||||
import LoadingPlaceholder from '../components/LoadingPlaceholder';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
|
||||
chapters: {
|
||||
listStyle: 'none',
|
||||
padding: 0,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
width: '50vw',
|
||||
height: 'calc(100vh - 64px)',
|
||||
overflowY: 'auto',
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
|
||||
loading: {
|
||||
margin: '10px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function Manga() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
|
||||
const classes = useStyles();
|
||||
|
||||
const { setTitle } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
|
||||
|
||||
const { id } = useParams<{id: string}>();
|
||||
|
||||
@@ -33,16 +67,25 @@ export default function Manga() {
|
||||
.then((data) => setChapters(data));
|
||||
}, []);
|
||||
|
||||
const chapterCards = chapters.map((chapter) => (
|
||||
<ol style={{ listStyle: 'none', padding: 0 }}>
|
||||
<ChapterCard chapter={chapter} />
|
||||
</ol>
|
||||
));
|
||||
const chapterCards = (
|
||||
<LoadingPlaceholder
|
||||
shouldRender={chapters.length > 0}
|
||||
>
|
||||
<ol className={classes.chapters}>
|
||||
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
|
||||
</ol>
|
||||
</LoadingPlaceholder>
|
||||
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{manga && <MangaDetails manga={manga} />}
|
||||
<div className={classes.root}>
|
||||
<LoadingPlaceholder
|
||||
shouldRender={manga !== undefined}
|
||||
component={MangaDetails}
|
||||
componentProps={{ manga }}
|
||||
/>
|
||||
{chapterCards}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,121 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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 CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Page from '../components/Page';
|
||||
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
|
||||
import NavbarContext from '../context/NavbarContext';
|
||||
import client from '../util/client';
|
||||
import useLocalStorage from '../util/useLocalStorage';
|
||||
|
||||
const style = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
backgroundColor: '#343a40',
|
||||
} as React.CSSProperties;
|
||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||
reader: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto',
|
||||
},
|
||||
|
||||
loading: {
|
||||
margin: '50px auto',
|
||||
},
|
||||
|
||||
pageNumber: {
|
||||
display: settings.showPageNumber ? 'block' : 'none',
|
||||
position: 'fixed',
|
||||
bottom: '50px',
|
||||
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||
width: '50px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
borderRadius: '10px',
|
||||
},
|
||||
});
|
||||
|
||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||
const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 });
|
||||
|
||||
export default function Reader() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
|
||||
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
|
||||
|
||||
const classes = useStyles(settings)();
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const [pageCount, setPageCount] = useState<number>(-1);
|
||||
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||
const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>();
|
||||
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
||||
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
|
||||
const [curPage, setCurPage] = useState<number>(0);
|
||||
|
||||
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
setOverride(
|
||||
{
|
||||
status: true,
|
||||
value: (
|
||||
<ReaderNavBar
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
manga={manga}
|
||||
chapter={chapter}
|
||||
curPage={curPage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
// clean up for when we leave the reader
|
||||
return () => setOverride({ status: false, value: <div /> });
|
||||
}, [manga, chapter, settings, curPage, chapterIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||
setTitle('Reader');
|
||||
client.get(`/api/v1/manga/${mangaId}/`)
|
||||
.then((response) => response.data)
|
||||
.then((data: IManga) => {
|
||||
setManga(data);
|
||||
setTitle(data.title);
|
||||
});
|
||||
}, [chapterIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setChapter(initialChapter);
|
||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||
.then((response) => response.data)
|
||||
.then((data:IChapter) => {
|
||||
setTitle(data.name);
|
||||
setPageCount(data.pageCount);
|
||||
setChapter(data);
|
||||
});
|
||||
}, []);
|
||||
}, [chapterIndex]);
|
||||
|
||||
if (pageCount === -1) {
|
||||
if (chapter.pageCount === -1) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<h3>wait</h3>
|
||||
<div className={classes.loading}>
|
||||
<CircularProgress thickness={5} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapped = range(pageCount).map((index) => (
|
||||
<div style={{ margin: '0 auto' }}>
|
||||
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div style={style}>
|
||||
{mapped}
|
||||
<div className={classes.reader}>
|
||||
<div className={classes.pageNumber}>
|
||||
{`${curPage + 1} / ${chapter.pageCount}`}
|
||||
</div>
|
||||
{range(chapter.pageCount).map((index) => (
|
||||
<Page
|
||||
key={index}
|
||||
index={index}
|
||||
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`}
|
||||
setCurPage={setCurPage}
|
||||
settings={settings}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -27,7 +30,7 @@ export default function SearchSingle() {
|
||||
const { sourceId } = useParams<{sourceId: string}>();
|
||||
const classes = useStyles();
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||
@@ -50,7 +53,8 @@ export default function SearchSingle() {
|
||||
} else {
|
||||
setError(false);
|
||||
setSearchTerm(value);
|
||||
setMessage('');
|
||||
setMangas([]);
|
||||
setMessage('loading...');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +64,7 @@ export default function SearchSingle() {
|
||||
client.get(`/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
|
||||
.then((response) => response.data)
|
||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||
setMessage('');
|
||||
if (data.mangaList.length > 0) {
|
||||
setMangas([
|
||||
...mangas,
|
||||
@@ -86,12 +91,18 @@ export default function SearchSingle() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<form className={classes.root} noValidate autoComplete="off">
|
||||
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
|
||||
<div className={classes.root}>
|
||||
<TextField
|
||||
inputRef={textInput}
|
||||
error={error}
|
||||
id="standard-basic"
|
||||
label="Search text.."
|
||||
onKeyDown={(e) => e.key === 'Enter' && processInput()}
|
||||
/>
|
||||
<Button variant="contained" color="primary" onClick={() => processInput()}>
|
||||
Search
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
{mangaGrid}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
@@ -13,7 +16,7 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
||||
|
||||
const { sourceId } = useParams<{sourceId: string}>();
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* 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/. */
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user