Compare commits

..

56 Commits

Author SHA1 Message Date
Aria Moradi e6ba2a0066 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 13:53:03 +04:30
Aria Moradi 9b56ef7d82 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-28 13:50:33 +04:30
Aria Moradi dd442c6653 fix extensions not being installed correctly 2021-03-28 13:40:18 +04:30
Aria Moradi 3044317b09 Update README.md 2021-03-28 06:00:34 +04:30
Aria Moradi 3a1e1e01dc continues gap switch
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 04:03:46 +04:30
Aria Moradi a567701639 chapter image fix for bato.to 2021-03-28 03:43:52 +04:30
Aria Moradi 1802271358 fix chapters not being loaded correctly 2021-03-28 03:19:01 +04:30
Aria Moradi 9e649eef79 fix a bug where add to library didn't work 2021-03-28 02:43:09 +04:30
Aria Moradi 1eb4a9c216 use logger to print exception 2021-03-28 02:11:40 +04:30
Aria Moradi e3f65d2192 fix URI exception 2021-03-28 01:35:46 +04:30
Aria Moradi bb09cfddb3 Migrate from Oracle Nashorn to Mozilla's Rhino 2021-03-28 01:15:50 +04:30
Aria Moradi d383939c9f improve loading spinner's place 2021-03-28 01:12:32 +04:30
Aria Moradi 32dd543562 Loading placeholder & css fixes 2021-03-27 22:21:39 +04:30
Aria Moradi 5a75f26791 even better logging messages 2021-03-27 19:40:47 +04:30
Aria Moradi 95c437efd5 better logging 2021-03-27 18:53:29 +04:30
Aria Moradi ec877f632f refactor & config improvments 2021-03-27 18:30:36 +04:30
Aria Moradi 8666cbf8bc lint some files 2021-03-26 12:15:14 +04:30
Aria Moradi 84b0c26450 change the copyright owner to Contributors to the Suwayomi project 2021-03-26 04:17:02 +04:30
Aria Moradi 64e5bbabb3 Update enters 2021-03-26 02:40:20 +04:30
Aria Moradi cc1a15e5ba Update enters 2021-03-26 02:39:54 +04:30
Aria Moradi d29e942a72 Update getAndroid.sh 2021-03-26 02:33:24 +04:30
Aria Moradi 8d86c88c38 Update getAndroid.ps1 2021-03-26 02:33:03 +04:30
Aria Moradi c7dc7421aa Update getAndroid.sh 2021-03-26 02:32:51 +04:30
Aria Moradi 34ed3e5c68 Update getAndroid.ps1 2021-03-26 02:29:31 +04:30
Aria Moradi 1a4a8af384 Merge pull request #42 from Syer10/getandroid
getAndroid.ps1: Silently continue if it cant remove the tmp folder
2021-03-26 02:21:43 +04:30
Aria Moradi 62b1e99bbf get the right dex2jar package please 2021-03-26 02:07:00 +04:30
Syer10 1aa3b76934 getAndroid.ps1: Silently continue if it cant remove the tmp folder 2021-03-25 16:47:30 -04:00
Aria Moradi 3e53c50f64 better spelling 2021-03-25 18:32:31 +04:30
Aria Moradi 430386bc84 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-25 18:21:57 +04:30
Aria Moradi 30049e8152 Update README.md 2021-03-25 18:21:14 +04:30
Aria Moradi 34d9a7a233 better use of dex2jar, get dex2jar from jitpack 2021-03-25 18:03:32 +04:30
Aria Moradi 183972475b better loging 2021-03-25 16:53:36 +04:30
Aria Moradi fd46727f8e Merge pull request #40 from Syer10/Powerhsell
Create getAndroid.ps1
2021-03-25 05:07:03 +04:30
Syer10 f6ce010aa2 Update readme 2021-03-24 20:34:32 -04:00
Syer10 d0ff30df9f Create getAndroid.ps1 2021-03-24 20:28:34 -04:00
Aria Moradi 8e449abd67 Merge pull request #39 from Syer10/LF
Standardize Line endings
2021-03-25 04:53:22 +04:30
Syer10 2986130268 Standardize Line endings 2021-03-24 19:51:18 -04:00
Aria Moradi 1c0c09f2f2 add server Desktop Environment 2021-03-25 04:03:48 +04:30
Aria Moradi 44100cb5b6 closes #37 2021-03-25 01:03:51 +04:30
Aria Moradi cfc6e5cd2a Fix for jip's Arch Linux 2021-03-24 19:28:27 +04:30
Aria Moradi c067d14c2c Update README.md 2021-03-24 19:26:37 +04:30
Aria Moradi aded854a2b Update README.md 2021-03-24 19:24:50 +04:30
Aria Moradi e79d0b9dd2 search loading message 2021-03-24 03:31:38 +04:30
Aria Moradi a0115d88b0 fix search bugs 2021-03-24 03:28:02 +04:30
Aria Moradi 85ec2ed367 drawer hide on click outside of it 2021-03-23 04:28:23 +04:30
Aria Moradi bf908c4d17 chapter prev/next UI+Backend 2021-03-23 03:50:55 +04:30
Aria Moradi f41c5c9428 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-19 14:55:48 +03:30
Aria Moradi 04837983fa reader ui changes 2021-03-19 14:52:20 +03:30
Aria Moradi 5d484b012c new layout for manga page for >md 2021-03-18 22:55:17 +03:30
Aria Moradi 436a8d0585 improvments on the reader 2021-03-18 21:46:24 +03:30
Aria Moradi 28cc0a6f84 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-17 19:22:44 +03:30
Aria Moradi 26cc2f2c96 MangaDetails component improved drastically 2021-03-17 19:17:03 +03:30
Aria Moradi 149107e749 fix material error 2021-03-16 23:42:51 +03:30
Aria Moradi a74936c5f5 Update README.md 2021-03-16 23:24:14 +03:30
Aria Moradi ff8c8913d4 Update README.md 2021-03-16 23:14:36 +03:30
Aria Moradi 83426e1302 Update README.md 2021-03-16 23:13:44 +03:30
112 changed files with 2641 additions and 1050 deletions
+26 -5
View File
@@ -1,6 +1,27 @@
# * text=auto
# https://help.github.com/articles/dealing-with-line-endings/ * text eol=lf
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
# 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
+1
View File
@@ -25,6 +25,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
## Device information ## Device information
- Tachidesk version: (Example: v0.2.3-r255-win32) - Tachidesk version: (Example: v0.2.3-r255-win32)
- Server Operating System: (Example: Ubuntu 20.04) - 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) - 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 Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82) - Client Web Browser: (Example: Google Chrome 89.0.4389.82)
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
}, },
{ {
"type": "body", "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" "message": "The requested information was not filled out"
}, },
{ {
@@ -1,5 +1,12 @@
package xyz.nulldev.ts.config 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.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
+8 -6
View File
@@ -18,9 +18,7 @@ repositories {
dependencies { dependencies {
// Android stub library // Android stub library
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
implementation(fileTree("lib/")) implementation(fileTree("lib/"))
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
// Android JAR libs // Android JAR libs
@@ -41,10 +39,10 @@ dependencies {
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1") compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
// Config API // Config API
implementation( project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// dex2jar // dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
// compileOnly( "dex2jar:dex-translator") compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser // APK parser
compileOnly("net.dongliu:apk-parser:2.6.10") compileOnly("net.dongliu:apk-parser:2.6.10")
@@ -55,7 +53,11 @@ dependencies {
// AndroidX annotations // AndroidX annotations
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01") 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') //def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
+99
View File
@@ -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!"
+9 -1
View File
@@ -1,5 +1,13 @@
#!/usr/bin/env bash #!/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 # foolproof against running from AndroidCompat dir instead of running from project root
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
cd .. cd ..
@@ -13,7 +21,7 @@ pushd "tmp"
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar 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 "Patching JAR..."
echo "Removing org.json..." 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; 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 kotlin.NotImplementedError;
import javax.script.ScriptEngine; import javax.script.ScriptEngine;
@@ -22,11 +14,18 @@ import javax.script.ScriptEngineManager;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.io.Closeable; 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 { public final class Duktape implements Closeable, AutoCloseable {
private ScriptEngineManager factory = new ScriptEngineManager(); 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 * 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() {} 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 * 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. * 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!"); throw new NotImplementedError("Not implemented!");
} }
/** // /**
* Attaches to a global JavaScript object called {@code name} that implements {@code type}. // * 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} 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 // * {@code type} must be an interface that does not extend any other interfaces, and cannot define
* any overloaded methods. // * any overloaded methods.
* <p>Methods of the interface may return {@code void} or any of the following supported argument // * <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}, // * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
* {@link Double}, {@link String}. // * {@link Double}, {@link String}.
*/ // */
public synchronized <T> T get(final String name, final Class<T> type) { // public synchronized <T> T get(final String name, final Class<T> type) {
throw new NotImplementedError("Not implemented!"); // throw new NotImplementedError("Not implemented!");
} // }
/** /**
* Release the native resources associated with this object. You <strong>must</strong> call this * 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!");
}
}
+32 -16
View File
@@ -1,5 +1,5 @@
![image](https://github.com/AriaMoradi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png) ![image](https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png)
# Tachidesk # Tachidesk
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/). A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
@@ -13,26 +13,33 @@ Here is a list of current features:
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources. - Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
- A library to save your mangas and categories to put them into. - A library to save your mangas and categories to put them into.
- Searching and browsing installed sources. - Searching and browsing installed sources.
- A minimal chapter reader. - A decent chapter reader.
- Ability to download Mangas for offline read(This partially works) - Ability to download Mangas for offline read(This partially works)
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens. **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 ## Downloading and Running the app
### Downloading the app
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
### All Operating Systems ### 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. 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 latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
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. 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.
### Windows only ### Windows
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`. 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. Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting ## General troubleshooting
@@ -55,18 +62,25 @@ This project has two components:
2. **webUI:** A react SPA project that works with the server to do the presentation. 2. **webUI:** A react SPA project that works with the server to do the presentation.
## Building from source ## Building from source
### Get Android stubs jar ### Prerequisite: Get Android stubs jar
#### Manual download #### Manual download
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`. Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work) #### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
Run `AndroidCompat/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository. 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.
### building the jar ### Prerequisite: Software dependencies
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`. 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 ### building the Windows package
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`. Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running for development purposes ## Running for development purposes
### `server` module ### `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 ### `webUI` module
How to do it is described in `webUI/react/README.md` but for short, How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once) first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
@@ -75,6 +89,8 @@ 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. and supports HMR and all the other goodies you'll need.
## Credit ## Credit
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`. 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`. Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
@@ -85,7 +101,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
## License ## 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 This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this License, v. 2.0. If a copy of the MPL was not distributed with this
+3 -1
View File
@@ -61,7 +61,7 @@ configure(listOf(
// Logging // Logging
implementation("org.slf4j:slf4j-api:1.7.30") 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") implementation("io.github.microutils:kotlin-logging:2.0.3")
// RxJava // RxJava
@@ -76,6 +76,8 @@ configure(listOf(
// dependency of :AndroidCompat:Config // dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0") implementation("com.typesafe:config:1.4.0")
implementation("io.github.config4k:config4k:0.4.2")
// to get application content root // to get application content root
implementation("net.harawata:appdirs:1.2.0") implementation("net.harawata:appdirs:1.2.0")
+5 -8
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9" id("edu.sc.seis.launch4j") version "2.4.9"
} }
val TachideskVersion = "v0.2.5" val TachideskVersion = "v0.2.7"
repositories { repositories {
@@ -57,19 +57,17 @@ dependencies {
implementation("org.jsoup:jsoup:1.13.1") implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0") implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("com.squareup.duktape:duktape-android:1.3.0")
val coroutinesVersion = "1.3.9" val coroutinesVersion = "1.3.9"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon // dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation(fileTree("lib/dex2jar/")) implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// api // api
implementation("io.javalin:javalin:3.12.0") 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") implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// Exposed ORM // Exposed ORM
@@ -87,9 +85,8 @@ dependencies {
implementation(project(":AndroidCompat")) implementation(project(":AndroidCompat"))
implementation(project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// uncomment to test extensions directly
// testImplementation("org.jetbrains.kotlin:kotlin-test") // implementation(fileTree("lib/"))
// testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
} }
val name = "ir.armor.tachidesk.Main" 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.
@@ -1,67 +0,0 @@
==== dx-*.jar
Apache 2.0 http://www.apache.org/licenses/LICENSE-2.0.html
==== antlr-*.jar
[The BSD License]
Copyright (c) 2003-2007, Terence Parr
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of the author nor the names of its contributors
may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
==== asm-*.jar
ASM: a very small and fast Java bytecode manipulation framework
Copyright (c) 2000-2005 INRIA, France Telecom
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holders nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,252 +0,0 @@
package ir.armor.tachidesk;
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
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
// such as for AndroidManifest.xml in .apk files
public static int endDocTag = 0x00100101;
public static int startTag = 0x00100102;
public static int endTag = 0x00100103;
static void prt(String str) {
//System.err.print(str);
}
public static String decompressXML(byte[] xml) {
StringBuilder finalXML = new StringBuilder();
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminently display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
int numbStrings = LEW(xml, 4 * 4);
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
int sitOff = 0x24; // Offset of start of StringIndexTable
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode)
// chars.
int stOff = sitOff + numbStrings * 4; // StringTable follows
// StrIndexTable
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan
// forward from this point to the flag for the start of an XML start
// tag.
int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
// word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// int)
for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
if (LEW(xml, ii) == startTag) {
xmlTagOff = ii;
break;
}
} // end of hack, scanning for start of first start tag
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for startTag and 03011000 for endTag
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value
// TMP, dump string table to tr for debugging
// tr.addSelect("strings", null);
// for (int ii=0; ii<numbStrings; ii++) {
// // Length of string starts at StringTable plus offset in StrIndTable
// String str = compXmlString(xml, sitOff, stOff, ii);
// tr.add(String.valueOf(ii), str);
// }
// tr.parent();
// Step through the XML tree element tags and attributes
int off = xmlTagOff;
int indent = 0;
int startTagLineNo = -2;
while (off < xml.length) {
int tag0 = LEW(xml, off);
// int tag1 = LEW(xml, off+1*4);
int lineNo = LEW(xml, off + 2 * 4);
// int tag3 = LEW(xml, off+3*4);
int nameNsSi = LEW(xml, off + 4 * 4);
int nameSi = LEW(xml, off + 5 * 4);
if (tag0 == startTag) { // XML START TAG
int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
// to follow
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
off += 9 * 4; // Skip over 6+3 words of startTag data
String name = compXmlString(xml, sitOff, stOff, nameSi);
// tr.addSelect(name, null);
startTagLineNo = lineNo;
// Look for the Attributes
StringBuffer sb = new StringBuffer();
for (int ii = 0; ii < numbAttrs; ii++) {
int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
// Ind, or FFFFFFFF
int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
// Index
int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
// Ind, or
// FFFFFFFF
int attrFlags = LEW(xml, off + 3 * 4);
int attrResId = LEW(xml, off + 4 * 4); // AttrValue
// ResourceId or dup
// AttrValue StrInd
off += 5 * 4; // Skip over the 5 words of an attribute
String attrName = compXmlString(xml, sitOff, stOff,
attrNameSi);
String attrValue = attrValueSi != -1 ? compXmlString(xml,
sitOff, stOff, attrValueSi) : "resourceID 0x"
+ Integer.toHexString(attrResId);
sb.append(" " + attrName + "=\"" + attrValue + "\"");
// tr.add(attrName, attrValue);
}
finalXML.append("<" + name + sb + ">");
prtIndent(indent, "<" + name + sb + ">");
indent++;
} else if (tag0 == endTag) { // XML END TAG
indent--;
off += 6 * 4; // Skip over 6 words of endTag data
String name = compXmlString(xml, sitOff, stOff, nameSi);
finalXML.append("</" + name + ">");
prtIndent(indent, "</" + name + "> (line " + startTagLineNo
+ "-" + lineNo + ")");
// tr.parent(); // Step back up the NobTree
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
break;
} else {
prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
+ "' at offset " + off);
break;
}
} // end of while loop scanning tags and attributes of XML tree
//prt(" end at offset " + off);
return finalXML.toString();
} // end of decompressXML
public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
if (strInd < 0)
return null;
int strOff = stOff + LEW(xml, sitOff + strInd * 4);
return compXmlStringAt(xml, strOff);
}
public static String spaces = " ";
public static void prtIndent(int indent, String str) {
prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
}
// compXmlStringAt -- Return the string stored in StringTable format at
// offset strOff. This offset points to the 16 bit string length, which
// is followed by that number of 16 bit (Unicode) chars.
public static String compXmlStringAt(byte[] arr, int strOff) {
int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
byte[] chars = new byte[strLen];
for (int ii = 0; ii < strLen; ii++) {
chars[ii] = arr[strOff + 2 + ii * 2];
}
return new String(chars); // Hack, just use 8 byte chars
} // end of compXmlStringAt
// LEW -- Return value of a Little Endian 32 bit word from the byte array
// at offset off.
public static int LEW(byte[] arr, int off) {
return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
| arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
} // end of LEW
public static Document loadXMLFromString(String xml) throws Exception {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
return docBuilder.parse(new InputSource(new StringReader(xml)));
}
public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
ZipFile zip = null;
zip = new ZipFile(filePath);
ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
ZipEntry classesDex = zip.getEntry("classes.dex");
// write dex file
InputStream dexStream = zip.getInputStream(classesDex);
try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
byte[] buffer = new byte[1024];
int len;
while ((len = dexStream.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
}
// read xml file
InputStream is = zip.getInputStream(androidManifest);
byte[] buf = new byte[1024000]; // 100 kb
is.read(buf);
is.close();
zip.close();
String xml = APKExtractor.decompressXML(buf);
try {
Document xmlDoc = loadXMLFromString(xml);
String pkg = xmlDoc.getDocumentElement().getAttribute("package");
NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
for (int i = 0; i < nodes.getLength(); i++) {
NamedNodeMap attributes = nodes.item(i).getAttributes();
System.out.println(attributes.getNamedItem("name").getNodeValue());
if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
return pkg + attributes.getNamedItem("value").getNodeValue();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi 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.app.Application
import android.content.Context import android.content.Context
// import android.content.res.Configuration // import android.content.res.Configuration
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi 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.app.Application
import com.google.gson.Gson import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.cache.ChapterCache // import eu.kanade.tachiyomi.data.cache.ChapterCache
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.extension.api 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 android.content.Context
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.extension.util 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.annotation.SuppressLint
// import android.content.Context // import android.content.Context
// import android.content.pm.PackageInfo // import android.content.pm.PackageInfo
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network 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.annotation.SuppressLint
// import android.content.Context // import android.content.Context
// import android.os.Build // import android.os.Build
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network 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.Cookie
import okhttp3.CookieJar import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network 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 android.content.Context
// import eu.kanade.tachiyomi.BuildConfig // import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper // import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -1,260 +1,22 @@
package ir.armor.tachidesk 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin import ir.armor.tachidesk.server.applicationSetup
import ir.armor.tachidesk.util.addMangaToCategory import ir.armor.tachidesk.server.javalinSetup
import ir.armor.tachidesk.util.addMangaToLibrary
import ir.armor.tachidesk.util.createCategory
import ir.armor.tachidesk.util.getCategoryList
import ir.armor.tachidesk.util.getCategoryMangaList
import ir.armor.tachidesk.util.getChapter
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionIcon
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getLibraryMangas
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaCategories
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.openInBrowser
import ir.armor.tachidesk.util.removeCategory
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.removeMangaFromCategory
import ir.armor.tachidesk.util.removeMangaFromLibrary
import ir.armor.tachidesk.util.reorderCategory
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.updateCategory
class Main { class Main {
companion object { companion object {
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
serverSetup() applicationSetup()
javalinSetup()
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.exception(NullPointerException::class.java) { _, ctx ->
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")
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)
}
// 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/: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))
}
} }
} }
} }
@@ -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 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 * 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/. */ * 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.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.ChapterTable 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.MangaTable
import ir.armor.tachidesk.database.table.PageTable import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -12,5 +15,7 @@ data class ChapterDataClass(
val chapter_number: Float, val chapter_number: Float,
val scanlator: String?, val scanlator: String?,
val mangaId: Int, val mangaId: Int,
val chapterIndex: Int,
val chapterCount: Int,
val pageCount: Int? = null, val pageCount: Int? = null,
) )
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,11 +1,14 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable /*
* Copyright (C) Contributors to the Suwayomi project
/* This Source Code Form is subject to the terms of the Mozilla Public *
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object CategoryMangaTable : IntIdTable() { object CategoryMangaTable : IntIdTable() {
val category = reference("category", CategoryTable) val category = reference("category", CategoryTable)
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * 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 chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable() val scanlator = varchar("scanlator", 128).nullable()
val chapterIndex = integer("number_in_list")
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,12 +1,15 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.database.dataclass.MangaDataClass 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.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -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.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable 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.transactions.transaction
import org.jetbrains.exposed.sql.update 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) { fun createCategory(name: String) {
transaction { transaction {
val count = CategoryTable.selectAll().count() val count = CategoryTable.selectAll().count()
@@ -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.CategoryDataClass
import ir.armor.tachidesk.database.dataclass.MangaDataClass 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.transactions.transaction
import org.jetbrains.exposed.sql.update 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) { fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction { transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) { if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -14,7 +17,9 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
fun getChapterList(mangaId: Int): List<ChapterDataClass> { fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) val mangaDetails = getManga(mangaId)
@@ -27,8 +32,10 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
} }
).toBlocking().first() ).toBlocking().first()
val chapterCount = chapterList.count()
return transaction { return transaction {
chapterList.forEach { fetchedChapter -> chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) { if (chapterEntry == null) {
ChapterTable.insertAndGetId { ChapterTable.insertAndGetId {
@@ -38,12 +45,29 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it[chapter_number] = fetchedChapter.chapter_number it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator 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 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( ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value, ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url, it.url,
@@ -51,16 +75,19 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it.date_upload, it.date_upload,
it.chapter_number, it.chapter_number,
it.scanlator, it.scanlator,
mangaId mangaId,
chapterCount - index,
chapterCount
) )
} }
} }
} }
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass { fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
return transaction { return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! val chapterEntry = ChapterTable.select {
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
@@ -71,14 +98,20 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
} }
).toBlocking().first() ).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass( val chapter = ChapterDataClass(
chapterEntry[ChapterTable.id].value, chapterId,
chapterEntry[ChapterTable.url], chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name], chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload], chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number], chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator], chapterEntry[ChapterTable.scanlator],
mangaId, mangaId,
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count() pageList.count()
) )
@@ -93,6 +126,13 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
it[this.chapter] = chapterId 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
}
}
} }
} }
@@ -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 * 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/. */ * 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.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.APKExtractor
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ExtensionTable import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable 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 kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
import okio.sink import okio.sink
@@ -28,8 +34,45 @@ import java.io.File
import java.io.InputStream import java.io.InputStream
import java.net.URL import java.net.URL
import java.net.URLClassLoader 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 { fun installAPK(apkName: String): Int {
logger.debug("Installing $apkName")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName } val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk") val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
@@ -49,9 +92,9 @@ fun installAPK(apkName: String): Int {
downloadAPKFile(apkToDownload, apkFilePath) downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath) val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className) logger.debug(className)
// dex -> jar // dex -> jar
Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force") dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
// clean up // clean up
File(apkFilePath).delete() File(apkFilePath).delete()
@@ -69,11 +112,6 @@ fun installAPK(apkName: String): Int {
if (instance is HttpSource) { // single source if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource val httpSource = instance as HttpSource
transaction { transaction {
// SourceEntity.new {
// 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) { if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert { SourceTable.insert {
it[this.id] = httpSource.id it[this.id] = httpSource.id
@@ -82,9 +120,7 @@ fun installAPK(apkName: String): Int {
it[extension] = extensionId it[extension] = extensionId
} }
} }
// println(httpSource.id) logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}")
// println(httpSource.name)
// println()
} }
} else { // multi source } else { // multi source
val sourceFactory = instance as SourceFactory val sourceFactory = instance as SourceFactory
@@ -101,9 +137,7 @@ fun installAPK(apkName: String): Int {
it[positionInFactorySource] = index it[positionInFactorySource] = index
} }
} }
// println(httpSource.id) logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
// println(httpSource.name)
// println()
} }
} }
} }
@@ -134,9 +168,11 @@ private fun downloadAPKFile(url: String, apkPath: String) {
sink.close() sink.close()
} }
fun removeExtension(pkgName: String) { fun removeExtension(apkName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName } logger.debug("Uninstalling $apkName")
val fileNameWithoutType = pkgName.substringBefore(".apk")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction { transaction {
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id] 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 iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon" val saveDir = "${applicationDirs.extensionsRoot}/icon"
val fileName = apkName
return getCachedResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, apkName) {
network.client.newCall( network.client.newCall(
GET(iconUrl) GET(iconUrl)
).execute() ).execute()
@@ -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 * 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/. */ * 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.dataclass.ExtensionDataClass
import ir.armor.tachidesk.database.table.ExtensionTable import ir.armor.tachidesk.database.table.ExtensionTable
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
private val logger = KotlinLogging.logger {}
private object Data { private object Data {
var lastExtensionCheck: Long = 0 var lastExtensionCheck: Long = 0
} }
@@ -28,7 +34,7 @@ private fun extensionDatabaseIsEmtpy(): Boolean {
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> { fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
// update if 60 seconds has passed or requested offline and database is empty // update if 60 seconds has passed or requested offline and database is empty
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) { 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() Data.lastExtensionCheck = System.currentTimeMillis()
var foundExtensions: List<Extension.Available> var foundExtensions: List<Extension.Available>
runBlocking { runBlocking {
@@ -66,7 +72,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
} }
} }
} else { } else {
println("used cached extension list") logger.debug("used cached extension list")
} }
return transaction { return transaction {
@@ -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.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass 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.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
/* 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) { fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (!manga.inLibrary) { if (!manga.inLibrary) {
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
@@ -90,7 +93,7 @@ fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
return getCachedResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference] val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource 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.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -25,10 +28,16 @@ fun getTrueImageUrl(page: Page, source: HttpSource): String {
return page.imageUrl!! 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 mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! } 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 pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page( val tachiPage = Page(
@@ -49,7 +58,7 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
File(saveDir).mkdirs() File(saveDir).mkdirs()
val fileName = index.toString() val fileName = index.toString()
return getCachedResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first() source.fetchImage(tachiPage).toBlocking().first()
} }
} }
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,17 +1,21 @@
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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.SourceDataClass import ir.armor.tachidesk.database.dataclass.SourceDataClass
import ir.armor.tachidesk.database.entity.ExtensionEntity import ir.armor.tachidesk.database.entity.ExtensionEntity
import ir.armor.tachidesk.database.entity.SourceEntity import ir.armor.tachidesk.database.entity.SourceEntity
import ir.armor.tachidesk.database.table.ExtensionTable import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable 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.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
@@ -19,6 +23,8 @@ import java.lang.NullPointerException
import java.net.URL import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
private val logger = KotlinLogging.logger {}
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>() private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>() private val extensionCache = mutableListOf<Pair<String, Any>>()
@@ -29,7 +35,7 @@ fun getHttpSource(sourceId: Long): HttpSource {
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId } val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
if (cachedResult != null) { if (cachedResult != null) {
println("used cached HttpSource: ${cachedResult.second.name}") logger.debug("used cached HttpSource: ${cachedResult.second.name}")
return cachedResult.second return cachedResult.second
} }
@@ -41,17 +47,15 @@ fun getHttpSource(sourceId: Long): HttpSource {
val jarName = apkName.substringBefore(".apk") + ".jar" val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName" val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
println(jarName)
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath } val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false var usedCached = false
val instance = val instance =
if (cachedExtensionPair != null) { if (cachedExtensionPair != null) {
usedCached = true usedCached = true
println("Used cached Extension") logger.debug("Used cached Extension")
cachedExtensionPair.second cachedExtensionPair.second
} else { } else {
println("No Extension cache") logger.debug("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader) val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child) val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance() classToLoad.newInstance()
@@ -0,0 +1,510 @@
package ir.armor.tachidesk.impl.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 org.w3c.dom.Document
import org.xml.sax.InputSource
import java.io.IOException
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Paths
import java.util.zip.ZipFile
import javax.xml.parsers.DocumentBuilderFactory
object APKExtractor {
// decompressXML -- Parse the 'compressed' binary form of Android XML docs
// such as for AndroidManifest.xml in .apk files
var endDocTag = 0x00100101
var startTag = 0x00100102
var endTag = 0x00100103
fun prt(str: String?) {
// System.err.print(str);
}
fun decompressXML(xml: ByteArray): String {
val finalXML = StringBuilder()
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminently display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
val numbStrings = LEW(xml, 4 * 4)
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
val sitOff = 0x24 // Offset of start of StringIndexTable
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode)
// chars.
val stOff = sitOff + numbStrings * 4 // StringTable follows
// StrIndexTable
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan
// forward from this point to the flag for the start of an XML start
// tag.
var xmlTagOff = LEW(xml, 3 * 4) // Start from the offset in the 3rd
// word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// int)
var ii = xmlTagOff
while (ii < xml.size - 4) {
if (LEW(xml, ii) == startTag) {
xmlTagOff = ii
break
}
ii += 4
}
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for startTag and 03011000 for endTag
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, endDocTag)
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value
// TMP, dump string table to tr for debugging
// tr.addSelect("strings", null);
// for (int ii=0; ii<numbStrings; ii++) {
// // Length of string starts at StringTable plus offset in StrIndTable
// String str = compXmlString(xml, sitOff, stOff, ii);
// tr.add(String.valueOf(ii), str);
// }
// tr.parent();
// Step through the XML tree element tags and attributes
var off = xmlTagOff
var indent = 0
var startTagLineNo = -2
while (off < xml.size) {
val tag0 = LEW(xml, off)
// int tag1 = LEW(xml, off+1*4);
val lineNo = LEW(xml, off + 2 * 4)
// int tag3 = LEW(xml, off+3*4);
val nameNsSi = LEW(xml, off + 4 * 4)
val nameSi = LEW(xml, off + 5 * 4)
if (tag0 == startTag) { // XML START TAG
val tag6 = LEW(xml, off + 6 * 4) // Expected to be 14001400
val numbAttrs = LEW(xml, off + 7 * 4) // Number of Attributes
// to follow
// int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
off += 9 * 4 // Skip over 6+3 words of startTag data
val name = compXmlString(xml, sitOff, stOff, nameSi)
// tr.addSelect(name, null);
startTagLineNo = lineNo
// Look for the Attributes
val sb = StringBuffer()
for (ii in 0 until numbAttrs) {
val attrNameNsSi = LEW(xml, off) // AttrName Namespace Str
// Ind, or FFFFFFFF
val attrNameSi = LEW(xml, off + 1 * 4) // AttrName String
// Index
val attrValueSi = LEW(xml, off + 2 * 4) // AttrValue Str
// Ind, or
// FFFFFFFF
val attrFlags = LEW(xml, off + 3 * 4)
val attrResId = LEW(xml, off + 4 * 4) // AttrValue
// ResourceId or dup
// AttrValue StrInd
off += 5 * 4 // Skip over the 5 words of an attribute
val attrName = compXmlString(
xml, sitOff, stOff,
attrNameSi
)
val attrValue = if (attrValueSi != -1) compXmlString(xml, sitOff, stOff, attrValueSi)
else "resourceID 0x ${Integer.toHexString(attrResId)}"
sb.append(" $attrName=\"$attrValue\"")
// tr.add(attrName, attrValue);
}
finalXML.append("<$name$sb>")
prtIndent(indent, "<$name$sb>")
indent++
} else if (tag0 == endTag) { // XML END TAG
indent--
off += 6 * 4 // Skip over 6 words of endTag data
val name = compXmlString(xml, sitOff, stOff, nameSi)
finalXML.append("</$name>")
prtIndent(
indent,
"</" + name + "> (line " + startTagLineNo +
"-" + lineNo + ")"
)
// tr.parent(); // Step back up the NobTree
} else if (tag0 == endDocTag) { // END OF XML DOC TAG
break
} else {
prt(
" Unrecognized tag code '" + Integer.toHexString(tag0) +
"' at offset " + off
)
break
}
} // end of while loop scanning tags and attributes of XML tree
// prt(" end at offset " + off);
return finalXML.toString()
} // end of decompressXML
fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? {
if (strInd < 0) return null
val strOff = stOff + LEW(xml, sitOff + strInd * 4)
return compXmlStringAt(xml, strOff)
}
var spaces = " "
fun prtIndent(indent: Int, str: String) {
prt(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str)
}
// compXmlStringAt -- Return the string stored in StringTable format at
// offset strOff. This offset points to the 16 bit string length, which
// is followed by that number of 16 bit (Unicode) chars.
fun compXmlStringAt(arr: ByteArray, strOff: Int): String {
val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff
val chars = ByteArray(strLen)
for (ii in 0 until strLen) {
chars[ii] = arr[strOff + 2 + ii * 2]
}
return String(chars) // Hack, just use 8 byte chars
} // end of compXmlStringAt
// LEW -- Return value of a Little Endian 32 bit word from the byte array
// at offset off.
fun LEW(arr: ByteArray, off: Int): Int {
return (arr[off + 3].toInt() shl 24) and -0x1000000 or
(arr[off + 2].toInt() shl 16 and 0xff0000) or
(arr[off + 1].toInt() shl 8 and 0xff00) or
(arr[off].toInt() and 0xFF)
} // end of LEW
@Throws(Exception::class)
fun loadXMLFromString(xml: String?): Document {
val docBuilderFactory = DocumentBuilderFactory.newInstance()
val docBuilder = docBuilderFactory.newDocumentBuilder()
return docBuilder.parse(InputSource(StringReader(xml)))
}
@Throws(IOException::class)
fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String {
var zip: ZipFile? = null
zip = ZipFile(filePath)
val androidManifest = zip.getEntry("AndroidManifest.xml")
val classesDex = zip.getEntry("classes.dex")
// write dex file
val dexStream = zip.getInputStream(classesDex)
Files.newOutputStream(Paths.get(dexPath)).use { os ->
val buffer = ByteArray(1024)
var len: Int
while (dexStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
// read xml file
val `is` = zip.getInputStream(androidManifest)
val buf = ByteArray(1024000) // 100 kb
`is`.read(buf)
`is`.close()
zip.close()
val xml = decompressXML(buf)
try {
val xmlDoc = loadXMLFromString(xml)
val pkg = xmlDoc.documentElement.getAttribute("package")
val nodes = xmlDoc.getElementsByTagName("meta-data")
for (i in 0 until nodes.length) {
val attributes = nodes.item(i).attributes
println(attributes.getNamedItem("name").nodeValue)
if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue
}
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
}
// original Java code
// package ir.armor.tachidesk.impl.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 org.w3c.dom.Document;
// import org.w3c.dom.NamedNodeMap;
// import org.w3c.dom.NodeList;
// import org.xml.sax.InputSource;
//
// import javax.xml.parsers.DocumentBuilder;
// import javax.xml.parsers.DocumentBuilderFactory;
// import java.io.*;
// import java.nio.file.Files;
// import java.nio.file.Paths;
// import java.util.zip.ZipEntry;
// import java.util.zip.ZipFile;
//
// public class APKExtractor {
// // decompressXML -- Parse the 'compressed' binary form of Android XML docs
// // such as for AndroidManifest.xml in .apk files
// public static int endDocTag = 0x00100101;
// public static int startTag = 0x00100102;
// public static int endTag = 0x00100103;
//
// static void prt(String str) {
// //System.err.print(str);
// }
//
// public static String decompressXML(byte[] xml) {
//
// StringBuilder finalXML = new StringBuilder();
//
// // Compressed XML file/bytes starts with 24x bytes of data,
// // 9 32 bit words in little endian order (LSB first):
// // 0th word is 03 00 08 00
// // 3rd word SEEMS TO BE: Offset at then of StringTable
// // 4th word is: Number of strings in string table
// // WARNING: Sometime I indiscriminently display or refer to word in
// // little endian storage format, or in integer format (ie MSB first).
// int numbStrings = LEW(xml, 4 * 4);
//
// // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// // of the length/string data in the StringTable.
// int sitOff = 0x24; // Offset of start of StringIndexTable
//
// // StringTable, each string is represented with a 16 bit little endian
// // character count, followed by that number of 16 bit (LE) (Unicode)
// // chars.
// int stOff = sitOff + numbStrings * 4; // StringTable follows
// // StrIndexTable
//
// // XMLTags, The XML tag tree starts after some unknown content after the
// // StringTable. There is some unknown data after the StringTable, scan
// // forward from this point to the flag for the start of an XML start
// // tag.
// int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd
// // word.
// // Scan forward until we find the bytes: 0x02011000(x00100102 in normal
// // int)
// for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
// if (LEW(xml, ii) == startTag) {
// xmlTagOff = ii;
// break;
// }
// } // end of hack, scanning for start of first start tag
//
// // XML tags and attributes:
// // Every XML start and end tag consists of 6 32 bit words:
// // 0th word: 02011000 for startTag and 03011000 for endTag
// // 1st word: a flag?, like 38000000
// // 2nd word: Line of where this tag appeared in the original source file
// // 3rd word: FFFFFFFF ??
// // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// // 5th word: StringIndex of Element Name
// // (Note: 01011000 in 0th word means end of XML document, endDocTag)
//
// // Start tags (not end tags) contain 3 more words:
// // 6th word: 14001400 meaning??
// // 7th word: Number of Attributes that follow this tag(follow word 8th)
// // 8th word: 00000000 meaning??
//
// // Attributes consist of 5 words:
// // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// // 1st word: StringIndex of Attribute Name
// // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId
// // used
// // 3rd word: Flags?
// // 4th word: str ind of attr value again, or ResourceId of value
//
// // TMP, dump string table to tr for debugging
// // tr.addSelect("strings", null);
// // for (int ii=0; ii<numbStrings; ii++) {
// // // Length of string starts at StringTable plus offset in StrIndTable
// // String str = compXmlString(xml, sitOff, stOff, ii);
// // tr.add(String.valueOf(ii), str);
// // }
// // tr.parent();
//
// // Step through the XML tree element tags and attributes
// int off = xmlTagOff;
// int indent = 0;
// int startTagLineNo = -2;
// while (off < xml.length) {
// int tag0 = LEW(xml, off);
// // int tag1 = LEW(xml, off+1*4);
// int lineNo = LEW(xml, off + 2 * 4);
// // int tag3 = LEW(xml, off+3*4);
// int nameNsSi = LEW(xml, off + 4 * 4);
// int nameSi = LEW(xml, off + 5 * 4);
//
// if (tag0 == startTag) { // XML START TAG
// int tag6 = LEW(xml, off + 6 * 4); // Expected to be 14001400
// int numbAttrs = LEW(xml, off + 7 * 4); // Number of Attributes
// // to follow
// // int tag8 = LEW(xml, off+8*4); // Expected to be 00000000
// off += 9 * 4; // Skip over 6+3 words of startTag data
// String name = compXmlString(xml, sitOff, stOff, nameSi);
// // tr.addSelect(name, null);
// startTagLineNo = lineNo;
//
// // Look for the Attributes
// StringBuffer sb = new StringBuffer();
// for (int ii = 0; ii < numbAttrs; ii++) {
// int attrNameNsSi = LEW(xml, off); // AttrName Namespace Str
// // Ind, or FFFFFFFF
// int attrNameSi = LEW(xml, off + 1 * 4); // AttrName String
// // Index
// int attrValueSi = LEW(xml, off + 2 * 4); // AttrValue Str
// // Ind, or
// // FFFFFFFF
// int attrFlags = LEW(xml, off + 3 * 4);
// int attrResId = LEW(xml, off + 4 * 4); // AttrValue
// // ResourceId or dup
// // AttrValue StrInd
// off += 5 * 4; // Skip over the 5 words of an attribute
//
// String attrName = compXmlString(xml, sitOff, stOff,
// attrNameSi);
// String attrValue = attrValueSi != -1 ? compXmlString(xml,
// sitOff, stOff, attrValueSi) : "resourceID 0x"
// + Integer.toHexString(attrResId);
// sb.append(" " + attrName + "=\"" + attrValue + "\"");
// // tr.add(attrName, attrValue);
// }
// finalXML.append("<" + name + sb + ">");
// prtIndent(indent, "<" + name + sb + ">");
// indent++;
//
// } else if (tag0 == endTag) { // XML END TAG
// indent--;
// off += 6 * 4; // Skip over 6 words of endTag data
// String name = compXmlString(xml, sitOff, stOff, nameSi);
// finalXML.append("</" + name + ">");
// prtIndent(indent, "</" + name + "> (line " + startTagLineNo
// + "-" + lineNo + ")");
// // tr.parent(); // Step back up the NobTree
//
// } else if (tag0 == endDocTag) { // END OF XML DOC TAG
// break;
//
// } else {
// prt(" Unrecognized tag code '" + Integer.toHexString(tag0)
// + "' at offset " + off);
// break;
// }
// } // end of while loop scanning tags and attributes of XML tree
// //prt(" end at offset " + off);
// return finalXML.toString();
// } // end of decompressXML
//
// public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
// if (strInd < 0)
// return null;
// int strOff = stOff + LEW(xml, sitOff + strInd * 4);
// return compXmlStringAt(xml, strOff);
// }
//
// public static String spaces = " ";
//
// public static void prtIndent(int indent, String str) {
// prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str);
// }
//
// // compXmlStringAt -- Return the string stored in StringTable format at
// // offset strOff. This offset points to the 16 bit string length, which
// // is followed by that number of 16 bit (Unicode) chars.
// public static String compXmlStringAt(byte[] arr, int strOff) {
// int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
// byte[] chars = new byte[strLen];
// for (int ii = 0; ii < strLen; ii++) {
// chars[ii] = arr[strOff + 2 + ii * 2];
// }
// return new String(chars); // Hack, just use 8 byte chars
// } // end of compXmlStringAt
//
// // LEW -- Return value of a Little Endian 32 bit word from the byte array
// // at offset off.
// public static int LEW(byte[] arr, int off) {
// return ((arr[off + 3] << 24) & 0xff000000) |
// ((arr[off + 2] << 16) & 0xff0000) |
// ((arr[off + 1] << 8) & 0xff00) |
// (arr[off] & 0xFF);
// } // end of LEW
//
// public static Document loadXMLFromString(String xml) throws Exception {
// DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
// DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
// return docBuilder.parse(new InputSource(new StringReader(xml)));
// }
//
// public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException {
// ZipFile zip = null;
//
// zip = new ZipFile(filePath);
// ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml");
// ZipEntry classesDex = zip.getEntry("classes.dex");
//
// // write dex file
// InputStream dexStream = zip.getInputStream(classesDex);
// try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) {
// byte[] buffer = new byte[1024];
// int len;
// while ((len = dexStream.read(buffer)) > 0) {
// os.write(buffer, 0, len);
// }
// }
//
// // read xml file
// InputStream is = zip.getInputStream(androidManifest);
// byte[] buf = new byte[1024000]; // 100 kb
// is.read(buf);
// is.close();
// zip.close();
//
// String xml = APKExtractor.decompressXML(buf);
// try {
// Document xmlDoc = loadXMLFromString(xml);
// String pkg = xmlDoc.getDocumentElement().getAttribute("package");
// NodeList nodes = xmlDoc.getElementsByTagName("meta-data");
// for (int i = 0; i < nodes.getLength(); i++) {
// NamedNodeMap attributes = nodes.item(i).getAttributes();
// System.out.println(attributes.getNamedItem("name").getNodeValue());
// if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class"))
// return pkg + attributes.getNamedItem("value").getNodeValue();
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// return "";
// }
// }
@@ -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 * 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/. */ * 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 cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName" val filePath = "$saveDir/$fileName"
if (cachedFile != null) { 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"))
}
}
@@ -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 eu.kanade.tachiyomi.App
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables 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 net.harawata.appdirs.AppDirsFactory
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
@@ -12,6 +22,8 @@ import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File import java.io.File
private val logger = KotlinLogging.logger {}
object applicationDirs { object applicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
@@ -25,12 +37,17 @@ val systemTray by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() } val androidCompat by lazy { AndroidCompat() }
fun serverSetup() { fun applicationSetup() {
// register server config // register server config
GlobalConfigManager.registerModule( GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config) 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 // make dirs we need
listOf( listOf(
applicationDirs.dataRoot, applicationDirs.dataRoot,
@@ -41,10 +58,29 @@ fun serverSetup() {
File(it).mkdirs() 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() makeDataBaseTables()
// create system tray // create system tray
systemTray if (serverConfig.systemTrayEnabled)
try {
systemTray
} catch (e: Exception) {
e.printStackTrace()
}
// Load config API // Load config API
DI.global.addImport(ConfigKodeinModule().create()) DI.global.addImport(ConfigKodeinModule().create())
@@ -53,6 +89,11 @@ fun serverSetup() {
// start app // start app
androidCompat.startApp(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 // socks proxy settings
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString() System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
@@ -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 * 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/. */ * 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.CacheUtil
import dorkbox.util.Desktop import dorkbox.util.Desktop
import ir.armor.tachidesk.Main import ir.armor.tachidesk.Main
import ir.armor.tachidesk.server.serverConfig
import java.awt.event.ActionListener import java.awt.event.ActionListener
import java.io.IOException import java.io.IOException
@@ -24,7 +28,7 @@ fun openInBrowser() {
fun systemTray(): SystemTray? { fun systemTray(): SystemTray? {
try { try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java // 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")) if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
+16
View File
@@ -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 and port bindings
server.ip = 0.0.0.0 server.ip = "0.0.0.0"
server.port = 4567 server.port = 4567
# Socks5 proxy # Socks5 proxy
server.socksProxy = false server.socksProxy = false
server.socksProxyHost = "" server.socksProxyHost = ""
server.socksProxyPort = "" server.socksProxyPort = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.initialOpenInBrowserEnabled = true
+2
View File
@@ -8,11 +8,13 @@
"@testing-library/jest-dom": "^5.11.4", "@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/react-lazyload": "^3.1.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0", "react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-lazyload": "^3.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.1",
"web-vitals": "^0.2.4" "web-vitals": "^0.2.4"
+21 -5
View File
@@ -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 * 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/. */ * 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() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />); const [action, setAction] = useState<any>(<div />);
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true); const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = { const navBarContext = {
title, setTitle, action, setAction, title, setTitle, action, setAction, override, setOverride,
}; };
const darkThemeContext = { darkTheme, setDarkTheme }; const darkThemeContext = { darkTheme, setDarkTheme };
@@ -63,7 +69,12 @@ export default function App() {
<NavbarContext.Provider value={navBarContext}> <NavbarContext.Provider value={navBarContext}>
<CssBaseline /> <CssBaseline />
<NavBar /> <NavBar />
<Container maxWidth={false} disableGutters> <Container
id="appMainContainer"
maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch> <Switch>
<Route path="/sources/:sourceId/search/"> <Route path="/sources/:sourceId/search/">
<Search /> <Search />
@@ -80,8 +91,8 @@ export default function App() {
<Route path="/sources"> <Route path="/sources">
<Sources /> <Sources />
</Route> </Route>
<Route path="/manga/:mangaId/chapter/:chapterId"> <Route path="/manga/:mangaId/chapter/:chapterNum">
<Reader /> <></>
</Route> </Route>
<Route path="/manga/:id"> <Route path="/manga/:id">
<Manga /> <Manga />
@@ -106,6 +117,11 @@ export default function App() {
/> />
</Switch> </Switch>
</Container> </Container>
<Switch>
<Route path="/manga/:mangaId/chapter/:chapterIndex">
<Reader />
</Route>
</Switch>
</NavbarContext.Provider> </NavbarContext.Provider>
</ThemeProvider> </ThemeProvider>
</Router> </Router>
+12 -1
View File
@@ -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 * 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/. */ * 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> <DialogTitle>Set categories</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<FormGroup> <FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => ( {categoryInfos.map((categoryInfo) => (
<FormControlLabel <FormControlLabel
control={( control={(
+20 -4
View File
@@ -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 * 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/. */ * 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 CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link, useHistory } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -41,6 +46,7 @@ interface IProps{
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const history = useHistory();
const { chapter } = props; const { chapter } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10); 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> </Typography>
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <Link
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button> to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
</div> style={{ textDecoration: 'none' }}
>
<Button
variant="outlined"
style={{ marginLeft: 20 }}
>
open
</Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
</li> </li>
+4 -1
View File
@@ -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 * 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/. */ * 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 * 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/. */ * 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>
);
}
+5 -2
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -43,7 +46,7 @@ const useStyles = makeStyles({
}); });
interface IProps { interface IProps {
manga: IManga manga: IMangaCard
} }
const MangaCard = React.forwardRef((props: IProps, ref) => { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
+205 -34
View File
@@ -1,56 +1,189 @@
/* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Button, createStyles, makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import React, { useState } from 'react'; 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 client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect'; import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({ const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: { 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', 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': { '& 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',
}, },
}, },
})); }));
interface IProps{ interface IProps{
manga: IManga manga: IManga
source: ISource
} }
function getSourceName(source: ISource) { function getSourceName(source: ISource) {
if (source.name !== null) { return source.name; } if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id; return source.id;
} }
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) { export default function MangaDetails(props: IProps) {
const classes = useStyles(); const { setAction } = useContext(NavbarContext);
const { manga, source } = props;
const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>( 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); 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() { function addToLibrary() {
setInLibrary('adding'); // setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => { client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library'); setInLibrary('In Library');
}); });
} }
function removeFromLibrary() { function removeFromLibrary() {
setInLibrary('removing'); // setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => { client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library'); setInLibrary('Add To Library');
}); });
} }
function handleButtonClick() { function handleButtonClick() {
if (inLibrary === 'Not In Library') { if (inLibrary === 'Add To Library') {
addToLibrary(); addToLibrary();
} else { } else {
removeFromLibrary(); removeFromLibrary();
@@ -58,26 +191,64 @@ export default function MangaDetails(props: IProps) {
} }
return ( return (
<div> <div className={classes.root}>
<h1> <div className={classes.top}>
{manga.title} <div className={classes.leftRight}>
</h1> <div className={classes.leftSide}>
<h3> <img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
Source: </div>
{' '} <div className={classes.rightSide}>
{getSourceName(source)} <h1>
</h3> {manga.title}
<div className={classes.root}> </h1>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button> <h3>
{inLibrary === 'In Library' Author:
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>} {' '}
<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> </div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div> </div>
); );
} }
+6 -3
View File
@@ -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 * 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/. */ * 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'; import MangaCard from './MangaCard';
interface IProps{ interface IProps{
mangas: IManga[] mangas: IMangaCard[]
message?: string message?: string
hasNextPage: boolean hasNextPage: boolean
lastPageNum: number lastPageNum: number
@@ -48,7 +51,7 @@ export default function MangaGrid(props: IProps) {
} }
return ( return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}> <Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped} {mapped}
</Grid> </Grid>
); );
+32 -84
View File
@@ -1,23 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /*
// TODO: remove above! * Copyright (C) Contributors to the Suwayomi project
/* This Source Code Form is subject to the terms of the Mozilla Public *
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarContext from '../context/NavbarContext'; import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme'; import DarkTheme from '../context/DarkTheme';
import TemporaryDrawer from './TemporaryDrawer';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -31,89 +28,40 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() { export default function NavBar() {
const classes = useStyles(); const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const { title, action, override } = useContext(NavBarContext);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { darkTheme } = useContext(DarkTheme); const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return ( return (
<div className={classes.root}> <>
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}> {override.status && override.value}
<Toolbar> {!override.status
<IconButton && (
edge="start" <div className={classes.root}>
className={classes.menuButton} <AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
color="inherit" <Toolbar>
aria-label="menu" <IconButton
disableRipple edge="start"
onClick={() => setDrawerOpen(true)} className={classes.menuButton}
> color="inherit"
<MenuIcon /> aria-label="menu"
</IconButton> disableRipple
<Typography variant="h6" className={classes.title}> onClick={() => setDrawerOpen(true)}
{title}
</Typography>
{action}
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton> */}
{/* <Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
> >
Dark Theme <MenuIcon />
</IconButton>
</MenuItem> <Typography variant="h6" className={classes.title}>
<MenuItem {title}
onClick={() => { setDarkTheme(false); handleClose(); }} </Typography>
> {action}
Light Theme </Toolbar>
</AppBar>
</MenuItem> <TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</Menu> */} </div>
</Toolbar> )}
</AppBar> </>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
); );
} }
+119
View File
@@ -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>
);
}
+362
View File
@@ -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>
</>
);
}
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+45 -56
View File
@@ -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 * 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/. */ * 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) { export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
const classes = useStyles(); 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 ( return (
<div> <div>
<Drawer <Drawer
BackdropProps={{ invisible: true }}
open={drawerOpen} open={drawerOpen}
anchor="left" anchor="left"
onClose={() => setDrawerOpen(false)} 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> </Drawer>
</div> </div>
); );
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+8 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -9,6 +12,8 @@ type ContextType = {
setTitle: React.Dispatch<React.SetStateAction<string>> setTitle: React.Dispatch<React.SetStateAction<string>>
action: any action: any
setAction: React.Dispatch<React.SetStateAction<any>> setAction: React.Dispatch<React.SetStateAction<any>>
override: INavbarOverride
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
}; };
const NavBarContext = React.createContext<ContextType>({ const NavBarContext = React.createContext<ContextType>({
@@ -16,6 +21,8 @@ const NavBarContext = React.createContext<ContextType>({
setTitle: ():void => {}, setTitle: ():void => {},
action: <div />, action: <div />,
setAction: ():void => {}, setAction: ():void => {},
override: { status: false, value: <div /> },
setOverride: ():void => {},
}); });
export default NavBarContext; export default NavBarContext;
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+4 -1
View File
@@ -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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+54 -22
View File
@@ -1,22 +1,55 @@
/* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, useState, useContext } from 'react'; import React, { useEffect, useState, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import CircularProgress from '@material-ui/core/CircularProgress';
import ChapterCard from '../components/ChapterCard'; import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails'; import MangaDetails from '../components/MangaDetails';
import NavbarContext from '../context/NavbarContext'; import NavbarContext from '../context/NavbarContext';
import client from '../util/client'; 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() { export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext); const classes = useStyles();
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{id: string}>(); const { id } = useParams<{id: string}>();
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [source, setSource] = useState<ISource>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => { useEffect(() => {
@@ -28,32 +61,31 @@ export default function Manga() {
}); });
}, []); }, []);
useEffect(() => {
if (manga !== undefined) {
client.get(`/api/v1/source/${manga.sourceId}`)
.then((response) => response.data)
.then((data: ISource) => {
setSource(data);
});
}
}, [manga]);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`) client.get(`/api/v1/manga/${id}/chapters`)
.then((response) => response.data) .then((response) => response.data)
.then((data) => setChapters(data)); .then((data) => setChapters(data));
}, []); }, []);
const chapterCards = chapters.map((chapter) => ( const chapterCards = (
<ol style={{ listStyle: 'none', padding: 0 }}> <LoadingPlaceholder
<ChapterCard chapter={chapter} /> shouldRender={chapters.length > 0}
</ol> >
)); <ol className={classes.chapters}>
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
</ol>
</LoadingPlaceholder>
);
return ( return (
<> <div className={classes.root}>
{(manga && source) && <MangaDetails manga={manga} source={source} />} <LoadingPlaceholder
shouldRender={manga !== undefined}
component={MangaDetails}
componentProps={{ manga }}
/>
{chapterCards} {chapterCards}
</> </div>
); );
} }

Some files were not shown because too many files have changed in this diff Show More