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

|

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