Compare commits

...

107 Commits

Author SHA1 Message Date
Aria Moradi e6ba2a0066 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 13:53:03 +04:30
Aria Moradi 9b56ef7d82 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-28 13:50:33 +04:30
Aria Moradi dd442c6653 fix extensions not being installed correctly 2021-03-28 13:40:18 +04:30
Aria Moradi 3044317b09 Update README.md 2021-03-28 06:00:34 +04:30
Aria Moradi 3a1e1e01dc continues gap switch
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-28 04:03:46 +04:30
Aria Moradi a567701639 chapter image fix for bato.to 2021-03-28 03:43:52 +04:30
Aria Moradi 1802271358 fix chapters not being loaded correctly 2021-03-28 03:19:01 +04:30
Aria Moradi 9e649eef79 fix a bug where add to library didn't work 2021-03-28 02:43:09 +04:30
Aria Moradi 1eb4a9c216 use logger to print exception 2021-03-28 02:11:40 +04:30
Aria Moradi e3f65d2192 fix URI exception 2021-03-28 01:35:46 +04:30
Aria Moradi bb09cfddb3 Migrate from Oracle Nashorn to Mozilla's Rhino 2021-03-28 01:15:50 +04:30
Aria Moradi d383939c9f improve loading spinner's place 2021-03-28 01:12:32 +04:30
Aria Moradi 32dd543562 Loading placeholder & css fixes 2021-03-27 22:21:39 +04:30
Aria Moradi 5a75f26791 even better logging messages 2021-03-27 19:40:47 +04:30
Aria Moradi 95c437efd5 better logging 2021-03-27 18:53:29 +04:30
Aria Moradi ec877f632f refactor & config improvments 2021-03-27 18:30:36 +04:30
Aria Moradi 8666cbf8bc lint some files 2021-03-26 12:15:14 +04:30
Aria Moradi 84b0c26450 change the copyright owner to Contributors to the Suwayomi project 2021-03-26 04:17:02 +04:30
Aria Moradi 64e5bbabb3 Update enters 2021-03-26 02:40:20 +04:30
Aria Moradi cc1a15e5ba Update enters 2021-03-26 02:39:54 +04:30
Aria Moradi d29e942a72 Update getAndroid.sh 2021-03-26 02:33:24 +04:30
Aria Moradi 8d86c88c38 Update getAndroid.ps1 2021-03-26 02:33:03 +04:30
Aria Moradi c7dc7421aa Update getAndroid.sh 2021-03-26 02:32:51 +04:30
Aria Moradi 34ed3e5c68 Update getAndroid.ps1 2021-03-26 02:29:31 +04:30
Aria Moradi 1a4a8af384 Merge pull request #42 from Syer10/getandroid
getAndroid.ps1: Silently continue if it cant remove the tmp folder
2021-03-26 02:21:43 +04:30
Aria Moradi 62b1e99bbf get the right dex2jar package please 2021-03-26 02:07:00 +04:30
Syer10 1aa3b76934 getAndroid.ps1: Silently continue if it cant remove the tmp folder 2021-03-25 16:47:30 -04:00
Aria Moradi 3e53c50f64 better spelling 2021-03-25 18:32:31 +04:30
Aria Moradi 430386bc84 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-03-25 18:21:57 +04:30
Aria Moradi 30049e8152 Update README.md 2021-03-25 18:21:14 +04:30
Aria Moradi 34d9a7a233 better use of dex2jar, get dex2jar from jitpack 2021-03-25 18:03:32 +04:30
Aria Moradi 183972475b better loging 2021-03-25 16:53:36 +04:30
Aria Moradi fd46727f8e Merge pull request #40 from Syer10/Powerhsell
Create getAndroid.ps1
2021-03-25 05:07:03 +04:30
Syer10 f6ce010aa2 Update readme 2021-03-24 20:34:32 -04:00
Syer10 d0ff30df9f Create getAndroid.ps1 2021-03-24 20:28:34 -04:00
Aria Moradi 8e449abd67 Merge pull request #39 from Syer10/LF
Standardize Line endings
2021-03-25 04:53:22 +04:30
Syer10 2986130268 Standardize Line endings 2021-03-24 19:51:18 -04:00
Aria Moradi 1c0c09f2f2 add server Desktop Environment 2021-03-25 04:03:48 +04:30
Aria Moradi 44100cb5b6 closes #37 2021-03-25 01:03:51 +04:30
Aria Moradi cfc6e5cd2a Fix for jip's Arch Linux 2021-03-24 19:28:27 +04:30
Aria Moradi c067d14c2c Update README.md 2021-03-24 19:26:37 +04:30
Aria Moradi aded854a2b Update README.md 2021-03-24 19:24:50 +04:30
Aria Moradi e79d0b9dd2 search loading message 2021-03-24 03:31:38 +04:30
Aria Moradi a0115d88b0 fix search bugs 2021-03-24 03:28:02 +04:30
Aria Moradi 85ec2ed367 drawer hide on click outside of it 2021-03-23 04:28:23 +04:30
Aria Moradi bf908c4d17 chapter prev/next UI+Backend 2021-03-23 03:50:55 +04:30
Aria Moradi f41c5c9428 bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 16s
2021-03-19 14:55:48 +03:30
Aria Moradi 04837983fa reader ui changes 2021-03-19 14:52:20 +03:30
Aria Moradi 5d484b012c new layout for manga page for >md 2021-03-18 22:55:17 +03:30
Aria Moradi 436a8d0585 improvments on the reader 2021-03-18 21:46:24 +03:30
Aria Moradi 28cc0a6f84 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-17 19:22:44 +03:30
Aria Moradi 26cc2f2c96 MangaDetails component improved drastically 2021-03-17 19:17:03 +03:30
Aria Moradi 149107e749 fix material error 2021-03-16 23:42:51 +03:30
Aria Moradi a74936c5f5 Update README.md 2021-03-16 23:24:14 +03:30
Aria Moradi ff8c8913d4 Update README.md 2021-03-16 23:14:36 +03:30
Aria Moradi 83426e1302 Update README.md 2021-03-16 23:13:44 +03:30
Aria Moradi 9cd93d467c bump version
Publish / Validate Gradle Wrapper (push) Successful in 11s
Publish / Build FatJar (push) Failing after 17s
2021-03-16 16:15:20 +03:30
Aria Moradi 257f8a5a27 fix extensions not showing the all pesudo-language 2021-03-16 16:10:06 +03:30
Aria Moradi 79bab08cae improvements 2021-03-16 16:04:29 +03:30
Aria Moradi 4e699e4f5a update dex2jar 2021-03-16 15:44:50 +03:30
Aria Moradi 1128f40bac closes #32 2021-03-14 23:57:33 +03:30
Aria Moradi 53ef836326 fix windows path 2021-03-14 20:28:23 +03:30
Aria Moradi b8df0e89e5 Don't show installed if nothing is installed 2021-03-14 14:09:31 +03:30
Aria Moradi 472bfec6bf improve docs 2021-03-14 01:26:52 +03:30
Aria Moradi c1b86cedd2 move getAndroid.sh 2021-03-14 01:02:43 +03:30
Aria Moradi 428c65f075 Enforce more limits on the issue format. 2021-03-13 22:59:37 +03:30
Aria Moradi 92ed48f7f6 bump version to v0.2.4
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 15s
2021-03-13 11:08:39 +03:30
Aria Moradi 13e84bc492 Maskable icons 2021-03-13 11:06:22 +03:30
Aria Moradi 0ef86c34b7 server configuration fam 2021-03-11 14:43:29 +03:30
Aria Moradi 7e1a4259d7 fix langs not showing correctly 2021-03-09 18:05:34 +03:30
Aria Moradi c842c51fb6 section sources by lang 2021-03-09 16:44:09 +03:30
Aria Moradi 6f2f228e08 section extension languages 2021-03-08 21:04:42 +03:30
Aria Moradi c78eaa8b96 add issue closer 2021-03-08 13:47:58 +03:30
Aria Moradi f9606526d2 add issue closer 2021-03-08 13:39:25 +03:30
Aria Moradi fe4cc9ea2c add issue closer 2021-03-08 13:32:17 +03:30
Aria Moradi 54d0c05fcc add issue closer 2021-03-08 13:31:03 +03:30
Aria Moradi 2f7df73a37 add issue closer 2021-03-08 13:22:44 +03:30
Aria Moradi cf19f3626b improve text 2021-03-08 13:01:23 +03:30
Aria Moradi ff2da5e59b issue template 2021-03-08 12:57:12 +03:30
Aria Moradi e03922e518 fix PWA icons 2021-03-07 23:08:30 +03:30
Aria Moradi 893fba5b8c fix image urls 2021-03-07 22:35:27 +03:30
Aria Moradi c1786f8e24 migrate to axios, front-end part of configurable ServerAddress 2021-03-07 22:25:29 +03:30
Aria Moradi a59f974537 fix #25 2021-03-07 22:12:38 +03:30
Aria Moradi 7157e07328 better messages, axios client 2021-03-07 16:27:13 +03:30
Aria Moradi 954084bd82 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-07 10:51:24 +03:30
Aria Moradi 0915ba40f6 🤌 Tachidesk's logo! 2021-02-25 21:54:49 +03:30
Aria Moradi de30d55bcf darkTheme in localStorage 2021-02-25 14:38:16 +03:30
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
Aria Moradi 648b8e5960 bump version: v0.2.2
Publish / Validate Gradle Wrapper (push) Successful in 15s
Publish / Build FatJar (push) Failing after 16s
2021-02-21 04:42:33 +03:30
Aria Moradi ce545b1fd5 fix some bugs 2021-02-21 04:41:56 +03:30
Aria Moradi 9151034fbc category done! 2021-02-21 04:27:41 +03:30
Aria Moradi 312a8baa13 hide menu button for now 2021-02-20 02:59:32 +03:30
Aria Moradi 18b6168cd1 theme select in settings 2021-02-20 02:57:52 +03:30
Aria Moradi 9a282c3bf4 redirect / to library 2021-02-20 02:41:30 +03:30
Aria Moradi 2bbebe4c30 fix removing manga from library not working 2021-02-20 02:34:26 +03:30
Aria Moradi 162961b560 fix tabs 2021-02-20 02:28:55 +03:30
Aria Moradi f1cc37d0db finished the category screen 2021-02-20 01:23:52 +03:30
129 changed files with 4321 additions and 1329 deletions
+26 -5
View File
@@ -1,6 +1,27 @@
# * text=auto
# https://help.github.com/articles/dealing-with-line-endings/ * text eol=lf
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
# Windows forced line-endings
/.idea/* text eol=crlf
*.bat text eol=crlf
*.ps1 text eol=crlf
# Gradle wrapper
*.jar binary
# Images
*.webp binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.swp binary
*.pdf binary
+44
View File
@@ -0,0 +1,44 @@
---
name: "🐞 Bug report"
title: "[Bug] <short description>"
about: "Report a bug"
labels: "bug"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## Device information
- Tachidesk version: (Example: v0.2.3-r255-win32)
- Server Operating System: (Example: Ubuntu 20.04)
- Server Desktop Environment: N/A or (Example: Gnome 40)
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
- Client Operating System: <usually the same as above Server Operating System>
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
## Steps to reproduce
1. First Step
2. Second Step
### Expected behavior
Describe what should have happened. Remove this line after you are done.
### Actual behavior
Describe what happens instead. Remove this line after you are done.
## Other details
Describe additional details If necessary. Remove this line after you are done.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+29
View File
@@ -0,0 +1,29 @@
---
name: "🌟 Feature request"
title: "[Feature Request] <short description>"
about: "Suggest a feature to improve the project"
labels: "enhancement"
---
**PLEASE READ THIS**
I acknowledge that:
- I have updated to the latest version of the app.
- I have tried the troubleshooting guide described in `README.md`
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
- I will fill out the title and the information in this template
Note that the issue will be automatically closed if you do not fill out the title or requested information.
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
---
## What feature should be added to Tachidesk?
Explain What the feature is and how it should work in detail. Remove this line after you are done.
## Why/Project's Benefit/Existing Problem
Explain why this should be added. Remove this line after you are done.
+37
View File
@@ -0,0 +1,37 @@
name: Issue closer
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
with:
repo-token: ${{ github.token }}
rules: |
[
{
"type": "title",
"regex": ".*<short description>*",
"message": "You did not fill out the description in the title"
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed"
},
{
"type": "body",
"regex": "(Tachidesk version|Server Operating System|Server Desktop Environment|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
"message": "The requested information was not filled out"
},
{
"type": "body",
"regex": ".*Remove this line after you are done.*",
"message": "The lines requesting to be removed were not removed."
}
]
+1 -1
View File
@@ -1,4 +1,4 @@
dependencies { dependencies {
// Config API // Config API, moved to the global build.gradle
// implementation("com.typesafe:config:1.4.0") // implementation("com.typesafe:config:1.4.0")
} }
@@ -1,57 +1,65 @@
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
import mu.KotlinLogging import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import java.io.File import java.io.File
/** /**
* Manages app config. * Manages app config.
*/ */
open class ConfigManager { open class ConfigManager {
private val generatedModules private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() } val config by lazy { loadConfigs() }
//Public read-only view of modules //Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule> val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules get() = generatedModules
open val configFolder: String open val appConfigFile: String = "$dataRoot/server.conf"
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
val logger = KotlinLogging.logger {} val logger = KotlinLogging.logger {}
/** /**
* Get a config module * Get a config module
*/ */
inline fun <reified T : ConfigModule> module(): T inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
= loadedModules[T::class.java] as T
/** /**
* Get a config module (Java API) * Get a config module (Java API)
*/ */
fun <T : ConfigModule> module(type: Class<T>): T fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
= loadedModules[type] as T
/** /**
* Load configs * Load configs
*/ */
fun loadConfigs(): Config { fun loadConfigs(): Config {
val configs = mutableListOf<Config>() //Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
//Load reference config //Load user config
configs += ConfigFactory.parseResources("reference.conf") val userConfig =
File(appConfigFile).let{
ConfigFactory.parseFile(it)
}
//Load custom configs from dir val config = ConfigFactory.empty()
File(configFolder).listFiles()?.map { .withFallback(userConfig)
ConfigFactory.parseFile(it) .withFallback(compatConfig)
}?.filterNotNull()?.forEach { .withFallback(serverConfig)
configs += it.withFallback(configs.last()) .resolve()
}
val config = configs.last().resolve()
logger.debug { logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true)) "Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
@@ -1,35 +0,0 @@
package xyz.nulldev.ts.config
import com.typesafe.config.Config
import java.io.File
class ServerConfig(config: Config) : ConfigModule(config) {
val ip = config.getString("ip")
val port = config.getInt("port")
val allowConfigChanges = config.getBoolean("allowConfigChanges")
val enableWebUi = config.getBoolean("enableWebUi")
val useOldWebUi = config.getBoolean("useOldWebUi")
val prettyPrintApi = config.getBoolean("prettyPrintApi")
// TODO Apply to operation IDs
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
val rootDir = registerFile(config.getString("rootDir"))
val patchesDir = registerFile(config.getString("patchesDir"))
fun registerFile(file: String): File {
return File(file).apply {
mkdirs()
}
}
companion object {
fun register(config: Config)
= ServerConfig(config.getConfig("ts.server"))
}
}
+8 -6
View File
@@ -18,9 +18,7 @@ repositories {
dependencies { dependencies {
// Android stub library // Android stub library
// compileOnly( fileTree(File(rootProject.rootDir, "libs/android"), include: "*.jar")
implementation(fileTree("lib/")) implementation(fileTree("lib/"))
implementation(fileTree("${rootProject.rootDir}/server/lib/dex2jar/"))
// Android JAR libs // Android JAR libs
@@ -41,10 +39,10 @@ dependencies {
compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1") compileOnly( group= "xmlpull", name= "xmlpull", version= "1.1.3.1")
// Config API // Config API
implementation( project(":AndroidCompat:Config")) implementation(project(":AndroidCompat:Config"))
// dex2jar // dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
// compileOnly( "dex2jar:dex-translator") compileOnly("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
// APK parser // APK parser
compileOnly("net.dongliu:apk-parser:2.6.10") compileOnly("net.dongliu:apk-parser:2.6.10")
@@ -55,7 +53,11 @@ dependencies {
// AndroidX annotations // AndroidX annotations
compileOnly( "androidx.annotation:annotation:1.2.0-alpha01") compileOnly( "androidx.annotation:annotation:1.2.0-alpha01")
// compileOnly("io.reactivex:rxjava:1.3.8") // substitute for duktape-android
// 'org.mozilla:rhino' includes some code that we don't need so use 'org.mozilla:rhino-runtime' instead
implementation("org.mozilla:rhino-runtime:1.7.13")
// 'org.mozilla:rhino-engine' provides the same interface as 'javax.script' a.k.a Nashorn
implementation("org.mozilla:rhino-engine:1.7.13")
} }
//def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar') //def fatJarTask = tasks.getByPath(':AndroidCompat:JVMPatch:fatJar')
+99
View File
@@ -0,0 +1,99 @@
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# This is a windows only PowerShell script to create android.jar stubs
# foolproof against running from AndroidCompat dir instead of running from project root
if ($(Split-Path -Path (Get-Location) -Leaf) -eq "AndroidCompat" ) {
Set-Location ..
}
Write-Output "Getting required Android.jar..."
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
$android_jar = (Get-Location).Path + "\tmp\android.jar"
[IO.File]::WriteAllBytes($android_jar, [Convert]::FromBase64String($androidEncoded))
# We need to remove any stub classes that we have implementations for
Write-Output "Patching JAR..."
function Remove-Files-Zip($zipfile, $path)
{
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') | Out-Null
$stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open)
$mode = [IO.Compression.ZipArchiveMode]::Update
$zip = New-Object IO.Compression.ZipArchive($stream, $mode)
($zip.Entries | Where-Object { $_.FullName -like $path }) | ForEach-Object { Write-Output "Deleting: $($_.FullName)"; $_.Delete() }
$zip.Dispose()
$stream.Close()
$stream.Dispose()
}
Write-Output "Removing org.json..."
Remove-Files-Zip $android_jar 'org/json/*'
Write-Output "Removing org.apache..."
Remove-Files-Zip $android_jar 'org/apache/*'
Write-Output "Removing org.w3c..."
Remove-Files-Zip $android_jar 'org/w3c/*'
Write-Output "Removing org.xml..."
Remove-Files-Zip $android_jar 'org/xml/*'
Write-Output "Removing org.xmlpull..."
Remove-Files-Zip $android_jar 'org/xmlpull/*'
Write-Output "Removing junit..."
Remove-Files-Zip $android_jar 'junit/*'
Write-Output "Removing javax..."
Remove-Files-Zip $android_jar 'javax/*'
Write-Output "Removing java..."
Remove-Files-Zip $android_jar 'java/*'
Write-Output "Removing overriden classes..."
Remove-Files-Zip $android_jar 'android/app/Application.class'
Remove-Files-Zip $android_jar 'android/app/Service.class'
Remove-Files-Zip $android_jar 'android/net/Uri.class'
Remove-Files-Zip $android_jar 'android/net/Uri$Builder.class'
Remove-Files-Zip $android_jar 'android/os/Environment.class'
Remove-Files-Zip $android_jar 'android/text/format/Formatter.class'
Remove-Files-Zip $android_jar 'android/text/Html.class'
function Dedupe($path)
{
Push-Location $path
$classes = Get-ChildItem . *.* -Recurse | Where-Object { !$_.PSIsContainer }
$classes | ForEach-Object {
"Processing class: $($_.FullName)"
Remove-Files-Zip $android_jar "$($_.Name).class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)$*.class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)Kt.class" | Out-Null
Remove-Files-Zip $android_jar "$($_.Name)Kt$*.class" | Out-Null
}
Pop-Location
}
Dedupe "AndroidCompat/src/main/java"
Dedupe "server/src/main/java"
Dedupe "server/src/main/kotlin"
Write-Output "Copying Android.jar to library folder..."
Move-Item -Force $android_jar "AndroidCompat/lib/android.jar"
Write-Output "Cleaning up..."
Remove-Item -Recurse -Force "tmp"
Write-Output "Done!"
@@ -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,6 +1,3 @@
# Server ip and port bindings
ts.server.ip = 0.0.0.0
ts.server.port = 4567
# Allow/disallow preference changes (useful for demos) # Allow/disallow preference changes (useful for demos)
ts.server.allowConfigChanges = true ts.server.allowConfigChanges = true
+66 -32
View File
@@ -1,3 +1,5 @@
![image](https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png)
# Tachidesk # Tachidesk
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/). A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
@@ -5,35 +7,80 @@ Tachidesk is as multi-platform as you can get. Any platform that runs java and/o
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature. Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
## How do I run the app? ## Is this application usable? Should I test it?
#### Prerequisites Here is a list of current features:
You should have The Java Runtime Environment(JRE) 8 or newer (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
#### Download the app - Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases). - A library to save your mangas and categories to put them into.
- Searching and browsing installed sources.
- A decent chapter reader.
- Ability to download Mangas for offline read(This partially works)
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
Anyways, for more info checkout [finished milestone #1](https://github.com/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
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
Download the latest jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
#### Running pre-built jar packages
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk. 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
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Support and help
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## 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)
@@ -41,23 +88,10 @@ How to do it is described in `webUI/react/README.md` but for short,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
and supports HMR and all the other goodies you'll need. and supports HMR and all the other goodies you'll need.
## Is this application usable? Should I test it?
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
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.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## 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`.
@@ -67,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
+6 -1
View File
@@ -61,7 +61,7 @@ configure(listOf(
// Logging // Logging
implementation("org.slf4j:slf4j-api:1.7.30") implementation("org.slf4j:slf4j-api:1.7.30")
implementation("org.slf4j:slf4j-simple:1.7.30") implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.3") implementation("io.github.microutils:kotlin-logging:2.0.3")
// RxJava // RxJava
@@ -76,5 +76,10 @@ 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
implementation("net.harawata:appdirs:1.2.0")
} }
} }
+7 -13
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9" id("edu.sc.seis.launch4j") version "2.4.9"
} }
val TachideskVersion = "v0.2.1" val TachideskVersion = "v0.2.7"
repositories { repositories {
@@ -57,24 +57,19 @@ 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")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
// Exposed ORM // Exposed ORM
val exposed_version = "0.28.1" val exposed_version = "0.28.1"
implementation("org.jetbrains.exposed:exposed-core:$exposed_version") implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
@@ -90,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"
@@ -149,7 +143,7 @@ launch4j { //used for windows
bundledJre64Bit = true bundledJre64Bit = true
jreMinVersion = "8" jreMinVersion = "8"
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32" outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
icon = "${projectDir}/src/main/resources/icon/icon_round.ico" icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar" jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
} }
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,14 +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 net.harawata.appdirs.AppDirsFactory
object Config {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaRoot = "$dataRoot/manga"
}
@@ -1,271 +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 eu.kanade.tachiyomi.App import ir.armor.tachidesk.server.applicationSetup
import io.javalin.Javalin import ir.armor.tachidesk.server.javalinSetup
import ir.armor.tachidesk.util.addMangaToCategory
import ir.armor.tachidesk.util.addMangaToLibrary
import ir.armor.tachidesk.util.applicationSetup
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.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.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.systemTray
import ir.armor.tachidesk.util.updateCategory
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
class Main { class Main {
companion object { companion object {
val androidCompat by lazy { AndroidCompat() }
fun registerConfigModules() {
GlobalConfigManager.registerModules(
// ServerConfig.register(GlobalConfigManager.config),
// SyncConfigModule.register(GlobalConfigManager.config)
)
}
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
// System.getProperties()["proxySet"] = "true"
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
// System.getProperties()["socksProxyPort"] = "2020"
// make sure everything we need exists
applicationSetup() applicationSetup()
val tray = systemTray() javalinSetup()
registerConfigModules()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
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(4567)
if (hasWebUiBundled) {
openInBrowser()
}
// app.before() { ctx ->
// // allow the client which is running on another port
// ctx.header("Access-Control-Allow-Origin", "*")
// }
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()
println("fuck")
removeMangaFromLibrary(mangaId)
ctx.status(200)
}
// 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 all manga in the library, suitable if no categories are defined
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")!!
val isLanding = ctx.formParam("isLanding", "false").toBoolean()
createCategory(name, isLanding)
ctx.status(200)
}
// category modification
app.put("/api/v1/category/:categoryId") { ctx ->
val categoryId = ctx.pathParam("categoryId").toInt()
val name = ctx.formParam("name")!!
val isLanding = ctx.formParam("isLanding").toBoolean()
updateCategory(categoryId, name, isLanding)
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,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.Config
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,29 +14,31 @@ 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
object DBMangaer { object DBMangaer {
val db by lazy { val db by lazy {
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver") Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
} }
} }
fun makeDataBaseTables() { fun makeDataBaseTables() {
// mention db object to connect // must mention db object so the lazy block executes
DBMangaer.db val db = DBMangaer.db
// val db = DBMangaer.db db.useNestedTransactions = true
// db.useNestedTransactions = true
transaction { transaction {
SchemaUtils.create(ExtensionTable) SchemaUtils.createMissingTablesAndColumns(
SchemaUtils.create(SourceTable) ExtensionTable,
SchemaUtils.create(MangaTable) SourceTable,
SchemaUtils.create(ChapterTable) MangaTable,
SchemaUtils.create(PageTable) ChapterTable,
SchemaUtils.create(CategoryTable) PageTable,
SchemaUtils.create(CategoryMangaTable) CategoryTable,
CategoryMangaTable,
)
} }
} }
@@ -1,10 +1,15 @@
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 CategoryDataClass( data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String, val name: String,
val isLanding: Boolean val isLanding: Boolean
) )
@@ -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/. */
@@ -11,9 +14,12 @@ import org.jetbrains.exposed.sql.ResultRow
object CategoryTable : IntIdTable() { object CategoryTable : IntIdTable() {
val name = varchar("name", 64) val name = varchar("name", 64)
val isLanding = bool("is_landing").default(false) val isLanding = bool("is_landing").default(false)
val order = integer("order").default(0)
} }
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
categoryEntry[CategoryTable.id].value,
categoryEntry[CategoryTable.order],
categoryEntry[CategoryTable.name], categoryEntry[CategoryTable.name],
categoryEntry[CategoryTable.isLanding], categoryEntry[CategoryTable.isLanding],
) )
@@ -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
@@ -25,15 +28,16 @@ object MangaTable : IntIdTable() {
val thumbnail_url = varchar("thumbnail_url", 2048).nullable() val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
val inLibrary = bool("in_library").default(false) val inLibrary = bool("in_library").default(false)
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/. */
@@ -0,0 +1,69 @@
package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryMangaTable
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
fun createCategory(name: String) {
transaction {
val count = CategoryTable.selectAll().count()
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert {
it[CategoryTable.name] = name
it[CategoryTable.order] = count.toInt() + 1
}
}
}
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
}
}
}
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
transaction {
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
it[CategoryTable.order] = index + 1
}
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,18 +1,25 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.impl
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.dataclass.MangaDataClass 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.CategoryTable
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.SortOrder
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.insert import org.jetbrains.exposed.sql.insert
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
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 {
@@ -21,6 +28,10 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
it[CategoryMangaTable.category] = categoryId it[CategoryMangaTable.category] = categoryId
it[CategoryMangaTable.manga] = mangaId it[CategoryMangaTable.manga] = mangaId
} }
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = false
}
} }
} }
} }
@@ -28,6 +39,11 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) { fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
transaction { transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) } CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
MangaTable.update({ MangaTable.id eq mangaId }) {
it[MangaTable.defaultCategory] = true
}
}
} }
} }
@@ -38,3 +54,11 @@ fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
} }
} }
} }
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,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
}
}
} }
} }
@@ -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.Config
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,14 +34,51 @@ 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 = "${Config.extensionsRoot}/$fileNameWithoutType" val dirPathWithoutType = "${applicationDirs.extensionsRoot}/$fileNameWithoutType"
// check if we don't have the dex file already downloaded // check if we don't have the dex file already downloaded
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar" val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) { if (!File(jarPath).exists()) {
runBlocking { runBlocking {
val api = ExtensionGithubApi() val api = ExtensionGithubApi()
@@ -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,10 +168,12 @@ 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 jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar" val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
val fileNameWithoutType = apkName.substringBefore(".apk")
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]
@@ -157,10 +193,9 @@ val network: NetworkHelper by injectLazy()
fun getExtensionIcon(apkName: String): Pair<InputStream, String> { 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 = "${Config.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()
@@ -168,5 +203,5 @@ fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
} }
fun getExtensionIconUrl(apkName: String): String { fun getExtensionIconUrl(apkName: String): String {
return "http://127.0.0.1:4567/api/v1/extension/icon/$apkName" return "/api/v1/extension/icon/$apkName"
} }
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -9,12 +12,15 @@ import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
import ir.armor.tachidesk.database.table.ExtensionTable import ir.armor.tachidesk.database.table.ExtensionTable
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
private val logger = KotlinLogging.logger {}
private object Data { private object Data {
var lastExtensionCheck: Long = 0 var lastExtensionCheck: Long = 0
} }
@@ -28,7 +34,7 @@ private fun extensionDatabaseIsEmtpy(): Boolean {
fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> { fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
// update if 60 seconds has passed or requested offline and database is empty // update if 60 seconds has passed or requested offline and database is empty
if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) { if (Data.lastExtensionCheck + 60 * 1000 < System.currentTimeMillis() || (offline && extensionDatabaseIsEmtpy())) {
println("Getting extensions list from the internet") logger.debug("Getting extensions list from the internet")
Data.lastExtensionCheck = System.currentTimeMillis() Data.lastExtensionCheck = System.currentTimeMillis()
var foundExtensions: List<Extension.Available> var foundExtensions: List<Extension.Available>
runBlocking { runBlocking {
@@ -66,7 +72,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
} }
} }
} else { } else {
println("used cached extension list") logger.debug("used cached extension list")
} }
return transaction { return transaction {
@@ -1,16 +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.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.and
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) {
@@ -24,18 +30,20 @@ fun addMangaToLibrary(mangaId: Int) {
fun removeMangaFromLibrary(mangaId: Int) { fun removeMangaFromLibrary(mangaId: Int) {
val manga = getManga(mangaId) val manga = getManga(mangaId)
if (!manga.inLibrary) { if (manga.inLibrary) {
transaction { transaction {
MangaTable.update({ MangaTable.id eq manga.id }) { MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = false it[inLibrary] = false
it[defaultCategory] = true
} }
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
} }
} }
} }
fun getLibraryMangas(): List<MangaDataClass> { fun getLibraryMangas(): List<MangaDataClass> {
return transaction { return transaction {
MangaTable.select { MangaTable.inLibrary eq true }.map { MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
MangaTable.toDataClass(it) MangaTable.toDataClass(it)
} }
} }
@@ -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.Config
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,18 +82,19 @@ 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])
) )
} }
} }
fun getThumbnail(mangaId: Int): Pair<InputStream, String> { fun getThumbnail(mangaId: 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 saveDir = Config.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()) {
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -1,6 +1,9 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.impl
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -14,7 +17,7 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
fun proxyThumbnailUrl(mangaId: Int): String { fun proxyThumbnailUrl(mangaId: Int): String {
return "http://127.0.0.1:4567/api/v1/manga/$mangaId/thumbnail" return "/api/v1/manga/$mangaId/thumbnail"
} }
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
@@ -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,
@@ -1,17 +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.Config
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 org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 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
@@ -26,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(
@@ -50,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()!! }
@@ -70,7 +78,7 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaTitle = mangaEntry[MangaTable.title] val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString() val sourceName = source.toString()
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir" val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist // make sure dirs exist
File(mangaDir).mkdirs() File(mangaDir).mkdirs()
return mangaDir return mangaDir
@@ -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(
@@ -1,54 +1,61 @@
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.Config
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
val className = extensionRecord.classFQName val className = extensionRecord.classFQName
val jarName = apkName.substringBefore(".apk") + ".jar" val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${Config.extensionsRoot}/$jarName" val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
println(jarName)
val cachedExtensionPair = extensionCache.firstOrNull { it.first == jarPath } 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()
@@ -77,7 +84,7 @@ fun getSourceList(): List<SourceDataClass> {
SourceDataClass( SourceDataClass(
it[SourceTable.id].value.toString(), it[SourceTable.id].value.toString(),
it[SourceTable.name], it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])), it[SourceTable.lang],
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]), getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
getHttpSource(it[SourceTable.id].value).supportsLatest getHttpSource(it[SourceTable.id].value).supportsLatest
) )
@@ -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 "";
// }
// }
@@ -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"))
}
}
@@ -0,0 +1,101 @@
package ir.armor.tachidesk.server
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ch.qos.logback.classic.Level
import eu.kanade.tachiyomi.App
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import ir.armor.tachidesk.server.util.systemTray
import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File
private val logger = KotlinLogging.logger {}
object applicationDirs {
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
val extensionsRoot = "$dataRoot/extensions"
val thumbnailsRoot = "$dataRoot/thumbnails"
val mangaRoot = "$dataRoot/manga"
}
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTray by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() {
// register server config
GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config)
)
// set application wide logging level
if (serverConfig.debugLogsEnabled) {
(mu.KotlinLogging.logger(org.slf4j.Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
}
// make dirs we need
listOf(
applicationDirs.dataRoot,
applicationDirs.extensionsRoot,
"${applicationDirs.extensionsRoot}/icon",
applicationDirs.thumbnailsRoot
).forEach {
File(it).mkdirs()
}
// create conf file if doesn't exist
try {
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
if (!dataConfFile.exists()) {
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
dataConfFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
} catch (e: Exception) {
logger.error("Exception while creating initial server.conf:\n", e)
}
makeDataBaseTables()
// create system tray
if (serverConfig.systemTrayEnabled)
try {
systemTray
} catch (e: Exception) {
e.printStackTrace()
}
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
// Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false")
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog")
System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
}
@@ -0,0 +1,71 @@
package ir.armor.tachidesk.server.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 dorkbox.systemTray.MenuItem
import dorkbox.systemTray.SystemTray
import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.server.serverConfig
import java.awt.event.ActionListener
import java.io.IOException
fun openInBrowser() {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
}
}
fun systemTray(): SystemTray? {
try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = serverConfig.debugLogsEnabled
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e: IOException) {
e.printStackTrace()
}
}
)
)
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
@@ -1,48 +0,0 @@
package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
import ir.armor.tachidesk.database.table.CategoryTable
import ir.armor.tachidesk.database.table.toDataClass
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
fun createCategory(name: String, isLanding: Boolean) {
transaction {
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
CategoryTable.insert {
it[CategoryTable.name] = name
it[CategoryTable.isLanding] = isLanding
}
}
}
fun updateCategory(categoryId: Int, name: String, isLanding: Boolean) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
it[CategoryTable.name] = name
it[CategoryTable.isLanding] = isLanding
}
}
}
fun removeCategory(categoryId: Int) {
transaction {
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
fun getCategoryList(): List<CategoryDataClass> {
return transaction {
CategoryTable.selectAll().map {
CategoryTable.toDataClass(it)
}
}
}
@@ -1,71 +0,0 @@
package ir.armor.tachidesk.util
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import dorkbox.systemTray.MenuItem
import dorkbox.systemTray.SystemTray
import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import java.awt.event.ActionListener
import java.io.File
import java.io.IOException
fun applicationSetup() {
// make dirs we need
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
File("${Config.extensionsRoot}/icon").mkdirs()
File(Config.thumbnailsRoot).mkdirs()
makeDataBaseTables()
}
fun openInBrowser() {
Desktop.browseURL("http://127.0.0.1:4567")
}
val icon = Main::class.java.getResource("/icon/icon_round.png")
fun systemTray(): SystemTray? {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
}
}
)
)
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+16
View File
@@ -0,0 +1,16 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger - %msg%n</pattern>
</encoder>
</appender>
<logger name="Exposed" level="ERROR"/>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
@@ -0,0 +1,13 @@
# Server ip and port bindings
server.ip = "0.0.0.0"
server.port = 4567
# Socks5 proxy
server.socksProxy = false
server.socksProxyHost = ""
server.socksProxyPort = ""
# misc
server.debugLogsEnabled = false
server.systemTrayEnabled = true
server.initialOpenInBrowserEnabled = true
@@ -1,13 +0,0 @@
/*
* This Kotlin source file was generated by the Gradle 'init' task.
*/
package ir.armor.tachidesk
import kotlin.test.Test
import kotlin.test.assertTrue
class AppTest {
@Test fun testAppHasAGreeting() {
assertTrue(true)
}
}
+2
View File
@@ -13,5 +13,7 @@ module.exports = {
// Indent props with 4 spaces // Indent props with 4 spaces
'react/jsx-indent-props': ['error', 4], 'react/jsx-indent-props': ['error', 4],
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
}, },
}; };
+1
View File
@@ -1,3 +1,4 @@
node_modules/ node_modules/
.eslintcache .eslintcache
.vscode .vscode
.env
+4
View File
@@ -8,9 +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",
"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-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"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+3 -3
View File
@@ -7,9 +7,9 @@
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="A manga reader that runs tachiyomi's extensions"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/> <link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Tachidesk</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

