Compare commits

...

66 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
Aria Moradi 9cd93d467c bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 17s
2021-03-16 16:15:20 +03:30
Aria Moradi 257f8a5a27 fix extensions not showing the all pesudo-language 2021-03-16 16:10:06 +03:30
Aria Moradi 79bab08cae improvements 2021-03-16 16:04:29 +03:30
Aria Moradi 4e699e4f5a update dex2jar 2021-03-16 15:44:50 +03:30
Aria Moradi 1128f40bac closes #32 2021-03-14 23:57:33 +03:30
Aria Moradi 53ef836326 fix windows path 2021-03-14 20:28:23 +03:30
Aria Moradi b8df0e89e5 Don't show installed if nothing is installed 2021-03-14 14:09:31 +03:30
Aria Moradi 472bfec6bf improve docs 2021-03-14 01:26:52 +03:30
Aria Moradi c1b86cedd2 move getAndroid.sh 2021-03-14 01:02:43 +03:30
Aria Moradi 428c65f075 Enforce more limits on the issue format. 2021-03-13 22:59:37 +03:30
108 changed files with 2706 additions and 1004 deletions
+26 -5
View File
@@ -1,6 +1,27 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
* text=auto
* text eol=lf
# Windows forced line-endings
/.idea/* text eol=crlf
*.bat text eol=crlf
*.ps1 text eol=crlf
# Gradle wrapper
*.jar binary
# Images
*.webp binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.swp binary
*.pdf binary
+4 -3
View File
@@ -25,6 +25,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
## Device information
- Tachidesk version: (Example: v0.2.3-r255-win32)
- Server Operating System: (Example: Ubuntu 20.04)
- Server Desktop Environment: N/A or (Example: Gnome 40)
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
- Client Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
@@ -34,10 +35,10 @@ Note that the issue will be automatically closed if you do not fill out the titl
2. Second Step
### Expected behavior
Describe what should have happened
Describe what should have happened. Remove this line after you are done.
### Actual behavior
Describe what happens instead
Describe what happens instead. Remove this line after you are done.
## Other details
Describe additional details If necessary
Describe additional details If necessary. Remove this line after you are done.
+2 -2
View File
@@ -23,7 +23,7 @@ Note that the issue will be automatically closed if you do not fill out the titl
---
## What feature should be added to Tachidesk?
Explain What the feature is and how it should work in detail
Explain What the feature is and how it should work in detail. Remove this line after you are done.
## Why/Project's Benefit/Existing Problem
Explain why this should be added
Explain why this should be added. Remove this line after you are done.
+6 -1
View File
@@ -26,7 +26,12 @@ jobs:
},
{
"type": "body",
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out"
},
{
"type": "body",
"regex": ".*Remove this line after you are done.*",
"message": "The lines requesting to be removed were not removed."
}
]
@@ -1,5 +1,12 @@
package xyz.nulldev.ts.config
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
+8 -6
View File
@@ -18,9 +18,7 @@ repositories {
dependencies {
// Android stub library
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
implementation(fileTree("lib/"))
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
// Android JAR libs
@@ -41,10 +39,10 @@ dependencies {
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
// Config API
implementation( project(":AndroidCompat:Config"))
implementation(project(":AndroidCompat:Config"))
// dex2jar
// compileOnly( "dex2jar:dex-translator")
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser
compileOnly("net.dongliu:apk-parser:2.6.10")
@@ -55,7 +53,11 @@ dependencies {
// AndroidX annotations
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
// compileOnly("io.reactivex:rxjava:1.3.8")
// substitute for duktape-android
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
implementation("org.mozilla:rhino-runtime:1.7.13")
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
}
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
+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!"
@@ -1,4 +1,19 @@
#!/usr/bin/env bash
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# This is a bash script to create android.jar stubs
# foolproof against running from AndroidCompat dir instead of running from project root
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
cd ..
fi
echo "Getting required Android.jar..."
rm -rf "tmp"
mkdir -p "tmp"
@@ -6,7 +21,7 @@ pushd "tmp"
curl "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" | base64 --decode > android.jar
# We need to remove any stub classes that we might use
# We need to remove any stub classes that we have implementations for
echo "Patching JAR..."
echo "Removing org.json..."
@@ -1,20 +1,12 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.duktape;
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import kotlin.NotImplementedError;
import javax.script.ScriptEngine;
@@ -22,11 +14,18 @@ import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.Closeable;
/** A simple EMCAScript (Javascript) interpreter. */
/* Note (March 2021):
* The old implementation for duktape-android used the nashorn engine which is deprecated.
* This new implementation uses Mozilla's Rhino: https://github.com/mozilla/rhino
*/
/**
* A simple EMCAScript (Javascript) interpreter.
*/
public final class Duktape implements Closeable, AutoCloseable {
private ScriptEngineManager factory = new ScriptEngineManager();
private ScriptEngine engine = factory.getEngineByName("JavaScript");
private ScriptEngine engine = factory.getEngineByName("rhino");
/**
* Create a new interpreter instance. Calls to this method <strong>must</strong> matched with
@@ -38,17 +37,6 @@ public final class Duktape implements Closeable, AutoCloseable {
private Duktape() {}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting. Note that the result must be one of the supported Java types or the call will
* return null.
*
* @throws DuktapeException if there is an error evaluating the script.
*/
public synchronized Object evaluate(String script, String fileName) {
throw new NotImplementedError("Not implemented!");
}
/**
* Evaluate {@code script} and return a result. Note that the result must be one of the
* supported Java types or the call will return null.
@@ -76,18 +64,18 @@ public final class Duktape implements Closeable, AutoCloseable {
throw new NotImplementedError("Not implemented!");
}
/**
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
* any overloaded methods.
* <p>Methods of the interface may return {@code void} or any of the following supported argument
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
* {@link Double}, {@link String}.
*/
public synchronized <T> T get(final String name, final Class<T> type) {
throw new NotImplementedError("Not implemented!");
}
// /**
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
// * any overloaded methods.
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
// * {@link Double}, {@link String}.
// */
// public synchronized <T> T get(final String name, final Class<T> type) {
// throw new NotImplementedError("Not implemented!");
// }
/**
* Release the native resources associated with this object. You <strong>must</strong> call this
@@ -0,0 +1,37 @@
package com.squareup.duktape;
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
import java.io.Closeable;
import java.io.IOException;
/** This is the reference Duktape stub that tachiyomi's extensions depend on.
* Intended to be used as a reference.
*/
public class DuktapeStub implements Closeable {
public static Duktape create() {
throw new RuntimeException("Stub!");
}
@Override
public synchronized void close() throws IOException {
throw new RuntimeException("Stub!");
}
public synchronized Object evaluate(String script) {
throw new RuntimeException("Stub!");
}
public synchronized <T> void set(String name, Class<T> type, T object) {
throw new RuntimeException("Stub!");
}
}
+35 -20
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
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
@@ -13,27 +13,33 @@ Here is a list of current features:
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
- A library to save your mangas and categories to put them into.
- Searching and browsing installed sources.
- A minimal chapter reader.
- A decent chapter reader.
- Ability to download Mangas for offline read(This partially works)
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
## Downloading and Running the app
#### Prerequisites
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
#### Download the app
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
Download the latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
#### Running pre-built jar packages
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
#### Running pre-built Windows packages
Windows specific builds have java bundled inside them, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`, the rest will work like the jar release.
### Windows
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
#### Running on Docker
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
### Arch Linux
You can install Tachidesk from the AUR
```
yay -S tachidesk
```
### Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting
@@ -43,7 +49,7 @@ On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
@@ -56,18 +62,25 @@ This project has two components:
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Building from source
### Get Android stubs jar
### Prerequisite: Get Android stubs jar
#### Manual download
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
### building the jar
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
### Prerequisite: Software dependencies
You need this software packages installed in order to build this project:
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
- Nodejs LTS or latest
- Yarn
### building the full-blown jar
Run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building without `webUI` bundled
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar -x :webUI:copyBuild`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
### building the Windows package
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
## Running for development purposes
### `server` module
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
### `webUI` module
How to do it is described in `webUI/react/README.md` but for short,
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
@@ -76,7 +89,9 @@ How to do it is described in `webUI/react/README.md` but for short,
and supports HMR and all the other goodies you'll need.
## Credit
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
The `AndroidCompat` module was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
@@ -86,7 +101,7 @@ Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this pro
## License
Copyright (C) 2020-2021 Aria Moradi and contributors
Copyright (C) Contributors to the Suwayomi project
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
+3 -1
View File
@@ -61,7 +61,7 @@ configure(listOf(
// Logging
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("org.slf4j:slf4j-simple:1.7.30")
implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.3")
// RxJava
@@ -76,6 +76,8 @@ configure(listOf(
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0")
implementation("io.github.config4k:config4k:0.4.2")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
+6 -9
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9"
}
val TachideskVersion = "v0.2.4"
val TachideskVersion = "v0.2.7"
repositories {
@@ -57,19 +57,17 @@ dependencies {
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("com.squareup.duktape:duktape-android:1.3.0")
val coroutinesVersion = "1.3.9"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
// dex2jar
implementation(fileTree("lib/dex2jar/"))
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// api
implementation("io.javalin:javalin:3.12.0")
implementation("org.slf4j:slf4j-simple:1.8.0-beta4")
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// Exposed ORM
@@ -87,9 +85,8 @@ dependencies {
implementation(project(":AndroidCompat"))
implementation(project(":AndroidCompat:Config"))
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
// uncomment to test extensions directly
// implementation(fileTree("lib/"))
}
val name = "ir.armor.tachidesk.Main"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,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
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import android.content.Context
// import android.content.res.Configuration
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.app.Application
import com.google.gson.Gson
// import eu.kanade.tachiyomi.data.cache.ChapterCache
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.extension.api
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.content.Context
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.extension.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.annotation.SuppressLint
// import android.content.Context
// import android.os.Build
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@@ -1,5 +1,12 @@
package eu.kanade.tachiyomi.network
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.content.Context
// import eu.kanade.tachiyomi.BuildConfig
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@@ -1,248 +1,22 @@
package ir.armor.tachidesk
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import io.javalin.Javalin
import ir.armor.tachidesk.util.addMangaToCategory
import ir.armor.tachidesk.util.addMangaToLibrary
import ir.armor.tachidesk.util.createCategory
import ir.armor.tachidesk.util.getCategoryList
import ir.armor.tachidesk.util.getCategoryMangaList
import ir.armor.tachidesk.util.getChapter
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionIcon
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getLibraryMangas
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaCategories
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.openInBrowser
import ir.armor.tachidesk.util.removeCategory
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.removeMangaFromCategory
import ir.armor.tachidesk.util.removeMangaFromLibrary
import ir.armor.tachidesk.util.reorderCategory
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.updateCategory
import ir.armor.tachidesk.server.applicationSetup
import ir.armor.tachidesk.server.javalinSetup
class Main {
companion object {
@JvmStatic
fun main(args: Array<String>) {
serverSetup()
var hasWebUiBundled: Boolean = false
val app = Javalin.create { config ->
try {
this::class.java.classLoader.getResource("/react/index.html")
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled) {
openInBrowser()
}
app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("installing $apkName")
ctx.status(
installAPK(apkName)
)
}
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
println("uninstalling $apkName")
removeExtension(apkName)
ctx.status(200)
}
app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList())
}
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = true))
}
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = false))
}
app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId))
}
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId)
ctx.status(200)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId)
ctx.status(200)
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getMangaCategories(mangaId))
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
addMangaToCategory(mangaId, categoryId)
ctx.status(200)
}
// removes the manga from the category
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterId, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
val chapterId = ctx.pathParam("chapterId").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterId, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
}
// single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
// lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas())
}
// category list
app.get("/api/v1/category/") { ctx ->
ctx.json(getCategoryList())
}
// category create
app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!!
createCategory(name)
ctx.status(200)
}
// category modification
app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId")!!.toInt()
val name = ctx.formParam("name")
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding)
ctx.status(200)
}
// category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to)
ctx.status(200)
}
// category delete
app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
removeCategory(categoryId)
ctx.status(200)
}
// returns the manga list associated with a category
app.get("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId))
}
applicationSetup()
javalinSetup()
}
}
}
@@ -1,25 +0,0 @@
package ir.armor.tachidesk
import com.typesafe.config.Config
import xyz.nulldev.ts.config.ConfigModule
import java.io.File
class ServerConfig(config: Config) : ConfigModule(config) {
val ip = config.getString("ip")
val port = config.getInt("port")
// proxy
val socksProxy = config.getBoolean("socksProxy")
val socksProxyHost = config.getString("socksProxyHost")
val socksProxyPort = config.getString("socksProxyPort")
fun registerFile(file: String): File {
return File(file).apply {
mkdirs()
}
}
companion object {
fun register(config: Config) = ServerConfig(config.getConfig("server"))
}
}
@@ -1,10 +1,12 @@
package ir.armor.tachidesk.database
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.ChapterTable
@@ -12,6 +14,7 @@ import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -12,5 +15,7 @@ data class ChapterDataClass(
val chapter_number: Float,
val scanlator: String?,
val mangaId: Int,
val chapterIndex: Int,
val chapterCount: Int,
val pageCount: Int? = null,
)
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -8,7 +11,7 @@ import ir.armor.tachidesk.database.table.MangaStatus
data class MangaDataClass(
val id: Int,
val sourceId: Long,
val sourceId: String,
val url: String,
val title: String,
@@ -21,7 +24,8 @@ data class MangaDataClass(
val description: String? = null,
val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name,
val inLibrary: Boolean = false
val inLibrary: Boolean = false,
val source: SourceDataClass? = null
)
data class PagedMangaListDataClass(
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,13 +1,16 @@
package ir.armor.tachidesk.database.dataclass
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class SourceDataClass(
val id: String,
val name: String,
val lang: String,
val iconUrl: String,
val supportsLatest: Boolean
val name: String?,
val lang: String?,
val iconUrl: String?,
val supportsLatest: Boolean?
)
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.entity
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,11 +1,14 @@
package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import org.jetbrains.exposed.dao.id.IntIdTable
object CategoryMangaTable : IntIdTable() {
val category = reference("category", CategoryTable)
val manga = reference("manga", MangaTable)
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -13,5 +16,7 @@ object ChapterTable : IntIdTable() {
val chapter_number = float("chapter_number").default(-1f)
val scanlator = varchar("scanlator", 128).nullable()
val chapterIndex = integer("number_in_list")
val manga = reference("manga", MangaTable)
}
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,12 +1,15 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.util.proxyThumbnailUrl
import ir.armor.tachidesk.impl.proxyThumbnailUrl
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
@@ -28,13 +31,13 @@ object MangaTable : IntIdTable() {
val defaultCategory = bool("default_category").default(true)
// source is used by some ancestor of IntIdTable
val sourceReference = reference("source", SourceTable)
val sourceReference = long("source")
}
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
MangaDataClass(
mangaEntry[MangaTable.id].value,
mangaEntry[sourceReference].value,
mangaEntry[sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.database.table
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,4 +1,11 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
@@ -12,10 +19,6 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun createCategory(name: String) {
transaction {
val count = CategoryTable.selectAll().count()
@@ -1,4 +1,11 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.dataclass.MangaDataClass
@@ -14,10 +21,6 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
transaction {
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -14,11 +17,13 @@ import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
fun getChapterList(mangaId: Int): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId)
val source = getHttpSource(mangaDetails.sourceId)
val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList(
SManga.create().apply {
@@ -27,8 +32,10 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
}
).toBlocking().first()
val chapterCount = chapterList.count()
return transaction {
chapterList.forEach { fetchedChapter ->
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insertAndGetId {
@@ -38,12 +45,29 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
}
}
return@transaction chapterList.map {
// clear any orphaned chapters
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
if (dbChapterCount > chapterCount) { // we got some clean up due
// TODO
}
return@transaction chapterList.mapIndexed { index, it ->
ChapterDataClass(
ChapterTable.select { ChapterTable.url eq it.url }.firstOrNull()!![ChapterTable.id].value,
it.url,
@@ -51,18 +75,21 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
it.date_upload,
it.chapter_number,
it.scanlator,
mangaId
mangaId,
chapterCount - index,
chapterCount
)
}
}
}
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
fun getChapter(chapterIndex: Int, mangaId: Int): ChapterDataClass {
return transaction {
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
val chapterEntry = ChapterTable.select {
ChapterTable.chapterIndex eq chapterIndex and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
@@ -71,14 +98,20 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
}
).toBlocking().first()
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.selectAll().count() }
val chapter = ChapterDataClass(
chapterEntry[ChapterTable.id].value,
chapterId,
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count()
)
@@ -93,6 +126,13 @@ fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
it[this.chapter] = chapterId
}
}
} else {
transaction {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
}
}
}
@@ -1,20 +1,26 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.googlecode.dex2jar.tools.Dex2jarCmd
import com.googlecode.d2j.dex.Dex2jar
import com.googlecode.d2j.reader.MultiDexFileReader
import com.googlecode.dex2jar.tools.BaksmaliBaseDexExceptionHandler
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.APKExtractor
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.impl.util.APKExtractor
import ir.armor.tachidesk.server.applicationDirs
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
import okio.sink
@@ -28,8 +34,45 @@ import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
private val logger = KotlinLogging.logger {}
private fun dex2jar(dexFile: String, jarFile: String, fileNameWithoutType: String) {
// adopted from com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine
// source at: https://github.com/DexPatcher/dex2jar/tree/v2.1-20190905-lanchon/dex-tools/src/main/java/com/googlecode/dex2jar/tools/Dex2jarCmd.java
val jarFilePath = File(jarFile).toPath()
val reader = MultiDexFileReader.open(Files.readAllBytes(File(dexFile).toPath()))
val handler = BaksmaliBaseDexExceptionHandler()
Dex2jar
.from(reader)
.withExceptionHandler(handler)
.reUseReg(false)
.topoLogicalSort()
.skipDebug(true)
.optimizeSynchronized(false)
.printIR(false)
.noCode(false)
.skipExceptions(false)
.to(jarFilePath)
if (handler.hasException()) {
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
logger.error(
"Detail Error Information in File $errorFile\n" +
"Please report this file to one of following link if possible (any one).\n" +
" https://sourceforge.net/p/dex2jar/tickets/\n" +
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
" https://github.com/pxb1988/dex2jar/issues\n" +
" dex2jar@googlegroups.com"
)
handler.dump(errorFile, emptyArray<String>())
}
}
fun installAPK(apkName: String): Int {
logger.debug("Installing $apkName")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
@@ -49,9 +92,9 @@ fun installAPK(apkName: String): Int {
downloadAPKFile(apkToDownload, apkFilePath)
val className: String = APKExtractor.extract_dex_and_read_className(apkFilePath, dexFilePath)
println(className)
logger.debug(className)
// dex -> jar
Dex2jarCmd.main(dexFilePath, "-o", jarFilePath, "--force")
dex2jar(dexFilePath, jarFilePath, fileNameWithoutType)
// clean up
File(apkFilePath).delete()
@@ -69,11 +112,6 @@ fun installAPK(apkName: String): Int {
if (instance is HttpSource) { // single source
val httpSource = instance as HttpSource
transaction {
// SourceEntity.new {
// sourceId = httpSource.id
// name = httpSource.name
// this.extension = ExtensionEntity.find { ExtensionsTable.name eq extension.name }.first().id
// }
if (SourceTable.select { SourceTable.id eq httpSource.id }.count() == 0L) {
SourceTable.insert {
it[this.id] = httpSource.id
@@ -82,9 +120,7 @@ fun installAPK(apkName: String): Int {
it[extension] = extensionId
}
}
// println(httpSource.id)
// println(httpSource.name)
// println()
logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}")
}
} else { // multi source
val sourceFactory = instance as SourceFactory
@@ -101,9 +137,7 @@ fun installAPK(apkName: String): Int {
it[positionInFactorySource] = index
}
}
// println(httpSource.id)
// println(httpSource.name)
// println()
logger.debug("Installed source ${httpSource.name} with id:${httpSource.id}")
}
}
}
@@ -134,9 +168,11 @@ private fun downloadAPKFile(url: String, apkPath: String) {
sink.close()
}
fun removeExtension(pkgName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
val fileNameWithoutType = pkgName.substringBefore(".apk")
fun removeExtension(apkName: String) {
logger.debug("Uninstalling $apkName")
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
@@ -158,9 +194,8 @@ fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
val saveDir = "${applicationDirs.extensionsRoot}/icon"
val fileName = apkName
return getCachedResponse(saveDir, fileName) {
return getCachedImageResponse(saveDir, apkName) {
network.client.newCall(
GET(iconUrl)
).execute()
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -9,12 +12,15 @@ import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
import ir.armor.tachidesk.database.table.ExtensionTable
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
private val logger = KotlinLogging.logger {}
private object Data {
var lastExtensionCheck: Long = 0
}
@@ -28,7 +34,7 @@ private fun extensionDatabaseIsEmtpy(): Boolean {
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
// update if 60 seconds has passed or requested offline and database is empty
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
println("Getting extensions list from the internet")
logger.debug("Getting extensions list from the internet")
Data.lastExtensionCheck = System.currentTimeMillis()
var foundExtensions: List<Extension.Available>
runBlocking {
@@ -66,7 +72,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
}
}
} else {
println("used cached extension list")
logger.debug("used cached extension list")
}
return transaction {
@@ -1,20 +1,22 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
@@ -1,15 +1,18 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
@@ -21,7 +24,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
return if (mangaEntry[MangaTable.initialized]) {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
@@ -34,10 +37,11 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference])
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val fetchedManga = source.fetchMangaDetails(
SManga.create().apply {
url = mangaEntry[MangaTable.url]
@@ -65,7 +69,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
MangaDataClass(
mangaId,
mangaEntry[MangaTable.sourceReference].value,
mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title],
@@ -78,7 +82,8 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false
false,
getSource(mangaEntry[MangaTable.sourceReference])
)
}
}
@@ -88,8 +93,8 @@ fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference].value
return getCachedImageResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -52,7 +55,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
MangaDataClass(
mangaId,
sourceId,
sourceId.toString(),
manga.url,
manga.title,
@@ -70,7 +73,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
mangaId,
sourceId,
sourceId.toString(),
manga.url,
manga.title,
@@ -1,16 +1,19 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -25,10 +28,16 @@ fun getTrueImageUrl(page: Page, source: HttpSource): String {
return page.imageUrl!!
}
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.firstOrNull()!!
}
val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
val tachiPage = Page(
@@ -49,14 +58,14 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
File(saveDir).mkdirs()
val fileName = index.toString()
return getCachedResponse(saveDir, fileName) {
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
}
fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -18,6 +21,7 @@ fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaLi
}
fun sourceGlobalSearch(searchTerm: String) {
// TODO
}
data class FilterWrapper(
@@ -1,36 +1,45 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.dataclass.SourceDataClass
import ir.armor.tachidesk.database.entity.ExtensionEntity
import ir.armor.tachidesk.database.entity.SourceEntity
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable
import ir.armor.tachidesk.server.applicationDirs
import mu.KotlinLogging
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.lang.NullPointerException
import java.net.URL
import java.net.URLClassLoader
import java.util.Locale
private val logger = KotlinLogging.logger {}
private val sourceCache = mutableListOf<Pair<Long, HttpSource>>()
private val extensionCache = mutableListOf<Pair<String, Any>>()
fun getHttpSource(sourceId: Long): HttpSource {
val sourceRecord = transaction {
SourceEntity.findById(sourceId)
} ?: throw NullPointerException("Source with id $sourceId is not installed")
val cachedResult: Pair<Long, HttpSource>? = sourceCache.firstOrNull { it.first == sourceId }
if (cachedResult != null) {
println("used cached HttpSource: ${cachedResult.second.name}")
logger.debug("used cached HttpSource: ${cachedResult.second.name}")
return cachedResult.second
}
val result: HttpSource = transaction {
val sourceRecord = SourceEntity.findById(sourceId)!!
val extensionId = sourceRecord.extension.id.value
val extensionRecord = ExtensionEntity.findById(extensionId)!!
val apkName = extensionRecord.apkName
@@ -38,17 +47,15 @@ fun getHttpSource(sourceId: Long): HttpSource {
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
println(jarName)
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath }
var usedCached = false
val instance =
if (cachedExtensionPair != null) {
usedCached = true
println("Used cached Extension")
logger.debug("Used cached Extension")
cachedExtensionPair.second
} else {
println("No Extension cache")
logger.debug("No Extension cache")
val child = URLClassLoader(arrayOf<URL>(URL("file:$jarPath")), this::class.java.classLoader)
val classToLoad = Class.forName(className, true, child)
classToLoad.newInstance()
@@ -87,14 +94,14 @@ fun getSourceList(): List<SourceDataClass> {
fun getSource(sourceId: Long): SourceDataClass {
return transaction {
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
val source = SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()
return@transaction SourceDataClass(
source[SourceTable.id].value.toString(),
source[SourceTable.name],
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl],
getHttpSource(source[SourceTable.id].value).supportsLatest
sourceId.toString(),
source?.get(SourceTable.name),
source?.get(SourceTable.lang),
source?.let { ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl] },
source?.let { getHttpSource(sourceId).supportsLatest }
)
}
}
@@ -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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -52,7 +55,7 @@ private fun BufferedSource.saveTo(stream: OutputStream) {
}
}
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
fun getCachedImageResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName"
if (cachedFile != null) {
@@ -0,0 +1,260 @@
package ir.armor.tachidesk.server
import io.javalin.Javalin
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.impl.addMangaToCategory
import ir.armor.tachidesk.impl.addMangaToLibrary
import ir.armor.tachidesk.impl.createCategory
import ir.armor.tachidesk.impl.getCategoryList
import ir.armor.tachidesk.impl.getCategoryMangaList
import ir.armor.tachidesk.impl.getChapter
import ir.armor.tachidesk.impl.getChapterList
import ir.armor.tachidesk.impl.getExtensionIcon
import ir.armor.tachidesk.impl.getExtensionList
import ir.armor.tachidesk.impl.getLibraryMangas
import ir.armor.tachidesk.impl.getManga
import ir.armor.tachidesk.impl.getMangaCategories
import ir.armor.tachidesk.impl.getMangaList
import ir.armor.tachidesk.impl.getPageImage
import ir.armor.tachidesk.impl.getSource
import ir.armor.tachidesk.impl.getSourceList
import ir.armor.tachidesk.impl.getThumbnail
import ir.armor.tachidesk.impl.installAPK
import ir.armor.tachidesk.impl.removeCategory
import ir.armor.tachidesk.impl.removeExtension
import ir.armor.tachidesk.impl.removeMangaFromCategory
import ir.armor.tachidesk.impl.removeMangaFromLibrary
import ir.armor.tachidesk.impl.reorderCategory
import ir.armor.tachidesk.impl.sourceFilters
import ir.armor.tachidesk.impl.sourceGlobalSearch
import ir.armor.tachidesk.impl.sourceSearch
import ir.armor.tachidesk.impl.updateCategory
import ir.armor.tachidesk.server.util.openInBrowser
import mu.KotlinLogging
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
private val logger = KotlinLogging.logger {}
fun javalinSetup() {
var hasWebUiBundled = false
val app = Javalin.create { config ->
try {
Main::class.java.getResource("/react/index.html")
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
logger.warn("react build files are missing.")
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
openInBrowser()
}
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error("NullPointerException while handling the request", e)
ctx.status(404)
}
app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList())
}
app.get("/api/v1/extension/install/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
ctx.status(
installAPK(apkName)
)
}
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
removeExtension(apkName)
ctx.status(200)
}
// icon for extension named `apkName`
app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// list of sources
app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList())
}
// fetch source with id `sourceId`
app.get("/api/v1/source/:sourceId") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(getSource(sourceId))
}
// popular mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = true))
}
// latest mangas from source with id `sourceId`
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(getMangaList(sourceId, pageNum, popular = false))
}
// get manga info
app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getManga(mangaId))
}
// manga thumbnail
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val result = getThumbnail(mangaId)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
addMangaToLibrary(mangaId)
ctx.status(200)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
removeMangaFromLibrary(mangaId)
ctx.status(200)
}
// list manga's categories
app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getMangaCategories(mangaId))
}
// adds the manga to category
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
addMangaToCategory(mangaId, categoryId)
ctx.status(200)
}
// removes the manga from the category
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val categoryId = ctx.pathParam("categoryId").toInt()
removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
}
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapter(chapterIndex, mangaId))
}
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val index = ctx.pathParam("index").toInt()
val result = getPageImage(mangaId, chapterIndex, index)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
// global search
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
ctx.json(sourceGlobalSearch(searchTerm))
}
// single source search
app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
val searchTerm = ctx.pathParam("searchTerm")
val pageNum = ctx.pathParam("pageNum").toInt()
ctx.json(sourceSearch(sourceId, searchTerm, pageNum))
}
// source filter list
app.get("/api/v1/source/:sourceId/filters/") { ctx ->
val sourceId = ctx.pathParam("sourceId").toLong()
ctx.json(sourceFilters(sourceId))
}
// lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas())
}
// category list
app.get("/api/v1/category/") { ctx ->
ctx.json(getCategoryList())
}
// category create
app.post("/api/v1/category/") { ctx ->
val name = ctx.formParam("name")!!
createCategory(name)
ctx.status(200)
}
// category modification
app.patch("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val name = ctx.formParam("name")
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
updateCategory(categoryId, name, isLanding)
ctx.status(200)
}
// category re-ordering
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val from = ctx.formParam("from")!!.toInt()
val to = ctx.formParam("to")!!.toInt()
reorderCategory(categoryId, from, to)
ctx.status(200)
}
// category delete
app.delete("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
removeCategory(categoryId)
ctx.status(200)
}
// returns the manga list associated with a category
app.get("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
ctx.json(getCategoryMangaList(categoryId))
}
}
@@ -0,0 +1,31 @@
package ir.armor.tachidesk.server
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
class ServerConfig(config: Config) : ConfigModule(config) {
val ip: String by config
val port: Int by config
// proxy
val socksProxy: Boolean by config
val socksProxyHost: String by config
val socksProxyPort: String by config
// misc
val debugLogsEnabled: Boolean by config
val systemTrayEnabled: Boolean by config
val initialOpenInBrowserEnabled: Boolean by config
companion object {
fun register(config: Config) = ServerConfig(config.getConfig("server"))
}
}
@@ -1,8 +1,18 @@
package ir.armor.tachidesk
package ir.armor.tachidesk.server
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ch.qos.logback.classic.Level
import eu.kanade.tachiyomi.App
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import ir.armor.tachidesk.util.systemTray
import ir.armor.tachidesk.server.util.systemTray
import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import org.kodein.di.DI
import org.kodein.di.conf.global
@@ -12,6 +22,8 @@ import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
private val logger = KotlinLogging.logger {}
object applicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
val extensionsRoot = "$dataRoot/extensions"
@@ -25,12 +37,17 @@ val systemTray by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() }
fun serverSetup() {
fun applicationSetup() {
// register server config
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// set application wide logging level
if (serverConfig.debugLogsEnabled) {
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
}
// make dirs we need
listOf(
applicationDirs.dataRoot,
@@ -41,10 +58,29 @@ fun serverSetup() {
File(it).mkdirs()
}
// create conf file if doesn't exist
try {
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
if (!dataConfFile.exists()) {
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
dataConfFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
logger.error("Exception while creating initial server.conf:\n", e)
}
makeDataBaseTables()
// create system tray
systemTray
if (serverConfig.systemTrayEnabled)
try {
systemTray
} catch (e: Exception) {
e.printStackTrace()
}
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
@@ -53,6 +89,11 @@ fun serverSetup() {
// start app
androidCompat.startApp(App())
// Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util
package ir.armor.tachidesk.server.util
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -10,6 +13,7 @@ import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.server.serverConfig
import java.awt.event.ActionListener
import java.io.IOException
@@ -24,7 +28,7 @@ fun openInBrowser() {
fun systemTray(): SystemTray? {
try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
SystemTray.DEBUG = serverConfig.debugLogsEnabled
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
+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 = 0.0.0.0
server.ip = "0.0.0.0"
server.port = 4567
# Socks5 proxy
server.socksProxy = false
server.socksProxyHost = ""
server.socksProxyPort = ""
server.socksProxyPort = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.initialOpenInBrowserEnabled = true
@@ -1,13 +0,0 @@
/*
* This Kotlin source file was generated by the Gradle 'init' task.
*/
package ir.armor.tachidesk
import kotlin.test.Test
import kotlin.test.assertTrue
class AppTest {
@Test fun testAppHasAGreeting() {
assertTrue(true)
}
}
+2
View File
@@ -8,11 +8,13 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/react-lazyload": "^3.1.0",
"axios": "^0.21.1",
"fontsource-roboto": "^4.0.0",
"react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1",
"react-lazyload": "^3.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
+21 -5
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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -27,9 +30,12 @@ import useLocalStorage from './util/useLocalStorage';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
const [action, setAction] = useState<any>(<div />);
const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction,
title, setTitle, action, setAction, override, setOverride,
};
const darkThemeContext = { darkTheme, setDarkTheme };
@@ -63,7 +69,12 @@ export default function App() {
<NavbarContext.Provider value={navBarContext}>
<CssBaseline />
<NavBar />
<Container maxWidth={false} disableGutters>
<Container
id="appMainContainer"
maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch>
<Route path="/sources/:sourceId/search/">
<Search />
@@ -80,8 +91,8 @@ export default function App() {
<Route path="/sources">
<Sources />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterId">
<Reader />
<Route path="/manga/:mangaId/chapter/:chapterNum">
<></>
</Route>
<Route path="/manga/:id">
<Manga />
@@ -106,6 +117,11 @@ export default function App() {
/>
</Switch>
</Container>
<Switch>
<Route path="/manga/:mangaId/chapter/:chapterIndex">
<Reader />
</Route>
</Switch>
</NavbarContext.Provider>
</ThemeProvider>
</Router>
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -85,6 +88,14 @@ export default function CategorySelect(props: IProps) {
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -8,6 +12,7 @@ import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import { Link, useHistory } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({
root: {
@@ -41,6 +46,7 @@ interface IProps{
export default function ChapterCard(props: IProps) {
const classes = useStyles();
const history = useHistory();
const { chapter } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
@@ -63,9 +69,19 @@ export default function ChapterCard(props: IProps) {
</Typography>
</div>
</div>
<div style={{ display: 'flex' }}>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button>
</div>
<Link
to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
style={{ textDecoration: 'none' }}
>
<Button
variant="outlined"
style={{ marginLeft: 20 }}
>
open
</Button>
</Link>
</CardContent>
</Card>
</li>
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,4 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/require-default-props */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
const useStyles = makeStyles({
loading: {
margin: '10px auto',
display: 'flex',
justifyContent: 'center',
},
});
interface IProps {
shouldRender: boolean | (() => boolean)
children?: React.ReactNode
component?: string | React.FunctionComponent<any> | React.ComponentClass<any, any>
componentProps?: any
}
export default function LoadingPlaceholder(props: IProps) {
const {
children, shouldRender, component, componentProps,
} = props;
const classes = useStyles();
const condition = shouldRender instanceof Function ? shouldRender() : shouldRender;
if (condition) {
if (component) {
return React.createElement(component, componentProps);
}
if (children) {
return (
<>
{children}
</>
);
}
}
return (
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -43,7 +46,7 @@ const useStyles = makeStyles({
});
interface IProps {
manga: IManga
manga: IMangaCard
}
const MangaCard = React.forwardRef((props: IProps, ref) => {
const {
+208 -26
View File
@@ -1,18 +1,117 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Button, createStyles, makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import { Theme } from '@material-ui/core/styles';
import FavoriteIcon from '@material-ui/icons/Favorite';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FilterListIcon from '@material-ui/icons/FilterList';
import PublicIcon from '@material-ui/icons/Public';
import React, { useContext, useEffect, useState } from 'react';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'sticky',
top: '64px',
left: '0px',
width: '50vw',
height: 'calc(100vh - 64px)',
alignSelf: 'flex-start',
overflowY: 'auto',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex',
flexDirection: 'row-reverse',
},
leftSide: {
'& img': {
borderRadius: 4,
maxWidth: '100%',
minWidth: '100%',
height: 'auto',
},
maxWidth: '50%',
// [theme.breakpoints.up('md')]: {
// minWidth: '100px',
// },
},
rightSide: {
marginLeft: 15,
maxWidth: '100%',
'& span': {
fontWeight: '400',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
buttons: {
display: 'flex',
justifyContent: 'space-around',
'& button': {
marginLeft: 10,
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
},
'& span': {
display: 'block',
fontSize: '0.85em',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
},
},
}));
@@ -21,30 +120,70 @@ interface IProps{
manga: IManga
}
function getSourceName(source: ISource) {
if (source.name !== null) {
return `${source.name} (${source.lang.toLocaleUpperCase()})`;
}
return source.id;
}
function getValueOrUnknown(val: string) {
return val || 'UNKNOWN';
}
export default function MangaDetails(props: IProps) {
const classes = useStyles();
const { setAction } = useContext(NavbarContext);
const { manga } = props;
const [inLibrary, setInLibrary] = useState<string>(
manga.inLibrary ? 'In Library' : 'Not In Library',
manga.inLibrary ? 'In Library' : 'Add To Library',
);
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
useEffect(() => {
if (inLibrary === 'In Library') {
setAction(
<>
<IconButton
onClick={() => setCategoryDialogOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</>,
);
} else { setAction(<></>); }
}, [inLibrary, categoryDialogOpen]);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(inLibrary)();
function addToLibrary() {
setInLibrary('adding');
// setInLibrary('adding');
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('In Library');
});
}
function removeFromLibrary() {
setInLibrary('removing');
// setInLibrary('removing');
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
setInLibrary('Not In Library');
setInLibrary('Add To Library');
});
}
function handleButtonClick() {
if (inLibrary === 'Not In Library') {
if (inLibrary === 'Add To Library') {
addToLibrary();
} else {
removeFromLibrary();
@@ -52,21 +191,64 @@ export default function MangaDetails(props: IProps) {
}
return (
<div>
<h1>
{manga && manga.title}
</h1>
<div className={classes.root}>
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
{inLibrary === 'In Library'
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
<div className={classes.root}>
<div className={classes.top}>
<div className={classes.leftRight}>
<div className={classes.leftSide}>
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
</div>
<div className={classes.rightSide}>
<h1>
{manga.title}
</h1>
<h3>
Author:
{' '}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
Artist:
{' '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
Status:
{' '}
{manga.status}
</h3>
<h3>
Source:
{' '}
{getSourceName(manga.source)}
</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={() => handleButtonClick()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div>
<div className={classes.bottom}>
<div className={classes.description}>
<h4>About</h4>
<p>{manga.description}</p>
</div>
<div className={classes.genre}>
{manga.genre.split(', ').map((g) => <h5 key={g}>{g}</h5>)}
</div>
</div>
<CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div>
);
}
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -7,7 +10,7 @@ import Grid from '@material-ui/core/Grid';
import MangaCard from './MangaCard';
interface IProps{
mangas: IManga[]
mangas: IMangaCard[]
message?: string
hasNextPage: boolean
lastPageNum: number
@@ -48,7 +51,7 @@ export default function MangaGrid(props: IProps) {
}
return (
<Grid container spacing={1} xs={12} style={{ margin: 0, padding: '5px' }}>
<Grid container spacing={1} style={{ margin: 0, width: '100%', padding: '5px' }}>
{mapped}
</Grid>
);
+32 -84
View File
@@ -1,23 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove above!
/* This Source Code Form is subject to the terms of the Mozilla Public
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import MoreIcon from '@material-ui/icons/MoreVert';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
import TemporaryDrawer from './TemporaryDrawer';
const useStyles = makeStyles((theme) => ({
root: {
@@ -31,89 +28,40 @@ const useStyles = makeStyles((theme) => ({
},
}));
// const theme = createMuiTheme({
// overrides: {
// MuiAppBar: {
// colorPrimary: { backgroundColor: '#FFC0CB' },
// },
// },
// palette: { type: 'dark' },
// });
export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { title, action, override } = useContext(NavBarContext);
const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div className={classes.root}>
<AppBar position="static" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(true)}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton> */}
{/* <Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => { setDarkTheme(true); handleClose(); }}
<>
{override.status && override.value}
{!override.status
&& (
<div className={classes.root}>
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
disableRipple
onClick={() => setDrawerOpen(true)}
>
Dark Theme
</MenuItem>
<MenuItem
onClick={() => { setDarkTheme(false); handleClose(); }}
>
Light Theme
</MenuItem>
</Menu> */}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
</div>
)}
</>
);
}
+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
* 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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -27,68 +30,54 @@ interface IProps {
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
const classes = useStyles();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const sideList = (side: 'left') => (
<div
className={classes.list}
role="presentation"
onClick={() => setDrawerOpen(false)}
onKeyDown={() => setDrawerOpen(false)}
>
<List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
</Link>
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Sources" />
</ListItem>
</Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
</Link>
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Search">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Global Search" />
</ListItem>
</Link> */}
</List>
</div>
);
return (
<div>
<Drawer
BackdropProps={{ invisible: true }}
open={drawerOpen}
anchor="left"
onClose={() => setDrawerOpen(false)}
>
{sideList('left')}
<div
className={classes.list}
role="presentation"
onClick={() => setDrawerOpen(false)}
onKeyDown={() => setDrawerOpen(false)}
>
<List>
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
</Link>
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
</Link>
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Sources" />
</ListItem>
</Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
</Link>
</List>
</div>
</Drawer>
</div>
);
+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
* 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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -9,6 +12,8 @@ type ContextType = {
setTitle: React.Dispatch<React.SetStateAction<string>>
action: any
setAction: React.Dispatch<React.SetStateAction<any>>
override: INavbarOverride
setOverride: React.Dispatch<React.SetStateAction<INavbarOverride>>
};
const NavBarContext = React.createContext<ContextType>({
@@ -16,6 +21,8 @@ const NavBarContext = React.createContext<ContextType>({
setTitle: ():void => {},
action: <div />,
setAction: ():void => {},
override: { status: false, value: <div /> },
setOverride: ():void => {},
});
export default NavBarContext;
+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
* 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
* 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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+10 -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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -35,9 +38,13 @@ function groupExtensions(extensions: IExtension[]) {
return result;
}
function extensionDefaultLangs() {
return [...defualtLangs(), 'all'];
}
export default function Extensions() {
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
useEffect(() => {
setTitle('Extensions');
@@ -76,7 +83,7 @@ export default function Extensions() {
<>
{
Object.entries(extensions).map(([lang, list]) => (
(['installed', ...shownLangs].indexOf(lang) !== -1
((['installed', ...shownLangs].indexOf(lang) !== -1 && (list as []).length > 0)
&& (
<React.Fragment key={lang}>
<h1 key={lang} style={{ marginLeft: 25 }}>
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+54 -11
View File
@@ -1,17 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, useState, useContext } from 'react';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { useParams } from 'react-router-dom';
import CircularProgress from '@material-ui/core/CircularProgress';
import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import LoadingPlaceholder from '../components/LoadingPlaceholder';
const useStyles = makeStyles((theme: Theme) => ({
root: {
[theme.breakpoints.up('md')]: {
display: 'flex',
},
},
chapters: {
listStyle: 'none',
padding: 0,
[theme.breakpoints.up('md')]: {
width: '50vw',
height: 'calc(100vh - 64px)',
overflowY: 'auto',
margin: 0,
},
},
loading: {
margin: '10px 0',
display: 'flex',
justifyContent: 'center',
},
}));
export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
const classes = useStyles();
const { setTitle } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
const { id } = useParams<{id: string}>();
@@ -33,16 +67,25 @@ export default function Manga() {
.then((data) => setChapters(data));
}, []);
const chapterCards = chapters.map((chapter) => (
<ol style={{ listStyle: 'none', padding: 0 }}>
<ChapterCard chapter={chapter} />
</ol>
));
const chapterCards = (
<LoadingPlaceholder
shouldRender={chapters.length > 0}
>
<ol className={classes.chapters}>
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
</ol>
</LoadingPlaceholder>
);
return (
<>
{manga && <MangaDetails manga={manga} />}
<div className={classes.root}>
<LoadingPlaceholder
shouldRender={manga !== undefined}
component={MangaDetails}
componentProps={{ manga }}
/>
{chapterCards}
</>
</div>
);
}
+91 -27
View File
@@ -1,57 +1,121 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
/* eslint-disable @typescript-eslint/no-unused-vars */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import Page from '../components/Page';
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
const style = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
backgroundColor: '#343a40',
} as React.CSSProperties;
const useStyles = (settings: IReaderSettings) => makeStyles({
reader: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: '0 auto',
},
loading: {
margin: '50px auto',
},
pageNumber: {
display: settings.showPageNumber ? 'block' : 'none',
position: 'fixed',
bottom: '50px',
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
width: '50px',
textAlign: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
borderRadius: '10px',
},
});
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 });
export default function Reader() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
const classes = useStyles(settings)();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [pageCount, setPageCount] = useState<number>(-1);
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>();
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
const [curPage, setCurPage] = useState<number>(0);
const { setOverride, setTitle } = useContext(NavbarContext);
useEffect(() => {
setOverride(
{
status: true,
value: (
<ReaderNavBar
settings={settings}
setSettings={setSettings}
manga={manga}
chapter={chapter}
curPage={curPage}
/>
),
},
);
// clean up for when we leave the reader
return () => setOverride({ status: false, value: <div /> });
}, [manga, chapter, settings, curPage, chapterIndex]);
useEffect(() => {
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
setTitle('Reader');
client.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
});
}, [chapterIndex]);
useEffect(() => {
setChapter(initialChapter);
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
.then((response) => response.data)
.then((data:IChapter) => {
setTitle(data.name);
setPageCount(data.pageCount);
setChapter(data);
});
}, []);
}, [chapterIndex]);
if (pageCount === -1) {
if (chapter.pageCount === -1) {
return (
<div style={style}>
<h3>wait</h3>
<div className={classes.loading}>
<CircularProgress thickness={5} />
</div>
);
}
const mapped = range(pageCount).map((index) => (
<div style={{ margin: '0 auto' }}>
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
</div>
));
return (
<div style={style}>
{mapped}
<div className={classes.reader}>
<div className={classes.pageNumber}>
{`${curPage + 1} / ${chapter.pageCount}`}
</div>
{range(chapter.pageCount).map((index) => (
<Page
key={index}
index={index}
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`}
setCurPage={setCurPage}
settings={settings}
/>
))}
</div>
);
}
+17 -6
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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -27,7 +30,7 @@ export default function SearchSingle() {
const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]);
const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [message, setMessage] = useState<string>('');
const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
@@ -50,7 +53,8 @@ export default function SearchSingle() {
} else {
setError(false);
setSearchTerm(value);
setMessage('');
setMangas([]);
setMessage('loading...');
}
}
}
@@ -60,6 +64,7 @@ export default function SearchSingle() {
client.get(`/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.data)
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
setMessage('');
if (data.mangaList.length > 0) {
setMangas([
...mangas,
@@ -86,12 +91,18 @@ export default function SearchSingle() {
return (
<>
<form className={classes.root} noValidate autoComplete="off">
<TextField inputRef={textInput} error={error} id="standard-basic" label="Search text.." />
<div className={classes.root}>
<TextField
inputRef={textInput}
error={error}
id="standard-basic"
label="Search text.."
onKeyDown={(e) => e.key === 'Enter' && processInput()}
/>
<Button variant="contained" color="primary" onClick={() => processInput()}>
Search
</Button>
</form>
</div>
{mangaGrid}
</>
);
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -13,7 +16,7 @@ export default function SourceMangas(props: { popular: boolean }) {
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>();
const [mangas, setMangas] = useState<IManga[]>([]);
const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);

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