Compare commits

...

59 Commits

Author SHA1 Message Date
Aria Moradi 0e0d08ae5a bump to v0.3.9
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build artifacts and release (push) Failing after 19s
2021-05-24 18:32:01 +04:30
Aria Moradi 986b4c2c27 unused files removed 2021-05-24 18:31:39 +04:30
Aria Moradi 0bf9ccfcbd [SKIP CI] fix more typo 2021-05-24 18:22:48 +04:30
Aria Moradi 5e8c47928d [SKIP CI] fix typo 2021-05-24 18:22:16 +04:30
Aria Moradi ffae7f911f [SKIP CI] update windows instructions 2021-05-24 18:21:23 +04:30
Aria Moradi e37fdf6d79 [SKIP CI] update preview link 2021-05-24 17:31:07 +04:30
Aria Moradi b359116745 clean up tests 2021-05-24 17:09:05 +04:30
Aria Moradi 60073aace3 test new publish 2021-05-24 17:04:47 +04:30
Aria Moradi 874b13fa14 test new publish 2021-05-24 17:02:30 +04:30
Aria Moradi b146d1024b test new publish 2021-05-24 16:56:03 +04:30
Aria Moradi 332e95c021 test new publish 2021-05-24 16:52:30 +04:30
Aria Moradi 1f68141df5 test new publish 2021-05-24 16:39:17 +04:30
Aria Moradi dd731cd306 test new publish 2021-05-24 16:34:12 +04:30
Aria Moradi 38d8d03cae test new publish 2021-05-24 16:30:32 +04:30
Aria Moradi ec7d840f37 test new publish 2021-05-24 16:29:03 +04:30
Aria Moradi 2813dbb897 test new publish 2021-05-24 16:27:46 +04:30
Aria Moradi 77d1402b8a test new publish 2021-05-24 16:25:42 +04:30
Aria Moradi 08e8a9d105 Electron launcher 2021-05-24 15:37:25 +04:30
Aria Moradi 71661f70b6 flexible z names 2021-05-24 01:49:42 +04:30
Aria Moradi ac1e79ba83 electron! 2021-05-24 01:39:12 +04:30
Aria Moradi d082809776 prepare for electron 2021-05-24 00:42:25 +04:30
Aria Moradi a458a696db webview starts! 2021-05-23 23:04:02 +04:30
Aria Moradi 75786a91b0 add webview 2021-05-23 22:10:04 +04:30
Aria Moradi 6ddb5db57b use HEAD for counting commits
CI Publish / Validate Gradle Wrapper (push) Successful in 10s
CI Publish / Build artifacts and release (push) Failing after 18s
2021-05-23 18:22:35 +04:30
Aria Moradi 4f70cc9283 bump to v0.3.8 2021-05-23 17:27:33 +04:30
Aria Moradi 23b643d637 set default category when adding new manga 2021-05-23 15:28:46 +04:30
Aria Moradi fdfc256c4d Meaningful icons! 2021-05-23 13:48:02 +04:30
Aria Moradi fba56c1b75 replace win64 exe with @Syer10's MSVC build 2021-05-23 12:48:47 +04:30
Aria Moradi 4743bfacf7 [SKIP CI] removing Swing force fixed it for @nar1n 2021-05-21 16:47:46 +04:30
Aria Moradi 2356537f7c try swing 2021-05-21 15:55:37 +04:30
Aria Moradi fa071aee84 refactor github api 2021-05-20 20:41:00 +04:30
Aria Moradi c00ca23a8b put the comment where it should be 2021-05-20 20:27:22 +04:30
Aria Moradi 733b017936 fix webUI not being copied 2021-05-20 19:51:52 +04:30
Aria Moradi 4147f2e368 better comment 2021-05-20 19:21:30 +04:30
Aria Moradi 154b9992eb rewrite without retrofit and kotlin-serialization 2021-05-20 19:20:07 +04:30
Aria Moradi 88b881b043 get rid of guava 2021-05-20 17:56:33 +04:30
Aria Moradi 5d1491fb8c fix package directive 2021-05-20 16:23:13 +04:30
Aria Moradi 3a33196cf1 cleanup dependencies 2021-05-20 16:22:54 +04:30
Aria Moradi fa8e0478da lint file 2021-05-20 13:50:10 +04:30
Aria Moradi 7e7e069244 - Set log level eairlier
- Set AndroidCompat's data root properly
2021-05-20 13:48:33 +04:30
Aria Moradi 18e0d34af0 [SKIP CI] "improvments" 2021-05-20 10:52:57 +04:30
Aria Moradi 3fe3f35483 better commit messages 2021-05-20 10:33:33 +04:30
Aria Moradi cf8e274883 better use of kotlin DSL 2021-05-20 10:24:33 +04:30
Aria Moradi 10dee8b345 improve downloader 2021-05-20 02:36:20 +04:30
Aria Moradi ae8d30593f lint 2021-05-19 23:05:25 +04:30
Aria Moradi 9cde46b5da Fix chpater names, closes #81 2021-05-19 23:03:40 +04:30
Aria Moradi 8e61632155 open the right ip 2021-05-19 17:40:26 +04:30
Aria Moradi e2c4b4cb57 handle when the user runs the app instead of clicking on systemtray 2021-05-19 17:38:33 +04:30
Aria Moradi 326da504ea fix gradle complaning about lint tasks depending on webUI:copyBuild 2021-05-19 17:03:12 +04:30
Aria Moradi c5874a3f10 better chapter looks 2021-05-19 16:50:48 +04:30
Aria Moradi 02802fab97 Application mutex 2021-05-19 16:36:17 +04:30
Aria Moradi 29dea10be2 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 13:42:59 +04:30
Aria Moradi 6bc36193dc open server's location please! 2021-05-19 13:42:18 +04:30
Syer10 81e123388e Fix restore crashing (#90) 2021-05-19 05:29:44 +04:30
Aria Moradi 8ebd7869a5 [SKIP CI] name em 2021-05-19 04:30:21 +04:30
Aria Moradi 7a2f5f13f1 [SKIP CI] name em 2021-05-19 04:28:57 +04:30
Aria Moradi 25d7dad39f build the ref that you have been given! 2021-05-19 04:26:20 +04:30
Aria Moradi c8f8795920 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-19 03:57:27 +04:30
Aria Moradi 6fd8b36dca [SKIP CI] links to the new preview repo 2021-05-19 03:45:24 +04:30
67 changed files with 977 additions and 957 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
# Gradle wrapper
*.jar binary
# Images
# Binary files types
*.webp binary
*.png binary
*.jpg binary
-25
View File
@@ -1,25 +0,0 @@
#!/bin/bash
rm -rf preview/*.jar preview/*.zip
cp master/server/build/Tachidesk-*.jar preview
cp master/server/build/Tachidesk-*.zip preview
cd preview
new_jar_build=$(ls Tachidesk-*.jar)
echo "last jar build file name: $new_jar_build"
latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
echo "{ \"latest\": \"$latest\" }" > index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git status
if [ -n "$(git status --porcelain)" ]; then
git add .
git commit -m "Update preview repository"
git push
else
echo "No changes to commit"
fi
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
name: Build pull request
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
@@ -27,7 +27,7 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout master branch
- name: Checkout pull request
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
+43 -4
View File
@@ -69,11 +69,30 @@ jobs:
dependencies-cache-enabled: true
configuration-cache-enabled: true
# - name: Mock Build and copy webUI, Build Jar
# run: |
# mkdir -p master/server/build
# cd master/server/build
# echo "test" > Tachidesk-v0.3.8-r583.jar
- name: Generate Tag Name
id: GenTagName
run: |
cd master/server/build
genTag=$(ls *.jar | sed -e's/Tachidesk-\|.jar//g')
echo "$genTag"
echo "::set-output name=value::$genTag"
- name: make windows packages
run: |
cd master/scripts
./windows32-bundler.sh
./windows64-bundler.sh
./windows-bundler.sh win32
./windows-bundler.sh win64
# - name: Mock make windows packages
# run: |
# cd master/server/build
# echo test > Tachidesk-v0.3.8-r580-win32.zip
- name: Checkout preview branch
uses: actions/checkout@v2
@@ -83,6 +102,26 @@ jobs:
path: preview
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
- name: Deploy preview
- name: Create Tag
run: |
./master/.github/scripts/commit-preview.sh
TAG="${{ steps.GenTagName.outputs.value }}"
echo "tag: $TAG"
cd preview
echo "{ \"latest\": \"$TAG\" }" > index.json
git add index.json
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git commit -m "Updated to $TAG"
git push origin main
git tag $TAG
git push origin $TAG
- name: Upload Preview Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
artifacts: "master/server/build/*.jar,master/server/build/*.zip"
owner: "Suwayomi"
repo: "Tachidesk-preview"
tag: ${{ steps.GenTagName.outputs.value }}
+6 -6
View File
@@ -18,7 +18,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
build:
name: Build FatJar
name: Build artifacts and release
needs: check_wrapper
runs-on: ubuntu-latest
@@ -28,10 +28,10 @@ jobs:
with:
access_token: ${{ github.token }}
- name: Checkout master branch
- name: Checkout ${{ github.ref }}
uses: actions/checkout@v2
with:
ref: master
ref: ${{ github.ref }}
path: master
fetch-depth: 0
@@ -71,11 +71,11 @@ jobs:
- name: make windows packages
run: |
cd master/scripts
./windows32-bundler.sh
./windows64-bundler.sh
./windows-bundler.sh win32
./windows-bundler.sh win64
- name: Upload Release
uses: xresloader/upload-to-github-release@master
uses: xresloader/upload-to-github-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+4 -2
View File
@@ -10,5 +10,7 @@ server/src/main/resources/react
server/tmp/
server/tachiserver-data/
# OpenJDK downlaods
OpenJDK*
# bundle asset downlaods
OpenJDK*.zip
electron-*.zip
rcedit-*
@@ -7,6 +7,7 @@ package xyz.nulldev.ts.config
* License, v. 2.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 com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
@@ -41,21 +42,34 @@ open class ConfigManager {
*/
fun loadConfigs(): Config {
//Load reference configs
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
val baseConfig =
ConfigFactory.parseMap(
mapOf(
"ts.server.rootDir" to ApplicationRootDir
)
)
//Load user config
val userConfig =
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
File(ApplicationRootDir, "server.conf").let {
ConfigFactory.parseFile(it)
}
val config = ConfigFactory.empty()
.withFallback(baseConfig)
.withFallback(userConfig)
.withFallback(compatConfig)
.withFallback(serverConfig)
.resolve()
// set log level early
if (debugLogsEnabled(config)) {
setLogLevel(Level.DEBUG)
}
logger.debug {
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
}
@@ -0,0 +1,20 @@
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 ch.qos.logback.classic.Level
import com.typesafe.config.Config
import mu.KotlinLogging
import org.slf4j.Logger
fun setLogLevel(level: Level) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = level
}
fun debugLogsEnabled(config: Config)
= System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("server.debugLogsEnabled")).toBoolean()
+8 -4
View File
@@ -1,7 +1,7 @@
| Build | Stable | Preview | Support Server |
|-------|----------|---------|---------|
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk/raw/preview/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk/tree/preview/) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
| ![CI](https://github.com/Suwayomi/Tachidesk/actions/workflows/build_push.yml/badge.svg) | [![stable release](https://img.shields.io/github/release/Suwayomi/Tachidesk.svg?maxAge=3600&label=download)](https://github.com/Suwayomi/Tachidesk/releases) | [![preview](https://img.shields.io/badge/dynamic/json?url=https://github.com/Suwayomi/Tachidesk-preview/raw/main/index.json&label=download&query=$.latest&color=blue)](https://github.com/Suwayomi/Tachidesk-preview/releases/latest) | [![Discord](https://img.shields.io/discord/801021177333940224.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/DDZdqZWaHA) |
# Tachidesk
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
@@ -32,14 +32,18 @@ Here is a list of current features:
### All Operating Systems
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(Google is your friend for seeking assitance). Also an internet connection is required as almost everything this app does is downloading stuff.
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (or `java -jar Tachidesk-latest.jar` if you have the latest preview) from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
### Windows
Download the latest win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
Download the latest "Stable" win32 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview one from [the preview repository](https://github.com/Suwayomi/Tachidesk-preview/releases).
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-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
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-win64.zip` and run one of the Launcher files depending on what you want(see bellow). The rest works like the previous section.
#### Windows Launchers
- `Tachidesk Electron Launcher.bat`: Launches Tachidesk inside Electron as a desktop applicaton
- `Tachidesk Browser Launcher.bat`: Launches Tachidesk in a browser window
- `Tachidesk Debug Launcher.bat`: Launches Tachidesk with debug logs attached. If Tachidesk doesn't work for you, running this can give you insight into why.
### Arch Linux
You can install Tachidesk from the AUR
+2 -2
View File
@@ -59,14 +59,14 @@ configure(projects) {
implementation("ch.qos.logback:logback-classic:1.2.3")
implementation("io.github.microutils:kotlin-logging:2.0.6")
// RxJava
// ReactiveX
implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxkotlin:1.0.0")
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
// JSoup
implementation("org.jsoup:jsoup:1.13.1")
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.1")
implementation("io.github.config4k:config4k:0.4.2")
@@ -0,0 +1 @@
jre\bin\javaw "-Dir.armor.tachidesk.webInterface=electron" "-Dir.armor.tachidesk.electronPath=electron/electron.exe" -jar Tachidesk.jar
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,3 +1,3 @@
# Building `Tachidesk Launcher.exe`
1. compile `Tachidesk Launcher.c` statically using GCC MinGW: `gcc -o "Tachidesk Launcher.exe" "Tachidesk Launcher.c"`
1. compile `Tachidesk Launcher.c` statically with MSVC compiler.
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
+83
View File
@@ -0,0 +1,83 @@
#!/bin/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/.
electron_version="v12.0.9"
if [ $1 = "win32" ]; then
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
arch="win32"
electron="electron-$electron_version-win32-ia32.zip"
else
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
arch="win64"
electron="electron-$electron_version-win32-x64.zip"
fi
jre_dir="jdk8u292-b10-jre"
echo "creating windows bundle"
jar=$(ls ../server/build/Tachidesk-*.jar)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-$arch
# make release dir
mkdir $release_name
echo "Dealing with jre..."
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/$jre" -o $jre
fi
unzip $jre
mv $jre_dir $release_name/jre
echo "Dealing with electron"
if [ ! -f $electron ]; then
curl -L "https://github.com/electron/electron/releases/download/$electron_version/$electron" -o $electron
fi
unzip $electron -d $release_name/electron
# change electron's icon
rcedit="rcedit-x86.exe"
if [ ! -f $rcedit ]; then
curl -L "https://github.com/electron/rcedit/releases/download/v1.1.1/$rcedit" -o $rcedit
fi
# check if running under github actions
if [ $CI = true ]; then
# change electron executable's icon
sudo dpkg --add-architecture i386
wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -
sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport
sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"
sudo apt install --install-recommends winehq-stable
fi
# this script assumes that wine is installed here on out
WINEARCH=win32 wine $rcedit $release_name/electron/electron.exe --set-icon ../server/src/main/resources/icon/faviconlogo.ico
# copy artifacts
cp $jar $release_name/Tachidesk.jar
#cp "resources/Tachidesk Launcher-$arch.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Browser Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
cp "resources/Tachidesk Electron Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
# clean up from possible previous runs
if [ -f ../server/build/$zip_name ]; then
rm ../server/build/$zip_name
fi
mv $zip_name ../server/build/
-38
View File
@@ -1,38 +0,0 @@
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form 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/.
Write-Output "Downloading jre..."
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
if (!(Test-Path $jre)) {
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
}
Write-Output "creating windows bundle"
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
$release_name=$jar.BaseName + "-win32"
# make release dir
New-Item -ItemType Directory $release_name
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
# move jre
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
$zip_name="$release_name.zip"
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
Remove-Item -Force -Recurse $release_name
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
-41
View File
@@ -1,41 +0,0 @@
#!/bin/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/.
echo "Downloading jre..."
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
fi
echo "creating windows bundle"
jar=$(ls ../server/build/Tachidesk-*.jar)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win32
# make release dir
mkdir $release_name
unzip $jre
# move jre
mv jdk8u292-b10-jre $release_name/jre
cp $jar $release_name/Tachidesk.jar
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
mv $zip_name ../server/build/
-38
View File
@@ -1,38 +0,0 @@
# Copyright (C) Contributors to the Suwayomi project
#
# This Source Code Form 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/.
Write-Output "Downloading jre..."
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
if (!(Test-Path $jre)) {
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
}
Write-Output "creating windows bundle"
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
$release_name=$jar.BaseName + "-win64"
# make release dir
New-Item -ItemType Directory $release_name
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
# move jre
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
$zip_name="$release_name.zip"
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
Remove-Item -Force -Recurse $release_name
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
-41
View File
@@ -1,41 +0,0 @@
#!/bin/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/.
echo "Downloading jre..."
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
if [ ! -f $jre ]; then
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
fi
echo "creating windows bundle"
jar=$(ls ../server/build/Tachidesk-*.jar)
jar_name=$(echo $jar | cut -d'/' -f4)
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
# make release dir
mkdir $release_name
unzip $jre
# move jre
mv jdk8u292-b10-jre $release_name/jre
cp $jar $release_name/Tachidesk.jar
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
cp "resources/Tachidesk Launcher.bat" $release_name
cp "resources/Tachidesk Debug Launcher.bat" $release_name
zip_name=$release_name.zip
zip -9 -r $zip_name $release_name
rm -rf $release_name
mv $zip_name ../server/build/
+32 -36
View File
@@ -12,48 +12,27 @@ plugins {
}
repositories {
mavenCentral()
maven {
url = uri("https://repo1.maven.org/maven2/")
}
maven {
url = uri("https://jitpack.io")
}
}
dependencies {
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
val okhttpVersion = "4.10.0-RC1"
// okhttp
val okhttpVersion = "4.9.1" // version is locked by Tachiyomi extensions
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okio:okio:2.10.0")
// Retrofit
val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion")
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
// Reactivex
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// api
// Javalin api
implementation("io.javalin:javalin:3.13.6")
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
// jackson version is tied to javalin, ref: `io.javalin.core.util.OptionalDependency`
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.10.3")
// Exposed ORM
val exposedVersion = "0.31.1"
@@ -61,7 +40,6 @@ dependencies {
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
// current database driver
implementation("com.h2database:h2:1.4.200")
@@ -69,7 +47,19 @@ dependencies {
implementation("com.dorkbox:SystemTray:4.1")
implementation("com.dorkbox:Utilities:1.9")
implementation("com.google.guava:guava:30.1.1-jre")
// dependencies of Tachiyomi extensions, some are duplicate, keeping it here for reference
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("org.jsoup:jsoup:1.13.1")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
// Source models and interfaces from Tachiyomi 1.x
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
// implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidCompat
implementation(project(":AndroidCompat"))
@@ -85,6 +75,12 @@ dependencies {
val MainClass = "ir.armor.tachidesk.MainKt"
application {
mainClass.set(MainClass)
// for testing electron
// applicationDefaultJvmArgs = listOf(
// "-Dir.armor.tachidesk.webInterface=electron",
// "-Dir.armor.tachidesk.electronPath=/home/armor/programming/Suwayomi/Tachidesk/scripts/electron-v12.0.9-linux-x64/electron"
// )
}
sourceSets {
@@ -96,12 +92,12 @@ sourceSets {
}
// should be bumped with each stable release
val tachideskVersion = "v0.3.7"
val tachideskVersion = "v0.3.9"
// counts commit count on master
val tachideskRevision = Runtime
.getRuntime()
.exec("git rev-list master --count")
.exec("git rev-list HEAD --count")
.let { process ->
process.waitFor()
val output = process.inputStream.use {
@@ -171,10 +167,10 @@ tasks {
}
withType<LintTask> {
source(files("src"))
source(files("src/kotlin"))
}
withType<FormatTask> {
source(files("src"))
source(files("src/kotlin"))
}
}
@@ -1,53 +0,0 @@
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 eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
suspend fun findExtensions(): List<Extension.Available> {
val service: ExtensionGithubService = ExtensionGithubService.create()
val response = service.getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
}
@@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.extension.api
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import okhttp3.MediaType.Companion.toMediaType
import retrofit2.Retrofit
import retrofit2.http.GET
import uy.kohesive.injekt.injectLazy
/**
* Used to get the extension repo listing from GitHub.
*/
interface ExtensionGithubService {
companion object {
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.build()
}
@ExperimentalSerializationApi
fun create(): ExtensionGithubService {
val adapter = Retrofit.Builder()
.baseUrl(ExtensionGithubApi.BASE_URL)
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.client(client)
.build()
return adapter.create(ExtensionGithubService::class.java)
}
}
@GET("${ExtensionGithubApi.REPO_URL_PREFIX}/index.json.gz")
suspend fun getRepo(): JsonArray
}
@@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.extension.model
import eu.kanade.tachiyomi.source.Source
sealed class Extension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Int
abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false
) : Extension()
data class Available(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
) : Extension()
data class Untrusted(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Int,
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension()
}
@@ -1,9 +0,0 @@
package eu.kanade.tachiyomi.extension.model
enum class InstallStep {
Pending, Downloading, Installing, Installed, Error;
fun isCompleted(): Boolean {
return this == Installed || this == Error
}
}
@@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.model
sealed class LoadResult {
class Success(val extension: Extension.Installed) : LoadResult()
class Untrusted(val extension: Extension.Untrusted) : LoadResult()
class Error(val message: String? = null) : LoadResult() {
constructor(exception: Throwable) : this(exception.message)
}
}
@@ -1,234 +0,0 @@
package eu.kanade.tachiyomi.extension.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
// import android.annotation.SuppressLint
// import android.content.Context
// import android.content.pm.PackageInfo
// import android.content.pm.PackageManager
// import dalvik.system.PathClassLoader
// import eu.kanade.tachiyomi.data.preference.PreferenceValues
// import eu.kanade.tachiyomi.data.preference.PreferencesHelper
// import eu.kanade.tachiyomi.util.lang.Hash
// import kotlinx.coroutines.async
// import kotlinx.coroutines.runBlocking
// import timber.log.Timber
// import uy.kohesive.injekt.injectLazy
/**
* Class that handles the loading of the extensions installed in the system.
*/
// @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader {
// private val preferences: PreferencesHelper by injectLazy()
// private val allowNsfwSource by lazy {
// preferences.allowNsfwSource().get()
// }
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
// private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/**
* List of the trusted signatures.
*/
// var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().get() + officialSignature
/**
* Return a list of all the installed extensions initialized concurrently.
*
* @param context The application context.
*/
// fun loadExtensions(context: Context): List<LoadResult> {
// val pkgManager = context.packageManager
// val installedPkgs = pkgManager.getInstalledPackages(PACKAGE_FLAGS)
// val extPkgs = installedPkgs.filter { isPackageAnExtension(it) }
//
// if (extPkgs.isEmpty()) return emptyList()
//
// // Load each extension concurrently and wait for completion
// return runBlocking {
// val deferred = extPkgs.map {
// async { loadExtension(context, it.packageName, it) }
// }
// deferred.map { it.await() }
// }
// }
/**
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
// fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
// val pkgInfo = try {
// context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
// if (!isPackageAnExtension(pkgInfo)) {
// return LoadResult.Error("Tried to load a package that wasn't a extension")
// }
// return loadExtension(context, pkgName, pkgInfo)
// }
/**
* Loads an extension given its package name.
*
* @param context The application context.
* @param pkgName The package name of the extension to load.
* @param pkgInfo The package info of the extension.
*/
// private fun loadExtension(context: Context, pkgName: String, pkgInfo: PackageInfo): LoadResult {
// val pkgManager = context.packageManager
//
// val appInfo = try {
// pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
// } catch (error: PackageManager.NameNotFoundException) {
// // Unlikely, but the package may have been uninstalled at this point
// return LoadResult.Error(error)
// }
//
// val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
// val versionName = pkgInfo.versionName
// val versionCode = pkgInfo.versionCode
//
// if (versionName.isNullOrEmpty()) {
// val exception = Exception("Missing versionName for extension $extName")
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// // Validate lib version
// val libVersion = versionName.substringBeforeLast('.').toDouble()
// if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
// val exception = Exception(
// "Lib version is $libVersion, while only versions " +
// "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
// )
// Timber.w(exception)
// return LoadResult.Error(exception)
// }
//
// val signatureHash = getSignatureHash(pkgInfo)
//
// if (signatureHash == null) {
// return LoadResult.Error("Package $pkgName isn't signed")
// } else if (signatureHash !in trustedSignatures) {
// val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
// Timber.w("Extension $pkgName isn't trusted")
// return LoadResult.Untrusted(extension)
// }
//
// val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.BLOCKED && isNsfw) {
// return LoadResult.Error("NSFW extension $pkgName not allowed")
// }
//
// val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
//
// val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
// .split(";")
// .map {
// val sourceClass = it.trim()
// if (sourceClass.startsWith(".")) {
// pkgInfo.packageName + sourceClass
// } else {
// sourceClass
// }
// }
// .flatMap {
// try {
// when (val obj = Class.forName(it, false, classLoader).newInstance()) {
// is Source -> listOf(obj)
// is SourceFactory -> {
// if (isSourceNsfw(obj)) {
// emptyList()
// } else {
// obj.createSources()
// }
// }
// else -> throw Exception("Unknown source class type! ${obj.javaClass}")
// }
// } catch (e: Throwable) {
// Timber.e(e, "Extension load error: $extName.")
// return LoadResult.Error(e)
// }
// }
// .filter { !isSourceNsfw(it) }
//
// val langs = sources.filterIsInstance<CatalogueSource>()
// .map { it.lang }
// .toSet()
// val lang = when (langs.size) {
// 0 -> ""
// 1 -> langs.first()
// else -> "all"
// }
//
// val extension = Extension.Installed(
// extName,
// pkgName,
// versionName,
// versionCode,
// lang,
// isNsfw,
// sources,
// isUnofficial = signatureHash != officialSignature
// )
// return LoadResult.Success(extension)
// }
/**
* Returns true if the given package is an extension.
*
* @param pkgInfo The package info of the application.
*/
// private fun isPackageAnExtension(pkgInfo: PackageInfo): Boolean {
// return pkgInfo.reqFeatures.orEmpty().any { it.name == EXTENSION_FEATURE }
// }
/**
* Returns the signature hash of the package or null if it's not signed.
*
* @param pkgInfo The package info of the application.
*/
// private fun getSignatureHash(pkgInfo: PackageInfo): String? {
// val signatures = pkgInfo.signatures
// return if (signatures != null && signatures.isNotEmpty()) {
// Hash.sha256(signatures.first().toByteArray())
// } else {
// null
// }
// }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
// private fun isSourceNsfw(clazz: Any): Boolean {
// if (allowNsfwSource == PreferenceValues.NsfwAllowance.ALLOWED) {
// return false
// }
//
// if (clazz !is Source && clazz !is SourceFactory) {
// return false
// }
//
// // Annotations are proxied, hence this janky way of checking for them
// return clazz.javaClass.annotations
// .flatMap { it.javaClass.interfaces.map { it.simpleName } }
// .firstOrNull { it == Nsfw::class.java.simpleName } != null
// }
}
@@ -35,16 +35,16 @@ object Category {
}
}
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (name != null) it[CategoryTable.name] = name
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
if (isDefault != null) it[CategoryTable.isDefault] = isDefault
}
}
}
/**
/**
* Move the category from position `from` to `to`
*/
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable
@@ -9,25 +9,37 @@ package ir.armor.tachidesk.impl
import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
import ir.armor.tachidesk.model.database.table.CategoryTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.MangaDataClass
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
object Library {
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// TODO: `Category.isLanding` is to handle the default categories a new library manga gets,
// ..implement that shit at some time...
// ..also Consider to rename it to `isDefault`
suspend fun addMangaToLibrary(mangaId: Int) {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
transaction {
val defaultCategories = CategoryTable.select { CategoryTable.isDefault eq true }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) {
it[inLibrary] = true
it[MangaTable.inLibrary] = true
it[MangaTable.defaultCategory] = defaultCategories.isEmpty()
}
defaultCategories.forEach { category ->
CategoryMangaTable.insert {
it[CategoryMangaTable.category] = category[CategoryTable.id].value
it[CategoryMangaTable.manga] = mangaId
}
}
}
}
@@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass
@@ -9,7 +9,7 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaStatus
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.dataclass.MangaDataClass
@@ -9,13 +9,13 @@ package ir.armor.tachidesk.impl
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.storage.SafePath
import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
@@ -28,7 +28,7 @@ import java.io.File
import java.io.InputStream
object Page {
/**
/**
* A page might have a imageUrl ready from the get go, or we might need to
* go an extra step and call fetchImageUrl to get it.
*/
@@ -68,33 +68,28 @@ object Page {
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
val fileName = index.toString()
val fileName = String.format("%03d", index) // e.g. 001.jpeg
return getCachedImageResponse(saveDir, fileName) {
source.fetchImage(tachiPage).awaitSingle()
}
}
// TODO: rewrite this to match tachiyomi
private val applicationDirs by DI.global.instance<ApplicationDirs>()
fun getChapterDir(mangaId: Int, chapterId: Int): String {
private fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
val sourceDir = source.toString()
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
val chapterDir = SafePath.buildValidFilename(
when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
)
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
return "${applicationDirs.mangaRoot}/$sourceDir/$mangaDir/$chapterDir"
}
}
@@ -9,11 +9,11 @@ package ir.armor.tachidesk.impl
import ir.armor.tachidesk.impl.MangaList.processEntries
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
object Search {
// TODO
// TODO
fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId)
// source.getFilterList().toItems()
@@ -34,7 +34,7 @@ object Search {
val filter: Any
)
/**
/**
* Note: Exhentai had a filter serializer (now in SY) that we might be able to steal
*/
// private fun FilterList.toFilterWrapper(): List<FilterWrapper> {
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.impl
* License, v. 2.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.impl.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable
@@ -21,7 +21,7 @@ import ir.armor.tachidesk.impl.backup.models.MangaImpl
import ir.armor.tachidesk.impl.backup.models.Track
import ir.armor.tachidesk.impl.backup.models.TrackImpl
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.database.table.MangaTable
import mu.KotlinLogging
import org.jetbrains.exposed.sql.and
@@ -64,11 +64,7 @@ object LegacyBackupImport : LegacyBackupBase() {
logger.info {
"""
Restore Errors:
${
errors.map {
"${it.first} - ${it.second}"
}.joinToString("\n")
}
${ errors.joinToString("\n") { "${it.first} - ${it.second}" } }
Restore Summary:
- Missing Sources:
${validationResult.missingSources.joinToString("\n")}
@@ -119,6 +115,8 @@ object LegacyBackupImport : LegacyBackupBase() {
getHttpSource(manga.source)
} catch (e: NullPointerException) {
null
} catch (e: NoSuchElementException) {
null
}
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
@@ -0,0 +1,28 @@
package ir.armor.tachidesk.impl.download
import org.jetbrains.exposed.sql.ResultRow
import java.util.concurrent.LinkedBlockingQueue
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
data class Download(
val chapter: ResultRow,
)
private val downloadQueue = LinkedBlockingQueue<Download>()
class Downloader {
fun start() {
TODO()
}
fun stop() {
TODO()
}
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl
package ir.armor.tachidesk.impl.extension
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -8,14 +8,13 @@ package ir.armor.tachidesk.impl
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.net.Uri
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.extension.ExtensionsList.extensionTableAsDataClass
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
@@ -27,6 +26,7 @@ import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
import ir.armor.tachidesk.impl.util.await
import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.database.table.SourceTable
import ir.armor.tachidesk.server.ApplicationDirs
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl
package ir.armor.tachidesk.impl.extension
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -7,9 +7,9 @@ package ir.armor.tachidesk.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIconUrl
import ir.armor.tachidesk.impl.extension.github.ExtensionGithubApi
import ir.armor.tachidesk.impl.extension.github.OnlineExtension
import ir.armor.tachidesk.model.database.table.ExtensionTable
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import mu.KotlinLogging
@@ -25,7 +25,7 @@ object ExtensionsList {
private val logger = KotlinLogging.logger {}
var lastUpdateCheck: Long = 0
var updateMap = ConcurrentHashMap<String, Extension.Available>()
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
/** 60,000 milliseconds = 60 seconds */
private const val ExtensionUpdateDelayTime = 60 * 1000
@@ -63,7 +63,7 @@ object ExtensionsList {
}
}
private fun updateExtensionDatabase(foundExtensions: List<Extension.Available>) {
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
transaction {
foundExtensions.forEach { foundExtension ->
val extensionRecord = ExtensionTable.select { ExtensionTable.pkgName eq foundExtension.pkgName }.firstOrNull()
@@ -0,0 +1,119 @@
package ir.armor.tachidesk.impl.extension.github
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form 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.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.http.RealResponseBody
import okio.GzipSource
import okio.buffer
import uy.kohesive.injekt.injectLazy
import java.io.IOException
object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = "1.2"
private const val LIB_VERSION_MAX = "1.2"
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json
.map { it.asJsonObject }
.filter { element ->
val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.')
libVersion == LIB_VERSION_MAX
}
.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val nsfw = element["nsfw"].int == 1
val icon = "$REPO_URL_PREFIX/icon/${apkName.replace(".apk", ".png")}"
OnlineExtension(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
}
}
suspend fun findExtensions(): List<OnlineExtension> {
val response = getRepo()
return parseResponse(response)
}
fun getApkUrl(extension: ExtensionDataClass): String {
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
}
private val client by lazy {
val network: NetworkHelper by injectLazy()
network.client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.header("Content-Encoding", "gzip")
.header("Content-Type", "application/json")
.build()
}
.addInterceptor(UnzippingInterceptor())
.build()
}
private fun getRepo(): com.google.gson.JsonArray {
val request = Request.Builder()
.url("$REPO_URL_PREFIX/index.json.gz")
.build()
val response = client.newCall(request).execute().use { response -> response.body!!.string() }
return JsonParser.parseString(response).asJsonArray
}
}
// ref: https://stackoverflow.com/questions/51901333/okhttp-3-how-to-decompress-gzip-deflate-response-manually-using-java-android
private class UnzippingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
val response: Response = chain.proceed(chain.request())
return unzip(response)
}
@Throws(IOException::class)
private fun unzip(response: Response): Response {
if (response.body == null) {
return response
}
// check if we have gzip response
val contentEncoding: String? = response.headers["Content-Encoding"]
// this is used to decompress gzipped responses
return if (contentEncoding != null && contentEncoding == "gzip") {
val body = response.body!!
val contentLength: Long = body.contentLength()
val responseBody = GzipSource(body.source())
val strippedHeaders: Headers = response.headers.newBuilder().build()
response.newBuilder().headers(strippedHeaders)
.body(RealResponseBody(body.contentType().toString(), contentLength, responseBody.buffer()))
.build()
} else {
response
}
}
}
@@ -0,0 +1,12 @@
package ir.armor.tachidesk.impl.extension.github
data class OnlineExtension(
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val apkName: String,
val iconUrl: String
)
@@ -1,99 +0,0 @@
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 eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.FormBody
import okhttp3.OkHttpClient
import java.net.URLEncoder
// TODO: finish MangaDex support
class MangaDexHelper(private val mangaDexSource: HttpSource) {
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
private fun clientBuilder(
r18Toggle: Int,
okHttpClient: OkHttpClient = mangaDexSource.network.client
): OkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor { chain ->
val originalCookies = chain.request().header("Cookie") ?: ""
val newReq = chain
.request()
.newBuilder()
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
.build()
chain.proceed(newReq)
}.build()
private fun cookiesHeader(r18Toggle: Int): String {
val cookies = mutableMapOf<String, String>()
cookies["mangadex_h_toggle"] = r18Toggle.toString()
return buildCookies(cookies)
}
private fun buildCookies(cookies: Map<String, String>) =
cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
// fun isLogged(): Boolean {
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
// }
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = clientBuilder().newCall(
POST(
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
mangaDexSource.headers,
formBody.build()
)
).execute()
return response.body!!.string().isEmpty()
}
//
// fun logout(): Boolean {
// return withContext(Dispatchers.IO) {
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
// val listOfDexCookies = network.cookieManager.get(httpUrl)
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
// val token = cookie?.value
// if (token.isNullOrEmpty()) {
// return@withContext true
// }
// val result = clientBuilder().newCall(
// POSTWithCookie(
// "$baseUrl/ajax/actions.ajax.php?function=logout",
// REMEMBER_ME,
// token,
// headers
// )
// ).execute()
// val resultStr = result.body!!.string()
// if (resultStr.contains("success", true)) {
// network.cookieManager.remove(httpUrl)
// return@withContext true
// }
//
// false
// }
// }
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.util
package ir.armor.tachidesk.impl.util.lang
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.impl.util
package ir.armor.tachidesk.impl.util.storage
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -8,13 +8,9 @@ package ir.armor.tachidesk.impl.util
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okhttp3.Response
import okio.buffer
import okio.sink
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
object CachedImageResponse {
private fun pathToInputStream(path: String): InputStream {
@@ -45,18 +41,19 @@ object CachedImageResponse {
val response = fetcher()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
val fullPath = filePath + "." + contentType.substringAfter("image/")
val fullPath = "$filePath.tmp"
val saveFile = File(fullPath)
response.body!!.source().saveTo(saveFile)
Files.newOutputStream(Paths.get(fullPath)).use { output ->
response.body!!.source().use { input ->
output.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
return pathToInputStream(fullPath) to contentType
// find image type
val imageType = response.headers["content-type"]
?: ImageUtil.findImageType { saveFile.inputStream() }?.mime
?: "image/jpeg"
.substringAfter("image/")
saveFile.renameTo(File("$filePath.$imageType"))
return pathToInputStream(fullPath) to imageType
} else {
throw Exception("request error! ${response.code}")
}
@@ -0,0 +1,69 @@
package ir.armor.tachidesk.impl.util.storage
import java.io.InputStream
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form 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/. */
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
object ImageUtil {
fun findImageType(openStream: () -> InputStream): ImageType? {
return openStream().use { findImageType(it) }
}
fun findImageType(stream: InputStream): ImageType? {
try {
val bytes = ByteArray(8)
val length = if (stream.markSupported()) {
stream.mark(bytes.size)
stream.read(bytes, 0, bytes.size).also { stream.reset() }
} else {
stream.read(bytes, 0, bytes.size)
}
if (length == -1) {
return null
}
if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) {
return ImageType.JPG
}
if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) {
return ImageType.PNG
}
if (bytes.compareWith("GIF8".toByteArray())) {
return ImageType.GIF
}
if (bytes.compareWith("RIFF".toByteArray())) {
return ImageType.WEBP
}
} catch (e: Exception) {
}
return null
}
private fun ByteArray.compareWith(magic: ByteArray): Boolean {
return magic.indices.none { this[it] != magic[it] }
}
private fun charByteArrayOf(vararg bytes: Int): ByteArray {
return ByteArray(bytes.size).apply {
for (i in bytes.indices) {
set(i, bytes[i].toByte())
}
}
}
enum class ImageType(val mime: String) {
JPG("image/jpeg"),
PNG("image/png"),
GIF("image/gif"),
WEBP("image/webp")
}
}
@@ -0,0 +1,48 @@
package ir.armor.tachidesk.impl.util.storage
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form 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 okio.BufferedSource
import okio.buffer
import okio.sink
import java.io.File
import java.io.OutputStream
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt
/**
* Saves the given source to a file and closes it. Directories will be created if needed.
*
* @param file the file where the source is copied.
*/
fun BufferedSource.saveTo(file: File) {
try {
// Create parent dirs if needed
file.parentFile.mkdirs()
// Copy to destination
saveTo(file.outputStream())
} catch (e: Exception) {
close()
file.delete()
throw e
}
}
/**
* Saves the given source to an output stream and closes both resources.
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
it.flush()
}
}
}
@@ -0,0 +1,47 @@
package ir.armor.tachidesk.impl.util.storage
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form 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/. */
// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt
object SafePath {
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
* with a dot), but you can manually add it later.
*/
fun buildValidFilename(origName: String): String {
val name = origName.trim('.', ' ')
if (name.isEmpty()) {
return "(invalid)"
}
val sb = StringBuilder(name.length)
name.forEach { c ->
if (isValidFatFilenameChar(c)) {
sb.append(c)
} else {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
}
/**
* Returns true if the given character is a valid filename character, false otherwise.
*/
private fun isValidFatFilenameChar(c: Char): Boolean {
if (0x00.toChar() <= c && c <= 0x1f.toChar()) {
return false
}
return when (c) {
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7f.toChar() -> false
else -> true
}
}
}
@@ -0,0 +1,24 @@
package ir.armor.tachidesk.model.database.migration
import ir.armor.tachidesk.model.database.migration.lib.Migration
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form 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/. */
@Suppress("ClassName", "unused")
class M0003_DefaultCategory : Migration() {
/** this migration renamed CategoryTable.IS_LANDING to ChapterTable.IS_DEFAULT */
override fun run() {
with(TransactionManager.current()) {
exec("ALTER TABLE CATEGORY ALTER COLUMN IS_LANDING RENAME TO IS_DEFAULT")
commit()
currentDialect.resetCaches()
}
}
}
@@ -10,7 +10,7 @@ package ir.armor.tachidesk.model.database.migration.lib
// originally licenced under MIT by Andreas Mausch, Changes are licenced under Mozilla Public License, v. 2.0.
// adopted from: https://gitlab.com/andreas-mausch/exposed-migrations/-/tree/4bf853c18a24d0170eda896ddbb899cb01233595
import com.google.common.reflect.ClassPath
import ir.armor.tachidesk.server.ServerConfig
import mu.KotlinLogging
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Database
@@ -18,8 +18,15 @@ import org.jetbrains.exposed.sql.SchemaUtils.create
import org.jetbrains.exposed.sql.exists
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Paths
import java.time.Clock
import java.time.Instant.now
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.isDirectory
import kotlin.io.path.name
import kotlin.streams.toList
private val logger = KotlinLogging.logger {}
@@ -54,13 +61,31 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
logger.info { "Migrations finished successfully" }
}
@OptIn(ExperimentalPathApi::class)
private fun getTopLevelClasses(packageName: String): List<Class<*>> {
ServerConfig::class.java.getResource("/" + "ir.armor.tachidesk.model.database.migration".replace('.', '/'))
val path = "/" + packageName.replace('.', '/')
val uri = ServerConfig::class.java.getResource(path).toURI()
return when (uri.scheme) {
"jar" -> {
val fileSystem = FileSystems.newFileSystem(uri, emptyMap<String, Any>())
fileSystem.getPath(path)
}
else -> Paths.get(uri)
}.let { Files.walk(it, 1) }
.toList()
.filterNot { it.isDirectory() || it.name.contains('$') } // '$' means it's not a top level class
.filter { it.name.endsWith(".class") }
.map { Class.forName("$packageName.${it.name.substringBefore(".class")}") }
}
@Suppress("UnstableApiUsage")
fun loadMigrationsFrom(classPath: String): List<Migration> {
return ClassPath.from(Thread.currentThread().contextClassLoader)
.getTopLevelClasses(classPath)
fun loadMigrationsFrom(packageName: String): List<Migration> {
return getTopLevelClasses(packageName)
.map {
logger.debug("found Migration class ${it.name}")
val clazz = it.load().getDeclaredConstructor().newInstance()
val clazz = it.getDeclaredConstructor().newInstance()
if (clazz is Migration)
clazz
else
@@ -13,13 +13,13 @@ import org.jetbrains.exposed.sql.ResultRow
object CategoryTable : IntIdTable() {
val name = varchar("name", 64)
val isLanding = bool("is_landing").default(false)
val order = integer("order").default(0)
val isDefault = bool("is_default").default(false)
}
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
categoryEntry[this.id].value,
categoryEntry[this.order],
categoryEntry[this.name],
categoryEntry[this.isLanding],
categoryEntry[this.isDefault],
)
@@ -11,5 +11,5 @@ data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String,
val isLanding: Boolean
val default: Boolean
)
@@ -13,14 +13,8 @@ import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
import ir.armor.tachidesk.impl.Chapter.getChapter
import ir.armor.tachidesk.impl.Chapter.getChapterList
import ir.armor.tachidesk.impl.Chapter.modifyChapter
import ir.armor.tachidesk.impl.Extension.getExtensionIcon
import ir.armor.tachidesk.impl.Extension.installExtension
import ir.armor.tachidesk.impl.Extension.uninstallExtension
import ir.armor.tachidesk.impl.Extension.updateExtension
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.Library.addMangaToLibrary
import ir.armor.tachidesk.impl.Library
import ir.armor.tachidesk.impl.Library.getLibraryMangas
import ir.armor.tachidesk.impl.Library.removeMangaFromLibrary
import ir.armor.tachidesk.impl.Manga.getManga
import ir.armor.tachidesk.impl.Manga.getMangaThumbnail
import ir.armor.tachidesk.impl.MangaList.getMangaList
@@ -33,8 +27,13 @@ import ir.armor.tachidesk.impl.Source.getSourceList
import ir.armor.tachidesk.impl.backup.BackupFlags
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
import ir.armor.tachidesk.server.internal.About.getAbout
import ir.armor.tachidesk.server.util.openInBrowser
import ir.armor.tachidesk.impl.extension.Extension.getExtensionIcon
import ir.armor.tachidesk.impl.extension.Extension.installExtension
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
import ir.armor.tachidesk.server.impl_internal.About.getAbout
import ir.armor.tachidesk.server.util.Browser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -79,6 +78,12 @@ object JavalinSetup {
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.events { event ->
event.serverStarted {
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
Browser.openInBrowser()
}
}
}.start(serverConfig.ip, serverConfig.port)
// when JVM is prompted to shutdown, stop javalin gracefully
@@ -88,10 +93,6 @@ object JavalinSetup {
}
)
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
openInBrowser()
}
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error("NullPointerException while handling the request", e)
ctx.status(404)
@@ -216,24 +217,6 @@ object JavalinSetup {
)
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { addMangaToLibrary(mangaId) }
)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { removeMangaFromLibrary(mangaId) }
)
}
// list manga's categories
app.get("api/v1/manga/:mangaId/category/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
@@ -302,6 +285,16 @@ object JavalinSetup {
)
}
// submit a chapter for download
app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// TODO
}
// cancel a chapter download
app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx ->
// TODO
}
// global search, Not implemented yet
app.get("/api/v1/search/:searchTerm") { ctx ->
val searchTerm = ctx.pathParam("searchTerm")
@@ -322,6 +315,24 @@ object JavalinSetup {
ctx.json(sourceFilters(sourceId))
}
// adds the manga to library
app.get("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { Library.addMangaToLibrary(mangaId) }
)
}
// removes the manga from the library
app.delete("api/v1/manga/:mangaId/library") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.result(
future { Library.removeMangaFromLibrary(mangaId) }
)
}
// lists mangas that have no category assigned
app.get("/api/v1/library/") { ctx ->
ctx.json(getLibraryMangas())
@@ -348,8 +359,8 @@ object JavalinSetup {
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)
val isDefault = ctx.formParam("default")?.toBoolean()
updateCategory(categoryId, name, isDefault)
ctx.status(200)
}
@@ -432,5 +443,19 @@ object JavalinSetup {
}
)
}
// Download queue stats
app.ws("/api/v1/downloads") { ws ->
ws.onConnect { ctx ->
// TODO: send current stat
// TODO: add to downlad subscribers
}
ws.onMessage {
// TODO: send current stat
}
ws.onClose { ctx ->
// TODO: remove from subscribers
}
}
}
}
@@ -10,6 +10,8 @@ package ir.armor.tachidesk.server
import com.typesafe.config.Config
import io.github.config4k.getValue
import xyz.nulldev.ts.config.ConfigModule
import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.debugLogsEnabled
class ServerConfig(config: Config) : ConfigModule(config) {
val ip: String by config
@@ -21,7 +23,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
val socksProxyPort: String by config
// misc
val debugLogsEnabled: Boolean = System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("debugLogsEnabled")).toBoolean()
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
val systemTrayEnabled: Boolean by config
val initialOpenInBrowserEnabled: Boolean by config
@@ -7,16 +7,15 @@ package ir.armor.tachidesk.server
* License, v. 2.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.model.database.databaseUp
import ir.armor.tachidesk.server.util.systemTray
import ir.armor.tachidesk.server.util.AppMutex.handleAppMutex
import ir.armor.tachidesk.server.util.SystemTray.systemTray
import mu.KotlinLogging
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.conf.global
import org.kodein.di.singleton
import org.slf4j.Logger
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ApplicationRootDir
@@ -36,7 +35,7 @@ class ApplicationDirs(
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTray by lazy { systemTray() }
val systemTrayInstance by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() }
@@ -66,6 +65,9 @@ fun applicationSetup() {
ServerConfig.register(GlobalConfigManager.config)
)
// Make sure only one instance of the app is running
handleAppMutex()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
@@ -73,11 +75,6 @@ fun applicationSetup() {
// start app
androidCompat.startApp(App())
// set application wide logging level
if (serverConfig.debugLogsEnabled) {
(KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger).level = Level.DEBUG
}
// create conf file if doesn't exist
try {
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
@@ -97,7 +94,7 @@ fun applicationSetup() {
// create system tray
if (serverConfig.systemTrayEnabled) {
try {
systemTray
systemTrayInstance
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace()
}
@@ -1,4 +1,4 @@
package ir.armor.tachidesk.server.internal
package ir.armor.tachidesk.server.impl_internal
/*
* Copyright (C) Contributors to the Suwayomi project
@@ -0,0 +1,18 @@
package ir.armor.tachidesk.server.util
import mu.KotlinLogging
import kotlin.system.exitProcess
private val logger = KotlinLogging.logger {}
enum class ExitCode(val code: Int) {
Success(0),
MutexCheckFailedTachideskRunning(1),
MutexCheckFailedAnotherAppRunning(2);
}
fun shutdownApp(exitCode: ExitCode) {
logger.info("Shutting Down Tachidesk. Goodbye!")
exitProcess(exitCode.code)
}
@@ -0,0 +1,78 @@
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 io.javalin.plugin.json.JavalinJackson
import ir.armor.tachidesk.server.impl_internal.AboutDataClass
import ir.armor.tachidesk.server.serverConfig
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.Clear
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.OtherApplicationRunning
import ir.armor.tachidesk.server.util.AppMutex.AppMutexStat.TachideskInstanceRunning
import ir.armor.tachidesk.server.util.Browser.openInBrowser
import mu.KotlinLogging
import okhttp3.OkHttpClient
import okhttp3.Request.Builder
import java.io.IOException
import java.util.concurrent.TimeUnit
object AppMutex {
private val logger = KotlinLogging.logger {}
private enum class AppMutexStat(val stat: Int) {
Clear(0),
TachideskInstanceRunning(1),
OtherApplicationRunning(2)
}
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
private fun checkAppMutex(): AppMutexStat {
val client = OkHttpClient.Builder()
.connectTimeout(200, TimeUnit.MILLISECONDS)
.build()
val request = Builder()
.url("http://$appIP:${serverConfig.port}/api/v1/about/")
.build()
val response = try {
client.newCall(request).execute().use { response -> response.body!!.string() }
} catch (e: IOException) {
return AppMutexStat.Clear
}
return try {
JavalinJackson.fromJson(response, AboutDataClass::class.java)
AppMutexStat.TachideskInstanceRunning
} catch (e: IOException) {
AppMutexStat.OtherApplicationRunning
}
}
fun handleAppMutex() {
when (checkAppMutex()) {
Clear -> {
logger.info("Mutex status is clear, Resuming startup.")
}
TachideskInstanceRunning -> {
logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}")
logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.")
openInBrowser()
logger.info("Aborting startup.")
shutdownApp(ExitCode.MutexCheckFailedTachideskRunning)
}
OtherApplicationRunning -> {
logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.")
shutdownApp(ExitCode.MutexCheckFailedAnotherAppRunning)
}
}
}
}
@@ -0,0 +1,38 @@
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.util.Desktop
import ir.armor.tachidesk.server.serverConfig
object Browser {
private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip
private val appBaseUrl = "http://$appIP:${serverConfig.port}"
private val electronInstances = mutableListOf<Any>()
fun openInBrowser() {
val openInElectron = System.getProperty("ir.armor.tachidesk.webInterface")?.equals("electron")
if (openInElectron == true) {
try {
val electronPath = System.getProperty("ir.armor.tachidesk.electronPath")!!
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace()
}
} else {
try {
Desktop.browseURL(appBaseUrl)
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace()
}
}
}
}
@@ -9,60 +9,50 @@ package ir.armor.tachidesk.server.util
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.server.BuildConfig
import ir.armor.tachidesk.server.ServerConfig
import ir.armor.tachidesk.server.serverConfig
import kotlin.system.exitProcess
import ir.armor.tachidesk.server.util.Browser.openInBrowser
import ir.armor.tachidesk.server.util.ExitCode.Success
fun openInBrowser() {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.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(BuildConfig.name)
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk"
) {
openInBrowser()
}
)
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
mainMenu.add(
MenuItem("Quit") {
systemTray.shutdown()
exitProcess(0)
}
)
systemTray.installShutdownHook()
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
object SystemTray {
fun systemTray(): SystemTray? {
try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = serverConfig.debugLogsEnabled
CacheUtil.clear(BuildConfig.name)
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk"
) {
openInBrowser()
}
)
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
mainMenu.add(
MenuItem("Quit") {
shutdownApp(Success)
}
)
systemTray.installShutdownHook()
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
}
@@ -10,13 +10,13 @@ package ir.armor.tachidesk
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.impl.Extension.installExtension
import ir.armor.tachidesk.impl.Extension.uninstallExtension
import ir.armor.tachidesk.impl.Extension.updateExtension
import ir.armor.tachidesk.impl.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.Source.getSourceList
import ir.armor.tachidesk.impl.extension.Extension.installExtension
import ir.armor.tachidesk.impl.extension.Extension.uninstallExtension
import ir.armor.tachidesk.impl.extension.Extension.updateExtension
import ir.armor.tachidesk.impl.extension.ExtensionsList.getExtensionList
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.impl.util.lang.awaitSingle
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
import ir.armor.tachidesk.server.applicationSetup
import kotlinx.coroutines.Dispatchers
+12 -11
View File
@@ -2,19 +2,20 @@ plugins {
id("com.github.node-gradle.node") version "3.0.1"
}
val nodeRoot = "${project.projectDir}/react"
node {
nodeProjectDir.set(file("${project.projectDir}/react/"))
nodeProjectDir.set(file(nodeRoot))
}
tasks.named("yarn_build") {
dependsOn("yarn") // install node_modules
}
tasks {
register<Copy>("copyBuild") {
from(file("$nodeRoot/build"))
into(file("$rootDir/server/src/main/resources/react"))
tasks.register<Copy>("copyBuild") {
from(file("$rootDir/webUI/react/build"))
into(file("$rootDir/server/src/main/resources/react"))
}
dependsOn("yarn_build")
}
tasks.named("copyBuild") {
dependsOn("yarn_build")
}
named("yarn_build") {
dependsOn("yarn") // install node_modules
}
}
+3 -5
View File
@@ -26,9 +26,6 @@ const useStyles = makeStyles((theme) => ({
alignItems: 'center',
padding: 16,
},
read: {
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
},
bullet: {
display: 'inline-block',
margin: '0 2px',
@@ -80,16 +77,17 @@ export default function ChapterCard(props: IProps) {
.then(() => triggerChaptersUpdate());
};
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
return (
<>
<li>
<Card>
<CardContent className={`${classes.root} ${chapter.read && classes.read}`}>
<CardContent className={classes.root}>
<Link
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
style={{
textDecoration: 'none',
color: theme.palette.text.primary,
color: chapter.read ? readChapterColor : theme.palette.text.primary,
}}
>
<div style={{ display: 'flex' }}>
@@ -11,8 +11,11 @@ import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
import ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension';
import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/MoveToInbox';
import SettingsIcon from '@material-ui/icons/Settings';
import { Link } from 'react-router-dom';
const useStyles = makeStyles({
@@ -47,7 +50,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Library">
<ListItemIcon>
<InboxIcon />
<CollectionsBookmarkIcon />
</ListItemIcon>
<ListItemText primary="Library" />
</ListItem>
@@ -55,7 +58,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Extensions">
<ListItemIcon>
<InboxIcon />
<ExtensionIcon />
</ListItemIcon>
<ListItemText primary="Extensions" />
</ListItem>
@@ -63,7 +66,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/sources" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Sources">
<ListItemIcon>
<InboxIcon />
<ExploreIcon />
</ListItemIcon>
<ListItemText primary="Sources" />
</ListItem>
@@ -71,7 +74,7 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings">
<ListItemIcon>
<InboxIcon />
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="Settings" />
</ListItem>
+1 -1
View File
@@ -72,7 +72,7 @@ export default function Library() {
const defaultCategoryTab = {
category: {
name: 'Default',
isLanding: true,
default: true,
order: 0,
id: -1,
},
+4 -3
View File
@@ -7,7 +7,8 @@
import React, { useContext, useEffect, useState } from 'react';
import List from '@material-ui/core/List';
import InboxIcon from '@material-ui/icons/Inbox';
import ListAltIcon from '@material-ui/icons/ListAlt';
import BackupIcon from '@material-ui/icons/Backup';
import Brightness6Icon from '@material-ui/icons/Brightness6';
import DnsIcon from '@material-ui/icons/Dns';
import EditIcon from '@material-ui/icons/Edit';
@@ -50,13 +51,13 @@ export default function Settings() {
<List style={{ padding: 0 }}>
<ListItemLink href="/settings/categories">
<ListItemIcon>
<InboxIcon />
<ListAltIcon />
</ListItemIcon>
<ListItemText primary="Categories" />
</ListItemLink>
<ListItemLink href="/settings/backup">
<ListItemIcon>
<InboxIcon />
<BackupIcon />
</ListItemIcon>
<ListItemText primary="Backup" />
</ListItemLink>
+29 -12
View File
@@ -28,8 +28,9 @@ import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import NavbarContext from '../../context/NavbarContext';
import client from '../../util/client';
@@ -49,7 +50,8 @@ export default function Categories() {
const [categories, setCategories] = useState([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState('');
const [dialogName, setDialogName] = useState('');
const [dialogDefault, setDialogDefault] = useState(false);
const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
@@ -93,7 +95,8 @@ export default function Categories() {
};
const resetDialog = () => {
setDialogValue('');
setDialogName('');
setDialogDefault(false);
setCategoryToEdit(-1);
};
@@ -102,6 +105,13 @@ export default function Categories() {
setDialogOpen(true);
};
const handleEditDialogOpen = (index) => {
setDialogName(categories[index].name);
setDialogDefault(categories[index].default);
setCategoryToEdit(index);
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
@@ -110,7 +120,8 @@ export default function Categories() {
setDialogOpen(false);
const formData = new FormData();
formData.append('name', dialogValue);
formData.append('name', dialogName);
formData.append('default', dialogDefault);
if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData)
@@ -161,8 +172,7 @@ export default function Categories() {
/>
<IconButton
onClick={() => {
handleDialogOpen();
setCategoryToEdit(index);
handleEditDialogOpen(index);
}}
>
<EditIcon />
@@ -197,12 +207,9 @@ export default function Categories() {
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
{categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
</DialogTitle>
<DialogContent>
<DialogContentText>
Enter new category name.
</DialogContentText>
<TextField
autoFocus
margin="dense"
@@ -210,8 +217,18 @@ export default function Categories() {
label="Category Name"
type="text"
fullWidth
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value)}
value={dialogName}
onChange={(e) => setDialogName(e.target.value)}
/>
<FormControlLabel
control={(
<Checkbox
checked={dialogDefault}
onChange={(e) => setDialogDefault(e.target.checked)}
color="default"
/>
)}
label="Default category when adding new manga to library"
/>
</DialogContent>
<DialogActions>
+1 -1
View File
@@ -80,7 +80,7 @@ interface ICategory {
id: number
order: number
name: String
isLanding: boolean
default: boolean
}
interface INavbarOverride {