+12 -6
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "Tachidesk",
"name": "Create React App Sample", "name": "Tachidesk",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -8,18 +8,24 @@
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "favicon.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "favicon.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#ff2323",
"background_color": "#ffffff" "background_color": "#ff2323"
} }
+50 -19
View File
@@ -1,31 +1,42 @@
/* 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 React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
BrowserRouter as Router, Route, Switch, BrowserRouter as Router, Redirect, Route, Switch,
} from 'react-router-dom'; } from 'react-router-dom';
import { Container } from '@material-ui/core'; import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import Home from './screens/Home';
import Sources from './screens/Sources'; import Sources from './screens/Sources';
import Extensions from './screens/Extensions'; import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/SearchSingle'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle'; import NavbarContext from './context/NavbarContext';
import DarkTheme from './context/DarkTheme'; import DarkTheme from './context/DarkTheme';
import Library from './screens/Library'; import Library from './screens/Library';
import Settings from './screens/Settings';
import Categories from './screens/settings/Categories';
import useLocalStorage from './util/useLocalStorage';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
const [darkTheme, setDarkTheme] = useState<boolean>(true); const [action, setAction] = useState<any>(<div />);
const navTitleContext = { title, setTitle }; const [override, setOverride] = useState<INavbarOverride>({ status: false, value: <div /> });
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction, override, setOverride,
};
const darkThemeContext = { darkTheme, setDarkTheme }; const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo( const theme = React.useMemo(
@@ -55,12 +66,15 @@ export default function App() {
return ( return (
<Router> <Router>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NavBarTitle.Provider value={navTitleContext}> <NavbarContext.Provider value={navBarContext}>
<CssBaseline /> <CssBaseline />
<DarkTheme.Provider value={darkThemeContext}> <NavBar />
<NavBar /> <Container
</DarkTheme.Provider> id="appMainContainer"
<Container maxWidth={false} disableGutters> maxWidth={false}
disableGutters
style={{ paddingTop: '64px' }}
>
<Switch> <Switch>
<Route path="/sources/:sourceId/search/"> <Route path="/sources/:sourceId/search/">
<Search /> <Search />
@@ -69,16 +83,16 @@ export default function App() {
<Extensions /> <Extensions />
</Route> </Route>
<Route path="/sources/:sourceId/popular/"> <Route path="/sources/:sourceId/popular/">
<MangaList popular /> <SourceMangas popular />
</Route> </Route>
<Route path="/sources/:sourceId/latest/"> <Route path="/sources/:sourceId/latest/">
<MangaList popular={false} /> <SourceMangas popular={false} />
</Route> </Route>
<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 />
@@ -86,12 +100,29 @@ export default function App() {
<Route path="/library"> <Route path="/library">
<Library /> <Library />
</Route> </Route>
<Route path="/"> <Route path="/settings/categories">
<Home /> <Categories />
</Route> </Route>
<Route path="/settings">
<DarkTheme.Provider value={darkThemeContext}>
<Settings />
</DarkTheme.Provider>
</Route>
<Route
exact
path="/"
render={() => (
<Redirect to="/library" />
)}
/>
</Switch> </Switch>
</Container> </Container>
</NavBarTitle.Provider> <Switch>
<Route path="/manga/:mangaId/chapter/:chapterIndex">
<Reader />
</Route>
</Switch>
</NavbarContext.Provider>
</ThemeProvider> </ThemeProvider>
</Router> </Router>
); );
@@ -0,0 +1,124 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, useState } from 'react';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
import client from '../util/client';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
open: boolean
setOpen: (value: boolean) => void
mangaId: number
}
interface ICategoryInfo {
category: ICategory
selected: boolean
}
export default function CategorySelect(props: IProps) {
const classes = useStyles();
const { open, setOpen, mangaId } = props;
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
let tmpCategoryInfos: ICategoryInfo[] = [];
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data: ICategory[]) => {
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
})
.then(() => {
client.get(`/api/v1/manga/${mangaId}/category/`)
.then((response) => response.data)
.then((data: ICategory[]) => {
data.forEach((category) => {
tmpCategoryInfos[category.order - 1].selected = true;
});
setCategoryInfos(tmpCategoryInfos);
});
});
}, [updateTriggerHolder, open]);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
const { checked } = event.target as HTMLInputElement;
const method = checked ? client.get : client.delete;
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
.then(() => triggerUpdate());
};
return (
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.length === 0
&& (
<span>
No categories found!
<br />
You should make some from settings.
</span>
)}
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
<Checkbox
checked={categoryInfo.selected}
onChange={(e) => handleChange(e, categoryInfo.category.id)}
color="default"
/>
)}
label={categoryInfo.category.name}
/>
))}
</FormGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
);
}
+20 -4
View File
@@ -1,4 +1,8 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* eslint-disable @typescript-eslint/no-unused-vars */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -8,6 +12,7 @@ import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent'; import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link, useHistory } from 'react-router-dom';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -41,6 +46,7 @@ interface IProps{
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const history = useHistory();
const { chapter } = props; const { chapter } = props;
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10); const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
@@ -63,9 +69,19 @@ export default function ChapterCard(props: IProps) {
</Typography> </Typography>
</div> </div>
</div> </div>
<div style={{ display: 'flex' }}> <Link
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { window.location.href = `/manga/${chapter.mangaId}/chapter/${chapter.id}`; }}>open</Button> to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
</div> style={{ textDecoration: 'none' }}
>
<Button
variant="outlined"
style={{ marginLeft: 20 }}
>
open
</Button>
</Link>
</CardContent> </CardContent>
</Card> </Card>
</li> </li>
+21 -8
View File
@@ -1,4 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -9,6 +12,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
@@ -38,6 +43,7 @@ const useStyles = makeStyles((theme) => ({
interface IProps { interface IProps {
extension: IExtension extension: IExtension
notifyInstall: () => void
} }
export default function ExtensionCard(props: IProps) { export default function ExtensionCard(props: IProps) {
@@ -45,24 +51,31 @@ export default function ExtensionCard(props: IProps) {
extension: { extension: {
name, lang, versionName, installed, apkName, iconUrl, name, lang, versionName, installed, apkName, iconUrl,
}, },
notifyInstall,
} = props; } = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install')); const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles(); const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase(); const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() { function install() {
setInstalledState('installing'); setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => { client.get(`/api/v1/extension/install/${apkName}`)
setInstalledState('uninstall'); .then(() => {
}); setInstalledState('uninstall');
notifyInstall();
});
} }
function uninstall() { function uninstall() {
setInstalledState('uninstalling'); setInstalledState('uninstalling');
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => { client.get(`/api/v1/extension/uninstall/${apkName}`)
setInstalledState('install'); .then(() => {
}); // setInstalledState('install');
notifyInstall();
});
} }
function handleButtonClick() { function handleButtonClick() {
@@ -81,7 +94,7 @@ export default function ExtensionCard(props: IProps) {
variant="rounded" variant="rounded"
className={classes.icon} className={classes.icon}
alt={name} alt={name}
src={iconUrl} src={serverAddress + iconUrl}
/> />
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
@@ -0,0 +1,108 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useState } from 'react';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Switch from '@material-ui/core/Switch';
import IconButton from '@material-ui/core/IconButton';
import FilterListIcon from '@material-ui/icons/FilterList';
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
import ListItem from '@material-ui/core/ListItem';
import { langCodeToName } from '../util/language';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
shownLangs: string[]
setShownLangs: (arg0: string[]) => void
allLangs: string[]
}
export default function ExtensionLangSelect(props: IProps) {
const { shownLangs, setShownLangs, allLangs } = props;
// hold a copy and only sate state on parent when OK pressed, improves performance
const [mShownLangs, setMShownLangs] = useState(shownLangs);
const classes = useStyles();
const [open, setOpen] = useState<boolean>(false);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
setShownLangs(mShownLangs);
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
const { checked } = event.target as HTMLInputElement;
if (checked) {
setMShownLangs([...mShownLangs, lang]);
} else {
const clone = JSON.parse(JSON.stringify(mShownLangs));
clone.splice(clone.indexOf(lang), 1);
setMShownLangs(clone);
}
};
return (
<>
<IconButton
onClick={() => setOpen(true)}
aria-label="display more actions"
edge="end"
color="inherit"
>
<FilterListIcon />
</IconButton>
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Enabled Languages</DialogTitle>
<DialogContent dividers style={{ padding: 0 }}>
<List>
{allLangs.map((lang) => (
<ListItem key={lang}>
<ListItemText primary={langCodeToName(lang)} />
<ListItemSecondaryAction>
<Switch
checked={mShownLangs.indexOf(lang) !== -1}
onChange={(e) => handleChange(e, lang)}
/>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
</>
);
}
@@ -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>
);
}
+8 -3
View File
@@ -1,4 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -10,6 +13,7 @@ import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core'; import { Grid } from '@material-ui/core';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@@ -42,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 {
@@ -51,6 +55,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
}, },
} = props; } = props;
const classes = useStyles(); const classes = useStyles();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
return ( return (
<Grid item xs={6} sm={4} md={3} lg={2}> <Grid item xs={6} sm={4} md={3} lg={2}>
@@ -62,7 +67,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
className={classes.image} className={classes.image}
component="img" component="img"
alt={title} alt={title}
image={thumbnailUrl} image={serverAddress + thumbnailUrl}
title={title} title={title}
/> />
<div className={classes.gradient} /> <div className={classes.gradient} />
+221 -17
View File
@@ -1,36 +1,189 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { Button } 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 useLocalStorage from '../util/useLocalStorage';
import CategorySelect from './CategorySelect';
const useStyles = (inLibrary: string) => makeStyles((theme: Theme) => ({
root: {
width: '100%',
[theme.breakpoints.up('md')]: {
position: 'sticky',
top: '64px',
left: '0px',
width: '50vw',
height: 'calc(100vh - 64px)',
alignSelf: 'flex-start',
overflowY: 'auto',
},
},
top: {
padding: '10px',
// [theme.breakpoints.up('md')]: {
// minWidth: '50%',
// },
},
leftRight: {
display: 'flex',
},
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': {
color: inLibrary === 'In Library' ? '#2196f3' : 'inherit',
},
'& span': {
display: 'block',
fontSize: '0.85em',
},
'& a': {
textDecoration: 'none',
color: '#858585',
'& button': {
color: 'inherit',
},
},
},
bottom: {
paddingLeft: '10px',
paddingRight: '10px',
[theme.breakpoints.up('md')]: {
fontSize: '1.2em',
// maxWidth: '50%',
},
[theme.breakpoints.up('lg')]: {
fontSize: '1.3em',
},
},
description: {
'& h4': {
marginTop: '1em',
marginBottom: 0,
},
'& p': {
textAlign: 'justify',
textJustify: 'inter-word',
},
},
genre: {
display: 'flex',
flexWrap: 'wrap',
'& h5': {
border: '2px solid #2196f3',
borderRadius: '1.13em',
marginRight: '1em',
marginTop: 0,
marginBottom: '10px',
padding: '0.3em',
color: '#2196f3',
},
},
}));
interface IProps{ interface IProps{
manga: IManga manga: IManga
} }
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 { 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);
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');
fetch(`http://127.0.0.1:4567/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');
fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`, { method: 'DELETE', mode: 'cors' }).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();
@@ -38,13 +191,64 @@ export default function MangaDetails(props: IProps) {
} }
return ( return (
<> <div className={classes.root}>
<h1> <div className={classes.top}>
{manga && manga.title} <div className={classes.leftRight}>
</h1> <div className={classes.leftSide}>
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}> <img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button> </div>
<div className={classes.rightSide}>
<h1>
{manga.title}
</h1>
<h3>
Author:
{' '}
<span>{getValueOrUnknown(manga.author)}</span>
</h3>
<h3>
Artist:
{' '}
<span>{getValueOrUnknown(manga.artist)}</span>
</h3>
<h3>
Status:
{' '}
{manga.status}
</h3>
<h3>
Source:
{' '}
{getSourceName(manga.source)}
</h3>
</div>
</div>
<div className={classes.buttons}>
<div>
<IconButton onClick={() => handleButtonClick()}>
{inLibrary === 'In Library' && <FavoriteIcon />}
{inLibrary !== 'In Library' && <FavoriteBorderIcon />}
<span>{inLibrary}</span>
</IconButton>
</div>
{ /* eslint-disable-next-line react/jsx-no-target-blank */ }
<a href={manga.url} target="_blank">
<IconButton>
<PublicIcon />
<span>Open Site</span>
</IconButton>
</a>
</div>
</div> </div>
</> <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>
); );
} }

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