Compare commits
140 Commits
v0.3.0-rc1
...
v0.3.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f55460ffb | |||
| fbc5bd4642 | |||
| 5e0c7d3c9d | |||
| 083996a48d | |||
| 9d38f478e3 | |||
| 57274a0a01 | |||
| b3b56b7fc8 | |||
| 0b690577da | |||
| e9683a3a37 | |||
| f8f67b3eba | |||
| 7b16b082d8 | |||
| 2a783f0d8e | |||
| 42ae32de33 | |||
| cec7ddc486 | |||
| 9c55fc3868 | |||
| 104c5a8d83 | |||
| 7450b16742 | |||
| 3ecd0931a1 | |||
| 2f2a52ae2f | |||
| f464087c30 | |||
| 2364960388 | |||
| 76be4d64cd | |||
| 7d98e8ce47 | |||
| 40831fc681 | |||
| e38e7ccf26 | |||
| 98b9e2f2cf | |||
| 4bf3c12f76 | |||
| bab25f9ad9 | |||
| a62ee8f8c3 | |||
| 5f23691e20 | |||
| 3de9ccc62f | |||
| 1896f7f37b | |||
| 490643dc02 | |||
| 9808976088 | |||
| 5a73068a10 | |||
| 01d5c2540d | |||
| 866b01f865 | |||
| da6a953099 | |||
| bce8d58845 | |||
| 3cfce2db04 | |||
| 327aae5dd9 | |||
| 1bdfde7032 | |||
| 295a0817b0 | |||
| a02dc02d52 | |||
| dc012edf7d | |||
| 1e2eb11c13 | |||
| 3a825f4f25 | |||
| b9ea8c5f74 | |||
| 320d7e2536 | |||
| c200785479 | |||
| 8abb132ad6 | |||
| 8bb2269f36 | |||
| 9d17b26283 | |||
| 5909f15db7 | |||
| 11672ca576 | |||
| e09773def3 | |||
| f6d4432e6f | |||
| 45a6abc5c2 | |||
| dc5e677a38 | |||
| a82549dc17 | |||
| a002e19d9d | |||
| cdf1f98d28 | |||
| 0ff1ebdeb7 | |||
| 17f4a396f8 | |||
| 8aa3cf4368 | |||
| 0136c5e493 | |||
| 8b94b9ee80 | |||
| bed63f19f2 | |||
| e2a6545a84 | |||
| e3d3ec6895 | |||
| 7ba476bd79 | |||
| 2dd41ebd27 | |||
| 038df78171 | |||
| 6e5ff2b508 | |||
| ec8d1e8680 | |||
| 1f0f0c33b7 | |||
| 825940fcac | |||
| 4618834af2 | |||
| 55d968df5e | |||
| 63a078cf7d | |||
| 5304917e53 | |||
| 831b74d2ec | |||
| 1bad9dcd69 | |||
| dd43716851 | |||
| f2e55e95a2 | |||
| 14658a0c4d | |||
| 4195e7056b | |||
| 1d29e8b248 | |||
| b718c718df | |||
| a3601cf1b5 | |||
| 0236a9639b | |||
| 5f4c7454ee | |||
| 773120c96a | |||
| 4b273c6bf9 | |||
| b626aa66ba | |||
| 1dd029559e | |||
| 59cbe5d5bc | |||
| 40d1173653 | |||
| bf6a0aba5d | |||
| 34d8feacdd | |||
| 1ea821584c | |||
| 3d2fee19bb | |||
| 449d12779a | |||
| 6fb6a251e7 | |||
| 4d6220f894 | |||
| fe747bfc52 | |||
| 0c2d038870 | |||
| 4e3f73af75 | |||
| 63e5e1b45f | |||
| 2e1558bd96 | |||
| 0671dee8b2 | |||
| 8f91b8089a | |||
| 009b45f676 | |||
| 8f7d5eb311 | |||
| f3de835ef3 | |||
| fd6662f428 | |||
| fde137b3ed | |||
| a1349aa0e3 | |||
| c9ef5f9b9d | |||
| 8fbf564177 | |||
| ae0b1a818c | |||
| c04cc780b7 | |||
| 71ad1bb6e3 | |||
| c1be77ee9b | |||
| d1fa857ffb | |||
| 93fd81b38b | |||
| 2f116b40b2 | |||
| b884d34bdf | |||
| 309803368b | |||
| 19fc5be8f3 | |||
| c28fac14c0 | |||
| 66e38de29f | |||
| 282cb1d3be | |||
| b741ded595 | |||
| 6b290695fc | |||
| 4e43c554c0 | |||
| 090a72b35f | |||
| 3fcc269df3 | |||
| 9958e0eb34 | |||
| c5269002a2 |
@@ -1,24 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
git lfs install
|
cp master/server/build/Tachidesk-*.jar preview
|
||||||
#git lfs track "*.zip"
|
cd preview
|
||||||
|
|
||||||
cp ../master/repo/* .
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
new_jar_build=$(ls *.jar| tail -1)
|
||||||
echo "last jar build file name: $new_jar_build"
|
echo "last jar build file name: $new_jar_build"
|
||||||
|
|
||||||
new_win32_build=$(ls *.zip| tail -1)
|
|
||||||
echo "last win32 build file name: $new_win32_build"
|
|
||||||
|
|
||||||
cp -f $new_jar_build Tachidesk-latest.jar
|
cp -f $new_jar_build Tachidesk-latest.jar
|
||||||
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
|
||||||
|
rm -rf latest_pointer/*
|
||||||
|
cp $new_jar_build latest_pointer
|
||||||
|
cp ../master/server/build/Tachidesk-*.zip latest_pointer
|
||||||
|
|
||||||
|
latest=$(ls *.jar | tail -n1 | 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.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
git status
|
git status
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Update repo"
|
git commit -m "Update preview repository"
|
||||||
git push
|
git push
|
||||||
else
|
else
|
||||||
echo "No changes to commit"
|
echo "No changes to commit"
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Get last commit message
|
|
||||||
#last_commit_log=$(git log -1 --pretty=format:"%s")
|
|
||||||
#echo "last commit log: $last_commit_log"
|
|
||||||
#
|
|
||||||
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
|
||||||
#echo "count is: $filter_count"
|
|
||||||
|
|
||||||
mkdir -p repo/
|
|
||||||
cp server/build/Tachidesk-*.jar repo/
|
|
||||||
cp server/build/Tachidesk-*.zip repo/
|
|
||||||
|
|
||||||
ls repo
|
|
||||||
pwd
|
|
||||||
|
|
||||||
#if [ "$filter_count" -gt 0 ]; then
|
|
||||||
# cp server/build/Tachidesk-*.jar repo/
|
|
||||||
# cp server/build/Tachidesk-*.zip repo/
|
|
||||||
#fi
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
name: CI
|
name: CI Pull Request
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -33,7 +30,7 @@ jobs:
|
|||||||
- name: Checkout master branch
|
- name: Checkout master branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
path: master
|
path: master
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: CI build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_wrapper:
|
||||||
|
name: Validate Gradle Wrapper
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build FatJar
|
||||||
|
needs: check_wrapper
|
||||||
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.5.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Checkout master branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
path: master
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Download android.jar
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/react/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Build and copy webUI, Build Jar
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
build-root-directory: master
|
||||||
|
wrapper-directory: master
|
||||||
|
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: make windows package
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows-bundler.sh
|
||||||
|
|
||||||
|
- name: Checkout preview branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: preview
|
||||||
|
path: preview
|
||||||
|
|
||||||
|
- name: Deploy preview
|
||||||
|
run: |
|
||||||
|
./master/.github/scripts/commit-preview.sh
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Publish
|
name: CI Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -56,60 +56,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
- name: Build and copy webUI, Build Jar and launch4j
|
- name: Build and copy webUI, Build Jar
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :webUI:copyBuild :server:windowsPackage --stacktrace
|
arguments: :webUI:copyBuild :server:shadowJar --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
- name: make windows package
|
||||||
- name: Create repo artifacts
|
|
||||||
run: |
|
run: |
|
||||||
cd master
|
cd master/scripts
|
||||||
./.github/scripts/create-repo.sh
|
./windows-bundler.sh
|
||||||
|
|
||||||
- name: Upload Release
|
- name: Upload Release
|
||||||
uses: xresloader/upload-to-github-release@master
|
uses: xresloader/upload-to-github-release@master
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
file: "master/repo/*"
|
file: "master/server/build/*.jar;master/server/build/*.zip"
|
||||||
tags: true
|
tags: true
|
||||||
draft: true
|
draft: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
# - name: Create Release
|
|
||||||
# id: create_release
|
|
||||||
# uses: actions/create-release@v1
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# tag_name: ${{ github.ref }}
|
|
||||||
# release_name: Release ${{ github.ref }}
|
|
||||||
# body: |
|
|
||||||
# Release body
|
|
||||||
# draft: false
|
|
||||||
# prerelease: true
|
|
||||||
#
|
|
||||||
# - name: Get the Ref
|
|
||||||
# id: get-ref
|
|
||||||
# uses: ankitvgupta/ref-to-tag-action@master
|
|
||||||
# with:
|
|
||||||
# ref: ${{ github.ref }}
|
|
||||||
# head_ref: ${{ github.head_ref }}
|
|
||||||
#
|
|
||||||
# - name: Get the tag
|
|
||||||
# run: echo "The tag was ${{ steps.get-ref.outputs.tag }}"
|
|
||||||
#
|
|
||||||
# - name: Upload Release
|
|
||||||
# uses: AButler/upload-release-assets@v2.0
|
|
||||||
# with:
|
|
||||||
# files: 'master/repo/*'
|
|
||||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# release-tag: ${{ steps.get-ref.outputs.tag }}
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ plugins {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Write-Output "Getting required Android.jar..."
|
|||||||
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
Remove-Item -Recurse -Force "tmp" -ErrorAction SilentlyContinue | Out-Null
|
||||||
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
New-Item -ItemType Directory -Force -Path "tmp" | Out-Null
|
||||||
|
|
||||||
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT").content
|
$androidEncoded = (Invoke-WebRequest -Uri "https://android.googlesource.com/platform/prebuilts/sdk/+/3b8a524d25fa6c3d795afb1eece3f24870c60988/27/public/android.jar?format=TEXT" -UseBasicParsing).content
|
||||||
|
|
||||||
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
$android_jar = (Get-Location).Path + "\tmp\android.jar"
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,17 @@
|
|||||||
|
|
||||||
# This is a bash script to create android.jar stubs
|
# This is a bash script to create android.jar stubs
|
||||||
|
|
||||||
|
for dep in "curl" "base64" "zip"
|
||||||
|
do
|
||||||
|
which $dep >/dev/null 2>&1 || { echo >&2 "Error: This script needs $dep installed."; abort=yes; }
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $abort = yes ]; then
|
||||||
|
echo "Some of the dependencies didn't exist. Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
# foolproof against running from AndroidCompat dir instead of running from project root
|
# foolproof against running from AndroidCompat dir instead of running from project root
|
||||||
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
if [ "$(basename $(pwd))" = "AndroidCompat" ]; then
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
package com.squareup.duktape;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) 2015 Square, Inc.
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* you may not use this file except in compliance with the License.
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.squareup.duktape;
|
||||||
|
|
||||||
import kotlin.NotImplementedError;
|
import kotlin.NotImplementedError;
|
||||||
|
|
||||||
@@ -64,18 +73,18 @@ public final class Duktape implements Closeable, AutoCloseable {
|
|||||||
throw new NotImplementedError("Not implemented!");
|
throw new NotImplementedError("Not implemented!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
* Attaches to a global JavaScript object called {@code name} that implements {@code type}.
|
||||||
// * {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
* {@code type} defines the interface implemented in JavaScript that will be accessible to Java.
|
||||||
// * {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
* {@code type} must be an interface that does not extend any other interfaces, and cannot define
|
||||||
// * any overloaded methods.
|
* any overloaded methods.
|
||||||
// * <p>Methods of the interface may return {@code void} or any of the following supported argument
|
* <p>Methods of the interface may return {@code void} or any of the following supported argument
|
||||||
// * types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
* types: {@code boolean}, {@link Boolean}, {@code int}, {@link Integer}, {@code double},
|
||||||
// * {@link Double}, {@link String}.
|
* {@link Double}, {@link String}.
|
||||||
// */
|
*/
|
||||||
// public synchronized <T> T get(final String name, final Class<T> type) {
|
public synchronized <T> T get(final String name, final Class<T> type) {
|
||||||
// throw new NotImplementedError("Not implemented!");
|
throw new NotImplementedError("Not implemented!");
|
||||||
// }
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Release the native resources associated with this object. You <strong>must</strong> call this
|
* Release the native resources associated with this object. You <strong>must</strong> call this
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
package com.squareup.duktape;
|
package com.squareup.duktape;
|
||||||
|
|
||||||
/*
|
/* part of tachiyomi-extensions which is licensed under Apache License Version 2.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/. */
|
|
||||||
// part of tachiyomi-extensions which was originally licensed under Apache License Version 2.0
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Code Of Conduct
|
||||||
|
- Don't be a dick.
|
||||||
|
|
||||||
|
# expanding the code of conduct!
|
||||||
|
The contents of this document is up for debate and improvement! Discussions on discord.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Contributing
|
||||||
|
## Where should I start?
|
||||||
|
Checkout [This Kanban Board](https://github.com/Suwayomi/Tachidesk/projects/1) to see the rough development roadmap.
|
||||||
|
|
||||||
|
**Note to potential contributors:** Notify the developers on Suwayomi discord (#programming channel) or open a WIP pull request before starting if you decide to take on working on anything from/not from the roadmap in order to avoid parallel efforts on the same issue/feature.
|
||||||
|
|
||||||
|
## How does Tachidesk work?
|
||||||
|
This project has two components:
|
||||||
|
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||||
|
2. **webUI:** A react SPA(`create-react-app`) project that works with the server to do the presentation.
|
||||||
|
|
||||||
|
## Why a web app?
|
||||||
|
This structure is chosen to
|
||||||
|
- Achieve the maximum multi-platform-ness
|
||||||
|
- Gives the ability to acces Tachidesk from a remote web browser e.g. your phone, tablet or smart TV
|
||||||
|
- Eaise development of alternative user intefaces for Tachidesk
|
||||||
|
|
||||||
|
## User Interfaces for Tachidesk server
|
||||||
|
Currently there are three known interfaces for Tachidesk:
|
||||||
|
1. [webUI](https://github.com/Suwayomi/Tachidesk/tree/master/webUI/react): The react SPA that Tachidesk is traditionally shipped with.
|
||||||
|
2. [TachideskJUI](https://github.com/Suwayomi/TachideskJUI): A Jetbrains Compose Native app, re-uses components made for the upcoming Tachiyomi 1.x
|
||||||
|
3. [Equinox](https://github.com/Suwayomi/Equinox): A web user interface made with Vue.js, in super early stages of development.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
### Prerequisites
|
||||||
|
You need these software packages installed in order to build the project
|
||||||
|
### Server
|
||||||
|
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
||||||
|
- Android stubs jar
|
||||||
|
- Manual download: Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
|
- Automated download: Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
|
### webUI
|
||||||
|
- Nodejs LTS or latest
|
||||||
|
- Yarn
|
||||||
|
- Git
|
||||||
|
### building the full-blown jar
|
||||||
|
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
|
### building without `webUI` bundled(server only)
|
||||||
|
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
|
### building the Windows package
|
||||||
|
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||||
|
## Running in development mode
|
||||||
|
First satistify [the prerequisites](#prerequisites)
|
||||||
|
### server
|
||||||
|
run `./gradlew :server:run --stacktrace` to run the server
|
||||||
|
### webUI
|
||||||
|
How to do it is described in `webUI/react/README.md` but for short,
|
||||||
|
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
||||||
|
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically,
|
||||||
|
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||||
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
|
||||||

|
| Build | Stable | Preview | Support Server |
|
||||||
|
|-------|----------|---------|---------|
|
||||||
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [](https://discord.gg/DDZdqZWaHA) |
|
||||||
|
|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
|
<img src="https://github.com/Suwayomi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png" alt="drawing" width="200"/>
|
||||||
|
|
||||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it.
|
Tachidesk is an independent Tachiyomi compatible software and is **not a Fork of** Tachiyomi.
|
||||||
|
|
||||||
|
Tachidesk is as multi-platform as you can get. Any platform that runs java and/or has a modern browser can run it. This includes Windows, Linux, macOS, chrome OS, etc. Follow [Downloading and Running the app](#downloading-and-running-the-app) for installation instructions.
|
||||||
|
|
||||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||||
|
|
||||||
@@ -15,18 +22,17 @@ Here is a list of current features:
|
|||||||
- Searching and browsing installed sources.
|
- Searching and browsing installed sources.
|
||||||
- A decent chapter reader.
|
- A decent chapter reader.
|
||||||
- Ability to download Mangas for offline read(This partially works)
|
- Ability to download Mangas for offline read(This partially works)
|
||||||
|
- Backup and restore support powered by Tachiyomi Legacy Backups
|
||||||
|
|
||||||
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update. See [Troubleshooting](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting) if it happens.
|
||||||
|
|
||||||
Anyways, for more info checkout [finished milestone #1](https://github.com/Suwayomi/Tachidesk/issues/2) and [milestone #2](https://github.com/Suwayomi/Tachidesk/projects/1) to see what's implemented in more detail.
|
|
||||||
|
|
||||||
## Downloading and Running the app
|
## Downloading and Running the app
|
||||||
### All Operating Systems
|
### All Operating Systems
|
||||||
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
You should have The Java Runtime Environment(JRE) 8 or newer and a modern browser installed(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 jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
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).
|
||||||
|
|
||||||
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` (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
|
### Windows
|
||||||
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
Download the latest win32 release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||||
@@ -38,55 +44,22 @@ You can install Tachidesk from the AUR
|
|||||||
```
|
```
|
||||||
yay -S tachidesk
|
yay -S tachidesk
|
||||||
```
|
```
|
||||||
|
Or the latest preview version
|
||||||
|
```
|
||||||
|
yay -S tachidesk-preview
|
||||||
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||||
|
|
||||||
## General troubleshooting
|
### Using Tachidesk Remotely
|
||||||
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
|
You can run Tachidesk on your computer or a server and connect to it remotely through the web interface with a web browser on any device including a mobile or tablet or even your smart TV!, this method of using Tachidesk is only recommended if you are a power user and know what you are doing.
|
||||||
|
|
||||||
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
## Troubleshooting and Support
|
||||||
|
See [this troubleshooting wiki page](https://github.com/Suwayomi/Tachidesk/wiki/Troubleshooting).
|
||||||
|
|
||||||
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
## Contributing and Technical info
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
On Windows 7 and later : `C:\Users\<Account>\AppData\Local\Tachidesk`
|
|
||||||
|
|
||||||
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
|
||||||
|
|
||||||
## Support and help
|
|
||||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
This project has two components:
|
|
||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
|
||||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
|
||||||
|
|
||||||
## Building from source
|
|
||||||
### Prerequisite: Get Android stubs jar
|
|
||||||
#### Manual download
|
|
||||||
Download [android.jar](https://raw.githubusercontent.com/Suwayomi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
|
||||||
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
|
||||||
Run `AndroidCompat/getAndroid.sh`(MacOS/Linux) or `AndroidCompat/getAndroid.ps1`(Windows) from project's root directory to download and rebuild the jar file from Google's repository.
|
|
||||||
### Prerequisite: Software dependencies
|
|
||||||
You need this software packages installed in order to build this project:
|
|
||||||
- Java Development Kit and Java Runtime Environment version 8 or newer(both Oracle JDK and OpenJDK works)
|
|
||||||
- Nodejs LTS or latest
|
|
||||||
- Yarn
|
|
||||||
### building the full-blown jar
|
|
||||||
Run `./gradlew :webUI:copyBuild server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### building without `webUI` bundled(server only)
|
|
||||||
Delete the `server/src/main/resources/react` directory if exists from previous runs, then run `./gradlew server:shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
|
||||||
### building the Windows package
|
|
||||||
Run `./gradlew :server:windowsPackage` to build a server only bundle and `./gradlew :webUI:copyBuild :server:windowsPackage` to get a full bundle , the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
|
||||||
## Running for development purposes
|
|
||||||
### `server` module
|
|
||||||
Follow [Get Android stubs jar](#prerequisite-get-android-stubs-jar) then run `./gradlew :server:run --stacktrace` to run the server
|
|
||||||
### `webUI` module
|
|
||||||
How to do it is described in `webUI/react/README.md` but for short,
|
|
||||||
first cd into `webUI/react` then run `yarn` to install the node modules(do this only once)
|
|
||||||
then `yarn start` to start the development server, if a new browser window doesn't get opned automatically,
|
|
||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
|
||||||
and supports HMR and all the other goodies you'll need.
|
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
This project is a spiritual successor of [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server), Many of the ideas and the groundwork adopted in this project comes from TachiWeb.
|
||||||
|
|||||||
+12
-17
@@ -1,8 +1,7 @@
|
|||||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.4.21" apply false // Also in buildSrc Config.kt
|
kotlin("jvm") version "1.4.32"
|
||||||
id("java")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@@ -11,12 +10,10 @@ allprojects {
|
|||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://maven.google.com/")
|
maven("https://maven.google.com/")
|
||||||
maven("https://jitpack.io")
|
maven("https://jitpack.io")
|
||||||
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
maven("https://dl.bintray.com/inorichi/maven")
|
|
||||||
maven("https://dl.google.com/dl/android/maven2/")
|
maven("https://dl.google.com/dl/android/maven2/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +25,6 @@ val projects = listOf(
|
|||||||
)
|
)
|
||||||
|
|
||||||
configure(projects) {
|
configure(projects) {
|
||||||
apply(plugin = "java")
|
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
apply(plugin = "org.jetbrains.kotlin.jvm")
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -36,33 +32,32 @@ configure(projects) {
|
|||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation(kotlin("stdlib", KotlinCompilerVersion.VERSION))
|
implementation(kotlin("reflect"))
|
||||||
implementation(kotlin("reflect", version = "1.4.21"))
|
testImplementation(kotlin("test"))
|
||||||
testImplementation(kotlin("test", version = "1.4.21"))
|
|
||||||
|
|
||||||
// coroutines
|
// coroutines
|
||||||
val coroutinesVersion = "1.4.2"
|
val coroutinesVersion = "1.4.3"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$coroutinesVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
|
||||||
|
|
||||||
|
|
||||||
// Dependency Injection
|
// Dependency Injection
|
||||||
implementation("org.kodein.di:kodein-di-conf-jvm:7.1.0")
|
implementation("org.kodein.di:kodein-di-conf-jvm:7.5.0")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("org.slf4j:slf4j-api:1.7.30")
|
implementation("org.slf4j:slf4j-api:1.7.30")
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
implementation("ch.qos.logback:logback-classic:1.2.3")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.0.3")
|
implementation("io.github.microutils:kotlin-logging:2.0.6")
|
||||||
|
|
||||||
// RxJava
|
// RxJava
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
@@ -73,11 +68,11 @@ configure(projects) {
|
|||||||
|
|
||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.0")
|
implementation("com.typesafe:config:1.4.1")
|
||||||
implementation("io.github.config4k:config4k:0.4.2")
|
implementation("io.github.config4k:config4k:0.4.2")
|
||||||
|
|
||||||
// to get application content root
|
// to get application content root
|
||||||
implementation("net.harawata:appdirs:1.2.0")
|
implementation("net.harawata:appdirs:1.2.1")
|
||||||
|
|
||||||
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
// dex2jar: https://github.com/DexPatcher/dex2jar/releases/tag/v2.1-20190905-lanchon
|
||||||
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
implementation("com.github.DexPatcher.dex2jar:dex-tools:v2.1-20190905-lanchon")
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
start "" "jre/bin/javaw -jar Tachidesk.jar"
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/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"
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $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)-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.bat $release_name
|
||||||
|
cp resources/Tachidesk-debug.bat $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
cp $zip_name ../server/build/
|
||||||
+86
-106
@@ -1,29 +1,25 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.FormatTask
|
||||||
|
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// id("org.jetbrains.kotlin.jvm") version "1.4.21"
|
|
||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||||
id("org.jmailen.kotlinter") version "3.3.0"
|
id("org.jmailen.kotlinter") version "3.4.3"
|
||||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
id("edu.sc.seis.launch4j") version "2.5.0"
|
||||||
|
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.2.7"
|
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://jitpack.io")
|
url = uri("https://jitpack.io")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
// using source class from tachiyomi commit 9493577de27c40ce8b2b6122cc447d025e34c477 to not depend on tachiyomi.sourceapi
|
||||||
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
// implementation("tachiyomi.sourceapi:source-api:1.1")
|
||||||
@@ -34,10 +30,10 @@ dependencies {
|
|||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.9.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
|
|
||||||
// retrofit
|
// Retrofit
|
||||||
val retrofitVersion = "2.9.0"
|
val retrofitVersion = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
|
||||||
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
|
||||||
@@ -45,13 +41,10 @@ dependencies {
|
|||||||
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
implementation("com.squareup.retrofit2:adapter-rxjava:$retrofitVersion")
|
||||||
|
|
||||||
|
|
||||||
// reactivex
|
// Reactivex
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
// implementation("io.reactivex:rxandroid:1.2.1")
|
|
||||||
// implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
|
||||||
// implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
implementation("com.google.code.gson:gson:2.8.6")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
@@ -60,19 +53,24 @@ dependencies {
|
|||||||
|
|
||||||
|
|
||||||
// api
|
// api
|
||||||
implementation("io.javalin:javalin:3.12.0")
|
implementation("io.javalin:javalin:3.13.6")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.12.3")
|
||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposedVersion = "0.28.1"
|
val exposedVersion = "0.31.1"
|
||||||
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
|
||||||
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
|
||||||
implementation("com.h2database:h2:1.4.199")
|
implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion")
|
||||||
|
|
||||||
|
// current database driver
|
||||||
|
implementation("com.h2database:h2:1.4.200")
|
||||||
|
|
||||||
// tray icon
|
// tray icon
|
||||||
implementation("com.dorkbox:SystemTray:3.17")
|
implementation("com.dorkbox:SystemTray:4.1")
|
||||||
|
implementation("com.dorkbox:Utilities:1.9")
|
||||||
|
|
||||||
|
implementation("com.google.guava:guava:30.1.1-jre")
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
@@ -85,12 +83,9 @@ dependencies {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = "ir.armor.tachidesk.Main"
|
val MainClass = "ir.armor.tachidesk.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(name)
|
mainClass.set(MainClass)
|
||||||
|
|
||||||
// Required by ShadowJar.
|
|
||||||
mainClassName = name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -101,7 +96,11 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskRevision = Runtime
|
// should be bumped with each stable release
|
||||||
|
val tachideskVersion = "v0.3.4"
|
||||||
|
|
||||||
|
// counts commit count on master
|
||||||
|
val tachideskRevision = Runtime
|
||||||
.getRuntime()
|
.getRuntime()
|
||||||
.exec("git rev-list master --count")
|
.exec("git rev-list master --count")
|
||||||
.let { process ->
|
.let { process ->
|
||||||
@@ -114,102 +113,83 @@ val TachideskRevision = Runtime
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfig {
|
||||||
|
appName = rootProject.name
|
||||||
|
clsName = "BuildConfig"
|
||||||
|
packageName = "ir.armor.tachidesk.server"
|
||||||
|
version = tachideskVersion
|
||||||
|
|
||||||
|
|
||||||
|
buildConfigField("String", "name", rootProject.name) // alias for BuildConfig.NAME
|
||||||
|
buildConfigField("String", "version", tachideskVersion) // alias for BuildConfig.VERSION
|
||||||
|
buildConfigField("String", "revision", tachideskRevision)
|
||||||
|
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
launch4j { //used for windows
|
||||||
|
mainClassName = MainClass
|
||||||
|
bundledJrePath = "jre"
|
||||||
|
bundledJre64Bit = true
|
||||||
|
jreMinVersion = "8"
|
||||||
|
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win64"
|
||||||
|
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||||
|
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
||||||
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
"Main-Class" to "com.example.MainKt", //will make your jar (produced by jar task) runnable
|
"Main-Class" to MainClass,
|
||||||
"ImplementationTitle" to project.name,
|
"Implementation-Title" to rootProject.name,
|
||||||
"Implementation-Version" to project.version)
|
"Implementation-Vendor" to "The Suwayomi Project",
|
||||||
|
"Specification-Version" to tachideskVersion,
|
||||||
|
"Implementation-Version" to tachideskRevision
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
||||||
archiveBaseName.set("Tachidesk")
|
archiveBaseName.set(rootProject.name)
|
||||||
archiveVersion.set(TachideskVersion)
|
archiveVersion.set(tachideskVersion)
|
||||||
archiveClassifier.set(TachideskRevision)
|
archiveClassifier.set(tachideskRevision)
|
||||||
}
|
}
|
||||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs = listOf(
|
freeCompilerArgs = listOf(
|
||||||
"-Xopt-in=kotlin.RequiresOptIn",
|
"-Xopt-in=kotlin.RequiresOptIn",
|
||||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
test {
|
test {
|
||||||
useJUnit()
|
useJUnit()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
launch4j { //used for windows
|
withType<ShadowJar> {
|
||||||
mainClassName = name
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
bundledJrePath = "jre"
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
bundledJre64Bit = true
|
}
|
||||||
jreMinVersion = "8"
|
|
||||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
named("run") {
|
||||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
}
|
||||||
}
|
|
||||||
|
named<Copy>("processResources") {
|
||||||
tasks.register<Zip>("windowsPackage") {
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
|
mustRunAfter(":webUI:copyBuild")
|
||||||
destinationDirectory.set(File("$buildDir"))
|
}
|
||||||
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
|
|
||||||
dependsOn("windowsPackageWorkaround2")
|
withType<LintTask> {
|
||||||
}
|
source(files("src"))
|
||||||
|
}
|
||||||
tasks.register<Delete>("windowsPackageWorkaround2") {
|
|
||||||
delete(
|
withType<FormatTask> {
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
|
source(files("src"))
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
|
||||||
)
|
|
||||||
dependsOn("windowsPackageWorkaround")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Copy>("windowsPackageWorkaround") {
|
|
||||||
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
dependsOn("deleteUnwantedJreDir")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Delete>("deleteUnwantedJreDir") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
|
|
||||||
)
|
|
||||||
dependsOn("addJreToDistributable")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<Copy>("addJreToDistributable") {
|
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
|
||||||
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
|
|
||||||
eachFile {
|
|
||||||
path = path.replace(".*-jre".toRegex(),"jre")
|
|
||||||
}
|
}
|
||||||
dependsOn("downloadJre")
|
|
||||||
dependsOn("createExe")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
|
|
||||||
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
dest(buildDir)
|
|
||||||
overwrite(false)
|
|
||||||
onlyIfModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("run") {
|
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,7 @@ package ir.armor.tachidesk
|
|||||||
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
import ir.armor.tachidesk.server.JavalinSetup.javalinSetup
|
||||||
import ir.armor.tachidesk.server.applicationSetup
|
import ir.armor.tachidesk.server.applicationSetup
|
||||||
|
|
||||||
class Main {
|
fun main() {
|
||||||
companion object {
|
applicationSetup()
|
||||||
|
javalinSetup()
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
applicationSetup()
|
|
||||||
javalinSetup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ package ir.armor.tachidesk.impl
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ package ir.armor.tachidesk.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
import ir.armor.tachidesk.model.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.model.dataclass.CategoryDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import eu.kanade.tachiyomi.source.model.SManga
|
|||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
@@ -25,53 +28,81 @@ import org.jetbrains.exposed.sql.update
|
|||||||
|
|
||||||
object Chapter {
|
object Chapter {
|
||||||
/** get chapter list when showing a manga */
|
/** get chapter list when showing a manga */
|
||||||
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
return if (!onlineFetch) {
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
transaction {
|
||||||
|
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
|
||||||
val chapterList = source.fetchChapterList(
|
.map {
|
||||||
SManga.create().apply {
|
ChapterTable.toDataClass(it)
|
||||||
title = mangaDetails.title
|
|
||||||
url = mangaDetails.url
|
|
||||||
}
|
|
||||||
).awaitSingle()
|
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
|
||||||
|
|
||||||
return transaction {
|
|
||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
ChapterTable.insert {
|
|
||||||
it[url] = fetchedChapter.url
|
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
|
||||||
it[manga] = mangaId
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
} else {
|
||||||
it[name] = fetchedChapter.name
|
|
||||||
it[date_upload] = fetchedChapter.date_upload
|
|
||||||
it[chapter_number] = fetchedChapter.chapter_number
|
|
||||||
it[scanlator] = fetchedChapter.scanlator
|
|
||||||
|
|
||||||
it[chapterIndex] = index + 1
|
val mangaDetails = getManga(mangaId)
|
||||||
it[manga] = mangaId
|
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||||
|
val chapterList = source.fetchChapterList(
|
||||||
|
SManga.create().apply {
|
||||||
|
title = mangaDetails.title
|
||||||
|
url = mangaDetails.url
|
||||||
|
}
|
||||||
|
).awaitSingle()
|
||||||
|
|
||||||
|
val chapterCount = chapterList.count()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
|
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||||
|
if (chapterEntry == null) {
|
||||||
|
ChapterTable.insert {
|
||||||
|
it[url] = fetchedChapter.url
|
||||||
|
it[name] = fetchedChapter.name
|
||||||
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = index + 1
|
||||||
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
|
||||||
|
it[name] = fetchedChapter.name
|
||||||
|
it[date_upload] = fetchedChapter.date_upload
|
||||||
|
it[chapter_number] = fetchedChapter.chapter_number
|
||||||
|
it[scanlator] = fetchedChapter.scanlator
|
||||||
|
|
||||||
|
it[chapterIndex] = index + 1
|
||||||
|
it[manga] = mangaId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any orphaned chapters that are in the db but not in `chapterList`
|
// clear any orphaned chapters that are in the db but not in `chapterList`
|
||||||
val dbChapterCount = transaction { ChapterTable.selectAll().count() }
|
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
if (dbChapterCount > chapterCount) { // we got some clean up due
|
||||||
// TODO: delete orphan chapters
|
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } }
|
||||||
|
|
||||||
|
dbChapterList.forEach {
|
||||||
|
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||||
|
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
|
||||||
|
) {
|
||||||
|
transaction {
|
||||||
|
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
|
||||||
|
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chapterList.map { it ->
|
val dbChapterMap = transaction {
|
||||||
|
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||||
|
.associateBy({ it[ChapterTable.url] }, { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapterList.mapIndexed { index, it ->
|
||||||
|
|
||||||
|
val dbChapter = dbChapterMap.getValue(it.url)
|
||||||
|
|
||||||
ChapterDataClass(
|
ChapterDataClass(
|
||||||
it.url,
|
it.url,
|
||||||
it.name,
|
it.name,
|
||||||
@@ -79,6 +110,12 @@ object Chapter {
|
|||||||
it.chapter_number,
|
it.chapter_number,
|
||||||
it.scanlator,
|
it.scanlator,
|
||||||
mangaId,
|
mangaId,
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.isRead],
|
||||||
|
dbChapter[ChapterTable.isBookmarked],
|
||||||
|
dbChapter[ChapterTable.lastPageRead],
|
||||||
|
|
||||||
|
chapterCount - index,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,9 +168,37 @@ object Chapter {
|
|||||||
chapterEntry[ChapterTable.chapter_number],
|
chapterEntry[ChapterTable.chapter_number],
|
||||||
chapterEntry[ChapterTable.scanlator],
|
chapterEntry[ChapterTable.scanlator],
|
||||||
mangaId,
|
mangaId,
|
||||||
|
chapterEntry[ChapterTable.isRead],
|
||||||
|
chapterEntry[ChapterTable.isBookmarked],
|
||||||
|
chapterEntry[ChapterTable.lastPageRead],
|
||||||
|
|
||||||
chapterEntry[ChapterTable.chapterIndex],
|
chapterEntry[ChapterTable.chapterIndex],
|
||||||
chapterCount.toInt(),
|
chapterCount.toInt(),
|
||||||
pageList.count()
|
pageList.count()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
|
||||||
|
transaction {
|
||||||
|
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) { update ->
|
||||||
|
isRead?.also {
|
||||||
|
update[ChapterTable.isRead] = it
|
||||||
|
}
|
||||||
|
isBookmarked?.also {
|
||||||
|
update[ChapterTable.isBookmarked] = it
|
||||||
|
}
|
||||||
|
lastPageRead?.also {
|
||||||
|
update[ChapterTable.lastPageRead] = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPrevRead?.let {
|
||||||
|
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex less chapterIndex) }) {
|
||||||
|
it[ChapterTable.isRead] = markPrevRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash
|
|||||||
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
|
import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures
|
||||||
import ir.armor.tachidesk.impl.util.await
|
import ir.armor.tachidesk.impl.util.await
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ package ir.armor.tachidesk.impl
|
|||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ package ir.armor.tachidesk.impl
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.impl.Manga.getManga
|
import ir.armor.tachidesk.impl.Manga.getManga
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
import ir.armor.tachidesk.model.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.toDataClass
|
import ir.armor.tachidesk.model.database.table.toDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
import ir.armor.tachidesk.impl.Source.getSource
|
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.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.await
|
import ir.armor.tachidesk.impl.util.await
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
@@ -28,17 +29,24 @@ import org.kodein.di.instance
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
private fun truncate(text: String?, maxLength: Int): String? {
|
||||||
|
return if (text?.length ?: 0 > maxLength)
|
||||||
|
text?.take(maxLength - 3) + "..."
|
||||||
|
else
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
|
||||||
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].toString(),
|
mangaEntry[MangaTable.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
@@ -48,7 +56,8 @@ object Manga {
|
|||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
mangaEntry[MangaTable.inLibrary],
|
mangaEntry[MangaTable.inLibrary],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
@@ -66,7 +75,7 @@ object Manga {
|
|||||||
|
|
||||||
it[MangaTable.artist] = fetchedManga.artist
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
it[MangaTable.author] = fetchedManga.author
|
it[MangaTable.author] = fetchedManga.author
|
||||||
it[MangaTable.description] = fetchedManga.description
|
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
||||||
it[MangaTable.genre] = fetchedManga.genre
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
it[MangaTable.status] = fetchedManga.status
|
it[MangaTable.status] = fetchedManga.status
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
@@ -74,8 +83,9 @@ object Manga {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMangaThumbnail(mangaId)
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
@@ -83,7 +93,7 @@ object Manga {
|
|||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
@@ -93,28 +103,37 @@ object Manga {
|
|||||||
fetchedManga.genre,
|
fetchedManga.genre,
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
false,
|
false,
|
||||||
getSource(mangaEntry[MangaTable.sourceReference])
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
|
||||||
val saveDir = applicationDirs.thumbnailsRoot
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedImageResponse(saveDir, fileName) {
|
return getCachedImageResponse(saveDir, fileName) {
|
||||||
|
getManga(mangaId) // make sure is initialized
|
||||||
|
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
|
||||||
}
|
|
||||||
|
|
||||||
source.client.newCall(
|
source.client.newCall(
|
||||||
GET(thumbnailUrl, source.headers)
|
GET(thumbnailUrl, source.headers)
|
||||||
).await()
|
).await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearMangaThumbnail(mangaId: Int) {
|
||||||
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
|
clearCachedImage(saveDir, fileName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ package ir.armor.tachidesk.impl
|
|||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
import ir.armor.tachidesk.model.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
import ir.armor.tachidesk.model.dataclass.PagedMangaListDataClass
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
|||||||
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
import ir.armor.tachidesk.model.database.table.PageTable
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ package ir.armor.tachidesk.impl
|
|||||||
|
|
||||||
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
import ir.armor.tachidesk.impl.Extension.getExtensionIconUrl
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import ir.armor.tachidesk.model.dataclass.SourceDataClass
|
import ir.armor.tachidesk.model.dataclass.SourceDataClass
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
|
|||||||
@@ -12,15 +12,18 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
|
import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
import ir.armor.tachidesk.impl.backup.BackupFlags
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
import ir.armor.tachidesk.impl.backup.legacy.models.Backup.CURRENT_VERSION
|
||||||
|
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
import ir.armor.tachidesk.impl.backup.models.Manga
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
@@ -83,11 +86,11 @@ object LegacyBackupExport : LegacyBackupBase() {
|
|||||||
|
|
||||||
// Backup manga fields
|
// Backup manga fields
|
||||||
entry[Backup.MANGA] = parser.toJsonTree(manga)
|
entry[Backup.MANGA] = parser.toJsonTree(manga)
|
||||||
|
val mangaId = manga.id!!.toInt()
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
// Check if user wants chapter information in backup
|
||||||
if (options.includeChapters && false) { // TODO
|
if (options.includeChapters) {
|
||||||
// Backup all the chapters
|
// Backup all the chapters
|
||||||
val mangaId = manga.id!!.toInt()
|
|
||||||
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
|
val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { ChapterImpl.fromQuery(it) }
|
||||||
if (chapters.count() > 0) {
|
if (chapters.count() > 0) {
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
val chaptersJson = parser.toJsonTree(chapters)
|
||||||
@@ -97,14 +100,50 @@ object LegacyBackupExport : LegacyBackupBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO the rest
|
// Check if user wants category information in backup
|
||||||
|
if (options.includeCategories) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = getMangaCategories(mangaId)
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
val categoriesNames = categoriesForManga.map { it.name }
|
||||||
|
entry[Backup.CATEGORIES] = parser.toJsonTree(categoriesNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options.includeTracking) { // TODO
|
||||||
|
// val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
// if (tracks.isNotEmpty()) {
|
||||||
|
// entry[TRACK] = parser.toJsonTree(tracks)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // Check if user wants history information in backup
|
||||||
|
if (options.includeHistory) { // TODO
|
||||||
|
// val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
|
// if (historyForManga.isNotEmpty()) {
|
||||||
|
// val historyData = historyForManga.mapNotNull { history ->
|
||||||
|
// val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
||||||
|
// url?.let { DHistory(url, history.last_read) }
|
||||||
|
// }
|
||||||
|
// val historyJson = parser.toJsonTree(historyData)
|
||||||
|
// if (historyJson.asJsonArray.size() > 0) {
|
||||||
|
// entry[HISTORY] = historyJson
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupCategories(root: JsonArray) { // TODO
|
private fun backupCategories(root: JsonArray) {
|
||||||
// val categories = databaseHelper.getCategories().executeAsBlocking()
|
val categories = getCategoryList().map {
|
||||||
// categories.forEach { root.add(parser.toJsonTree(it)) }
|
CategoryImpl().apply {
|
||||||
|
name = it.name
|
||||||
|
order = it.order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.forEach { root.add(parser.toJsonTree(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
||||||
|
|||||||
+26
-14
@@ -7,10 +7,13 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.ValidationResult
|
import ir.armor.tachidesk.impl.Category.createCategory
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.validate
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.ValidationResult
|
||||||
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupValidator.validate
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
import ir.armor.tachidesk.impl.backup.legacy.models.DHistory
|
||||||
|
import ir.armor.tachidesk.impl.backup.models.CategoryImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.Chapter
|
import ir.armor.tachidesk.impl.backup.models.Chapter
|
||||||
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
import ir.armor.tachidesk.impl.backup.models.ChapterImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.Manga
|
import ir.armor.tachidesk.impl.backup.models.Manga
|
||||||
@@ -19,7 +22,7 @@ import ir.armor.tachidesk.impl.backup.models.Track
|
|||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||||
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
|
||||||
import ir.armor.tachidesk.impl.util.awaitSingle
|
import ir.armor.tachidesk.impl.util.awaitSingle
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
@@ -51,7 +54,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
sourceMapping = LegacyBackupValidator.getSourceMapping(json)
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
mangasJson.forEach {
|
||||||
@@ -77,13 +80,16 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
return validationResult
|
return validationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) { // TODO
|
private fun restoreCategories(jsonCategories: JsonElement) {
|
||||||
// db.inTransaction {
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
// backupManager.restoreCategories(categoriesJson.asJsonArray)
|
val dbCategories = getCategoryList()
|
||||||
// }
|
|
||||||
//
|
// Iterate over them and create missing categories
|
||||||
// restoreProgress += 1
|
backupCategories.forEach { category ->
|
||||||
// showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
if (dbCategories.none { it.name == category.name }) {
|
||||||
|
createCategory(category.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||||
@@ -145,10 +151,11 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
history: List<DHistory>,
|
history: List<DHistory>,
|
||||||
tracks: List<Track>
|
tracks: List<Track>
|
||||||
) {
|
) {
|
||||||
fetchManga(source, manga)
|
val fetchedManga = fetchManga(source, manga)
|
||||||
|
|
||||||
// updateChapters(source, fetchedManga, chapters)
|
updateChapters(source, fetchedManga, chapters)
|
||||||
|
|
||||||
|
// TODO
|
||||||
// backupManager.restoreCategoriesForManga(manga, categories)
|
// backupManager.restoreCategoriesForManga(manga, categories)
|
||||||
|
|
||||||
// backupManager.restoreHistoryForManga(history)
|
// backupManager.restoreHistoryForManga(history)
|
||||||
@@ -166,6 +173,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
* @return Updated manga.
|
* @return Updated manga.
|
||||||
*/
|
*/
|
||||||
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
private suspend fun fetchManga(source: Source, manga: Manga): SManga {
|
||||||
|
// make sure we have the manga record in library
|
||||||
transaction {
|
transaction {
|
||||||
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) {
|
||||||
MangaTable.insert {
|
MangaTable.insert {
|
||||||
@@ -180,8 +188,8 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update manga details
|
||||||
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
val fetchedManga = source.fetchMangaDetails(manga).awaitSingle()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) {
|
||||||
|
|
||||||
@@ -197,4 +205,8 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
|
|
||||||
return fetchedManga
|
return fetchedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateChapters(source: Source, fetchedManga: SManga, chapters: List<Chapter>) {
|
||||||
|
// TODO("Not yet implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,11 +9,11 @@ package ir.armor.tachidesk.impl.backup.legacy
|
|||||||
|
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
import ir.armor.tachidesk.impl.backup.legacy.models.Backup
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
object LegacyBackupRestoreValidator {
|
object LegacyBackupValidator {
|
||||||
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
data class ValidationResult(val missingSources: List<String>, val missingTrackers: List<String>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
class ChapterImpl : Chapter {
|
class ChapterImpl : Chapter {
|
||||||
@@ -45,7 +46,10 @@ class ChapterImpl : Chapter {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromQuery(chapterRecord: ResultRow): ChapterImpl {
|
fun fromQuery(chapterRecord: ResultRow): ChapterImpl {
|
||||||
return ChapterImpl().apply {
|
return ChapterImpl().apply {
|
||||||
// TODO
|
url = chapterRecord[ChapterTable.url]
|
||||||
|
read = chapterRecord[ChapterTable.isRead]
|
||||||
|
bookmark = chapterRecord[ChapterTable.isBookmarked]
|
||||||
|
last_page_read = chapterRecord[ChapterTable.lastPageRead]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.models
|
package ir.armor.tachidesk.impl.backup.models
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
open class MangaImpl : Manga {
|
open class MangaImpl : Manga {
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ object CachedImageResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
|
val target = "$fileName."
|
||||||
File(directoryPath).listFiles().forEach { file ->
|
File(directoryPath).listFiles().forEach { file ->
|
||||||
if (file.name.startsWith(fileName))
|
if (file.name.startsWith(target))
|
||||||
return "$directoryPath/${file.name}"
|
return "$directoryPath/${file.name}"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -64,4 +65,11 @@ object CachedImageResponse {
|
|||||||
throw Exception("request error! ${response.code}")
|
throw Exception("request error! ${response.code}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearCachedImage(saveDir: String, fileName: String) {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
cachedFile?.also {
|
||||||
|
File(it).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
import ir.armor.tachidesk.model.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
import ir.armor.tachidesk.model.database.table.SourceTable
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
|
||||||
val url = varchar("url", 2048)
|
|
||||||
val name = varchar("name", 512)
|
|
||||||
val date_upload = long("date_upload").default(0)
|
|
||||||
val chapter_number = float("chapter_number").default(-1f)
|
|
||||||
val scanlator = varchar("scanlator", 128).nullable()
|
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ir.armor.tachidesk.model.database
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.loadMigrationsFrom
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.runMigrations
|
||||||
|
import ir.armor.tachidesk.server.ApplicationDirs
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import org.kodein.di.instance
|
||||||
|
|
||||||
|
object DBManager {
|
||||||
|
val db by lazy {
|
||||||
|
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun databaseUp() {
|
||||||
|
// must mention db object so the lazy block executes
|
||||||
|
val db = DBManager.db
|
||||||
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
|
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
||||||
|
runMigrations(migrations)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0001_Initial : Migration() {
|
||||||
|
private class ExtensionTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("apk_name", 1024)
|
||||||
|
// default is the local source icon from tachiyomi
|
||||||
|
varchar("icon_url", 2048)
|
||||||
|
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
||||||
|
varchar("name", 128)
|
||||||
|
varchar("pkg_name", 128)
|
||||||
|
varchar("version_name", 16)
|
||||||
|
integer("version_code")
|
||||||
|
varchar("lang", 10)
|
||||||
|
bool("is_nsfw")
|
||||||
|
|
||||||
|
bool("is_installed").default(false)
|
||||||
|
bool("has_update").default(false)
|
||||||
|
bool("is_obsolete").default(false)
|
||||||
|
|
||||||
|
varchar("class_name", 1024).default("") // fully qualified name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SourceTable(extensionTable: ExtensionTable) : IdTable<Long>() {
|
||||||
|
override val id = long("id").entityId()
|
||||||
|
init {
|
||||||
|
varchar("name", 128)
|
||||||
|
varchar("lang", 10)
|
||||||
|
reference("extension", extensionTable)
|
||||||
|
bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("title", 512)
|
||||||
|
bool("initialized").default(false)
|
||||||
|
|
||||||
|
varchar("artist", 64).nullable()
|
||||||
|
varchar("author", 64).nullable()
|
||||||
|
varchar("description", 4096).nullable()
|
||||||
|
varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
|
integer("status").default(SManga.UNKNOWN)
|
||||||
|
varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
bool("in_library").default(false)
|
||||||
|
bool("default_category").default(true)
|
||||||
|
|
||||||
|
// source is used by some ancestor of IntIdTable
|
||||||
|
long("source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ChapterTable(mangaTable: MangaTable) : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("name", 512)
|
||||||
|
long("date_upload").default(0)
|
||||||
|
float("chapter_number").default(-1f)
|
||||||
|
varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
bool("read").default(false)
|
||||||
|
bool("bookmark").default(false)
|
||||||
|
integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
integer("number_in_list")
|
||||||
|
reference("manga", mangaTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PageTable(chapterTable: ChapterTable) : IntIdTable() {
|
||||||
|
init {
|
||||||
|
integer("index")
|
||||||
|
varchar("url", 2048)
|
||||||
|
varchar("imageUrl", 2048).nullable()
|
||||||
|
reference("chapter", chapterTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CategoryTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
varchar("name", 64)
|
||||||
|
bool("is_landing").default(false)
|
||||||
|
integer("order").default(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CategoryMangaTable : IntIdTable() {
|
||||||
|
init {
|
||||||
|
reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
||||||
|
reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** initial migration, create all tables */
|
||||||
|
override fun run() {
|
||||||
|
transaction {
|
||||||
|
val extensionTable = ExtensionTable()
|
||||||
|
val sourceTable = SourceTable(extensionTable)
|
||||||
|
val mangaTable = MangaTable()
|
||||||
|
val chapterTable = ChapterTable(mangaTable)
|
||||||
|
val pageTable = PageTable(chapterTable)
|
||||||
|
val categoryTable = CategoryTable()
|
||||||
|
val categoryMangaTable = CategoryMangaTable()
|
||||||
|
SchemaUtils.create(
|
||||||
|
extensionTable,
|
||||||
|
sourceTable,
|
||||||
|
mangaTable,
|
||||||
|
chapterTable,
|
||||||
|
pageTable,
|
||||||
|
categoryTable,
|
||||||
|
categoryMangaTable,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.database.migration.lib.Migration
|
||||||
|
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||||
|
import org.jetbrains.exposed.sql.vendors.currentDialect
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0002_ChapterTableIndexRename : Migration() {
|
||||||
|
/** this migration renamed ChapterTable.NUMBER_IN_LIST to ChapterTable.INDEX */
|
||||||
|
override fun run() {
|
||||||
|
with(TransactionManager.current()) {
|
||||||
|
exec("ALTER TABLE CHAPTER ALTER COLUMN NUMBER_IN_LIST RENAME TO INDEX")
|
||||||
|
commit()
|
||||||
|
currentDialect.resetCaches()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Andreas Mausch
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form 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/. */
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
abstract class Migration {
|
||||||
|
val name: String
|
||||||
|
val version: Int
|
||||||
|
|
||||||
|
init {
|
||||||
|
val groups = Regex("^M(\\d+)_(.*)$").matchEntire(this::class.simpleName!!)?.groupValues
|
||||||
|
?: throw IllegalArgumentException("Migration class name doesn't match convention")
|
||||||
|
version = groups[1].toInt()
|
||||||
|
name = groups[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun run()
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form 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/. */
|
||||||
|
|
||||||
|
// 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 org.jetbrains.exposed.dao.IntEntity
|
||||||
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
import org.jetbrains.exposed.sql.`java-time`.timestamp
|
||||||
|
|
||||||
|
object MigrationsTable : IdTable<Int>() {
|
||||||
|
override val id = integer("version").entityId()
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
val name = varchar("name", length = 400)
|
||||||
|
val executedAt = timestamp("executed_at")
|
||||||
|
|
||||||
|
init {
|
||||||
|
index(true, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MigrationEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||||
|
companion object : IntEntityClass<MigrationEntity>(MigrationsTable)
|
||||||
|
|
||||||
|
var version by MigrationsTable.id
|
||||||
|
var name by MigrationsTable.name
|
||||||
|
var executedAt by MigrationsTable.executedAt
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.migration.lib
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form 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/. */
|
||||||
|
|
||||||
|
// 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 mu.KotlinLogging
|
||||||
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
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.time.Clock
|
||||||
|
import java.time.Instant.now
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
fun runMigrations(migrations: List<Migration>, database: Database = TransactionManager.defaultDatabase!!, clock: Clock = Clock.systemUTC()) {
|
||||||
|
checkVersions(migrations)
|
||||||
|
|
||||||
|
logger.info { "Running migrations on database ${database.url}" }
|
||||||
|
|
||||||
|
val latestVersion = transaction(database) {
|
||||||
|
createTableIfNotExists(database)
|
||||||
|
MigrationEntity.all().maxByOrNull { it.version }?.version?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Database version before migrations: $latestVersion" }
|
||||||
|
|
||||||
|
migrations
|
||||||
|
.sortedBy { it.version }
|
||||||
|
.filter { shouldRun(latestVersion, it) }
|
||||||
|
.forEach {
|
||||||
|
logger.info { "Running migration version ${it.version}: ${it.name}" }
|
||||||
|
transaction(database) {
|
||||||
|
it.run()
|
||||||
|
|
||||||
|
MigrationEntity.new {
|
||||||
|
version = EntityID(it.version, MigrationsTable)
|
||||||
|
name = it.name
|
||||||
|
executedAt = now(clock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info { "Migrations finished successfully" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
|
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
||||||
|
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
||||||
|
.getTopLevelClasses(classPath)
|
||||||
|
.map {
|
||||||
|
logger.debug("found Migration class ${it.name}")
|
||||||
|
val clazz = it.load().getDeclaredConstructor().newInstance()
|
||||||
|
if (clazz is Migration)
|
||||||
|
clazz
|
||||||
|
else
|
||||||
|
throw RuntimeException("found a class that's not a Migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkVersions(migrations: List<Migration>) {
|
||||||
|
val sorted = migrations.map { it.version }.sorted()
|
||||||
|
if ((1..migrations.size).toList() != sorted) {
|
||||||
|
throw IllegalStateException("List of migrations version is not consecutive: $sorted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createTableIfNotExists(database: Database) {
|
||||||
|
if (MigrationsTable.exists()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val tableNames = database.dialect.allTablesNames()
|
||||||
|
when (tableNames.isEmpty()) {
|
||||||
|
true -> {
|
||||||
|
logger.info { "Empty database found, creating table for migrations" }
|
||||||
|
create(MigrationsTable)
|
||||||
|
}
|
||||||
|
false -> throw IllegalStateException("Tried to run migrations against a non-empty database without a Migrations table. This is not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldRun(latestVersion: Int?, migration: Migration): Boolean {
|
||||||
|
val run = latestVersion?.let { migration.version > it } ?: true
|
||||||
|
if (!run) {
|
||||||
|
logger.debug { "Skipping migration version ${migration.version}: ${migration.name}" }
|
||||||
|
}
|
||||||
|
return run
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -18,8 +18,8 @@ object CategoryTable : IntIdTable() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
categoryEntry[CategoryTable.id].value,
|
categoryEntry[this.id].value,
|
||||||
categoryEntry[CategoryTable.order],
|
categoryEntry[this.order],
|
||||||
categoryEntry[CategoryTable.name],
|
categoryEntry[this.name],
|
||||||
categoryEntry[CategoryTable.isLanding],
|
categoryEntry[this.isLanding],
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
|
object ChapterTable : IntIdTable() {
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val name = varchar("name", 512)
|
||||||
|
val date_upload = long("date_upload").default(0)
|
||||||
|
val chapter_number = float("chapter_number").default(-1f)
|
||||||
|
val scanlator = varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
|
val isRead = bool("read").default(false)
|
||||||
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
|
// index is reserved by a function
|
||||||
|
val chapterIndex = integer("index")
|
||||||
|
|
||||||
|
val manga = reference("manga", MangaTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||||
|
ChapterDataClass(
|
||||||
|
chapterEntry[this.url],
|
||||||
|
chapterEntry[this.name],
|
||||||
|
chapterEntry[this.date_upload],
|
||||||
|
chapterEntry[this.chapter_number],
|
||||||
|
chapterEntry[this.scanlator],
|
||||||
|
chapterEntry[this.manga].value,
|
||||||
|
chapterEntry[this.isRead],
|
||||||
|
chapterEntry[this.isBookmarked],
|
||||||
|
chapterEntry[this.lastPageRead],
|
||||||
|
chapterEntry[this.chapterIndex],
|
||||||
|
)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+13
-13
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -36,21 +36,21 @@ object MangaTable : IntIdTable() {
|
|||||||
|
|
||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaEntry[MangaTable.id].value,
|
mangaEntry[this.id].value,
|
||||||
mangaEntry[sourceReference].toString(),
|
mangaEntry[this.sourceReference].toString(),
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[this.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[this.title],
|
||||||
proxyThumbnailUrl(mangaEntry[MangaTable.id].value),
|
proxyThumbnailUrl(mangaEntry[this.id].value),
|
||||||
|
|
||||||
mangaEntry[MangaTable.initialized],
|
mangaEntry[this.initialized],
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
mangaEntry[this.artist],
|
||||||
mangaEntry[MangaTable.author],
|
mangaEntry[this.author],
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[this.description],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[this.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[this.status]).name,
|
||||||
mangaEntry[MangaTable.inLibrary]
|
mangaEntry[this.inLibrary]
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class MangaStatus(val status: Int) {
|
enum class MangaStatus(val status: Int) {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ir.armor.tachidesk.model.database
|
package ir.armor.tachidesk.model.database.table
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -10,13 +10,22 @@ package ir.armor.tachidesk.model.dataclass
|
|||||||
data class ChapterDataClass(
|
data class ChapterDataClass(
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val date_upload: Long,
|
val uploadDate: Long,
|
||||||
val chapter_number: Float,
|
val chapterNumber: Float,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
|
|
||||||
/** this chapter's index */
|
/** chapter is read */
|
||||||
val chapterIndex: Int? = null,
|
val read: Boolean,
|
||||||
|
|
||||||
|
/** chapter is bookmarked */
|
||||||
|
val bookmarked: Boolean,
|
||||||
|
|
||||||
|
/** last read page, zero means not read/no data */
|
||||||
|
val lastPageRead: Int,
|
||||||
|
|
||||||
|
/** this chapter's index, starts with 1 */
|
||||||
|
val index: Int? = null,
|
||||||
|
|
||||||
/** total chapter count, used to calculate if there's a next and prev chapter */
|
/** total chapter count, used to calculate if there's a next and prev chapter */
|
||||||
val chapterCount: Int? = null,
|
val chapterCount: Int? = null,
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package ir.armor.tachidesk.model.dataclass
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryMangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.CategoryTable
|
|
||||||
import ir.armor.tachidesk.model.database.ChapterTable
|
|
||||||
import ir.armor.tachidesk.model.database.ExtensionTable
|
|
||||||
import ir.armor.tachidesk.model.database.MangaTable
|
|
||||||
import ir.armor.tachidesk.model.database.PageTable
|
|
||||||
import ir.armor.tachidesk.model.database.SourceTable
|
|
||||||
import ir.armor.tachidesk.server.ApplicationDirs
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import org.kodein.di.instance
|
|
||||||
|
|
||||||
object DBMangaer {
|
|
||||||
val db by lazy {
|
|
||||||
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeDataBaseTables() {
|
|
||||||
// must mention db object so the lazy block executes
|
|
||||||
val db = DBMangaer.db
|
|
||||||
db.useNestedTransactions = true
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
SchemaUtils.createMissingTablesAndColumns(
|
|
||||||
ExtensionTable,
|
|
||||||
SourceTable,
|
|
||||||
MangaTable,
|
|
||||||
ChapterTable,
|
|
||||||
PageTable,
|
|
||||||
CategoryTable,
|
|
||||||
CategoryMangaTable,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ package ir.armor.tachidesk.model.dataclass
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.model.database.MangaStatus
|
import ir.armor.tachidesk.model.database.table.MangaStatus
|
||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -25,7 +25,9 @@ data class MangaDataClass(
|
|||||||
val genre: String? = null,
|
val genre: String? = null,
|
||||||
val status: String = MangaStatus.UNKNOWN.name,
|
val status: String = MangaStatus.UNKNOWN.name,
|
||||||
val inLibrary: Boolean = false,
|
val inLibrary: Boolean = false,
|
||||||
val source: SourceDataClass? = null
|
val source: SourceDataClass? = null,
|
||||||
|
|
||||||
|
val freshData: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PagedMangaListDataClass(
|
data class PagedMangaListDataClass(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package ir.armor.tachidesk.server
|
package ir.armor.tachidesk.server
|
||||||
|
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.Main
|
|
||||||
import ir.armor.tachidesk.impl.Category.createCategory
|
import ir.armor.tachidesk.impl.Category.createCategory
|
||||||
import ir.armor.tachidesk.impl.Category.getCategoryList
|
import ir.armor.tachidesk.impl.Category.getCategoryList
|
||||||
import ir.armor.tachidesk.impl.Category.removeCategory
|
import ir.armor.tachidesk.impl.Category.removeCategory
|
||||||
@@ -13,6 +12,7 @@ import ir.armor.tachidesk.impl.CategoryManga.getMangaCategories
|
|||||||
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
import ir.armor.tachidesk.impl.CategoryManga.removeMangaFromCategory
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapter
|
import ir.armor.tachidesk.impl.Chapter.getChapter
|
||||||
import ir.armor.tachidesk.impl.Chapter.getChapterList
|
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.getExtensionIcon
|
||||||
import ir.armor.tachidesk.impl.Extension.installExtension
|
import ir.armor.tachidesk.impl.Extension.installExtension
|
||||||
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
import ir.armor.tachidesk.impl.Extension.uninstallExtension
|
||||||
@@ -33,16 +33,18 @@ import ir.armor.tachidesk.impl.Source.getSourceList
|
|||||||
import ir.armor.tachidesk.impl.backup.BackupFlags
|
import ir.armor.tachidesk.impl.backup.BackupFlags
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
|
||||||
import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
|
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.server.util.openInBrowser
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.future.future
|
import kotlinx.coroutines.future.future
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@@ -53,7 +55,8 @@ import java.util.concurrent.CompletableFuture
|
|||||||
|
|
||||||
object JavalinSetup {
|
object JavalinSetup {
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
|
private val scope = CoroutineScope(Executors.newFixedThreadPool(200).asCoroutineDispatcher())
|
||||||
|
|
||||||
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
private fun <T> future(block: suspend CoroutineScope.() -> T): CompletableFuture<T> {
|
||||||
return scope.future(block = block)
|
return scope.future(block = block)
|
||||||
@@ -64,7 +67,10 @@ object JavalinSetup {
|
|||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
Main::class.java.getResource("/react/index.html")
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
|
this::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
|
// no exception so we can tell javalin to serve webUI
|
||||||
hasWebUiBundled = true
|
hasWebUiBundled = true
|
||||||
config.addStaticFiles("/react")
|
config.addStaticFiles("/react")
|
||||||
config.addSinglePageRoot("/", "/react/index.html")
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
@@ -74,6 +80,14 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
config.enableCorsForAllOrigins()
|
config.enableCorsForAllOrigins()
|
||||||
}.start(serverConfig.ip, serverConfig.port)
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
|
|
||||||
|
// when JVM is prompted to shutdown, stop javalin gracefully
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
thread(start = false) {
|
||||||
|
app.stop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
if (hasWebUiBundled && serverConfig.initialOpenInBrowserEnabled) {
|
||||||
openInBrowser()
|
openInBrowser()
|
||||||
}
|
}
|
||||||
@@ -89,6 +103,7 @@ object JavalinSetup {
|
|||||||
ctx.result(e.message ?: "Internal Server Error")
|
ctx.result(e.message ?: "Internal Server Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// list all extensions
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(
|
ctx.json(
|
||||||
future {
|
future {
|
||||||
@@ -97,6 +112,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// install extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
app.get("/api/v1/extension/install/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -107,6 +123,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
app.get("/api/v1/extension/update/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -117,6 +134,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uninstall extension identified with "pkgName"
|
||||||
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
app.get("/api/v1/extension/uninstall/:pkgName") { ctx ->
|
||||||
val pkgName = ctx.pathParam("pkgName")
|
val pkgName = ctx.pathParam("pkgName")
|
||||||
|
|
||||||
@@ -125,7 +143,7 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// icon for extension named `apkName`
|
// icon for extension named `apkName`
|
||||||
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
|
||||||
ctx.result(
|
ctx.result(
|
||||||
@@ -173,9 +191,11 @@ object JavalinSetup {
|
|||||||
// get manga info
|
// get manga info
|
||||||
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
app.get("/api/v1/manga/:mangaId/") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
ctx.json(
|
ctx.json(
|
||||||
future {
|
future {
|
||||||
getManga(mangaId)
|
getManga(mangaId, onlineFetch)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -236,7 +256,10 @@ object JavalinSetup {
|
|||||||
// get chapter list when showing a manga
|
// get chapter list when showing a manga
|
||||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(future { getChapterList(mangaId) })
|
|
||||||
|
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
|
||||||
|
|
||||||
|
ctx.json(future { getChapterList(mangaId, onlineFetch) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to display a chapter, get a chapter in order to show it's pages
|
// used to display a chapter, get a chapter in order to show it's pages
|
||||||
@@ -246,6 +269,22 @@ object JavalinSetup {
|
|||||||
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
ctx.json(future { getChapter(chapterIndex, mangaId) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used to modify a chapter's parameters
|
||||||
|
app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx ->
|
||||||
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
|
||||||
|
val read = ctx.formParam("read")?.toBoolean()
|
||||||
|
val bookmarked = ctx.formParam("bookmarked")?.toBoolean()
|
||||||
|
val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean()
|
||||||
|
val lastPageRead = ctx.formParam("lastPageRead")?.toInt()
|
||||||
|
|
||||||
|
modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
|
||||||
|
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get page at index "index"
|
||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
|
||||||
@@ -260,7 +299,7 @@ object JavalinSetup {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// global search
|
// global search, Not implemented yet
|
||||||
app.get("/api/v1/search/:searchTerm") { ctx ->
|
app.get("/api/v1/search/:searchTerm") { ctx ->
|
||||||
val searchTerm = ctx.pathParam("searchTerm")
|
val searchTerm = ctx.pathParam("searchTerm")
|
||||||
ctx.json(sourceGlobalSearch(searchTerm))
|
ctx.json(sourceGlobalSearch(searchTerm))
|
||||||
@@ -297,6 +336,11 @@ object JavalinSetup {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns some static info of the current app build
|
||||||
|
app.get("/api/v1/about/") { ctx ->
|
||||||
|
ctx.json(getAbout())
|
||||||
|
}
|
||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ServerConfig(config: Config) : ConfigModule(config) {
|
|||||||
val socksProxyPort: String by config
|
val socksProxyPort: String by config
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
val debugLogsEnabled: Boolean by config
|
val debugLogsEnabled: Boolean = System.getProperty("ir.armor.tachidesk.debugLogsEnabled", config.getString("debugLogsEnabled")).toBoolean()
|
||||||
val systemTrayEnabled: Boolean by config
|
val systemTrayEnabled: Boolean by config
|
||||||
val initialOpenInBrowserEnabled: Boolean by config
|
val initialOpenInBrowserEnabled: Boolean by config
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ package ir.armor.tachidesk.server
|
|||||||
|
|
||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.model.database.databaseUp
|
||||||
import ir.armor.tachidesk.model.dataclass.makeDataBaseTables
|
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.systemTray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
@@ -81,7 +80,7 @@ fun applicationSetup() {
|
|||||||
try {
|
try {
|
||||||
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
val dataConfFile = File("${applicationDirs.dataRoot}/server.conf")
|
||||||
if (!dataConfFile.exists()) {
|
if (!dataConfFile.exists()) {
|
||||||
Main::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
JavalinSetup::class.java.getResourceAsStream("/server-reference.conf").use { input ->
|
||||||
dataConfFile.outputStream().use { output ->
|
dataConfFile.outputStream().use { output ->
|
||||||
input.copyTo(output)
|
input.copyTo(output)
|
||||||
}
|
}
|
||||||
@@ -91,13 +90,13 @@ fun applicationSetup() {
|
|||||||
logger.error("Exception while creating initial server.conf:\n", e)
|
logger.error("Exception while creating initial server.conf:\n", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
makeDataBaseTables()
|
databaseUp()
|
||||||
|
|
||||||
// create system tray
|
// create system tray
|
||||||
if (serverConfig.systemTrayEnabled) {
|
if (serverConfig.systemTrayEnabled) {
|
||||||
try {
|
try {
|
||||||
systemTray
|
systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +108,6 @@ fun applicationSetup() {
|
|||||||
|
|
||||||
// socks proxy settings
|
// socks proxy settings
|
||||||
if (serverConfig.socksProxyEnabled) {
|
if (serverConfig.socksProxyEnabled) {
|
||||||
// System.getProperties()["proxySet"] = "true"
|
|
||||||
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||||
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||||
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}")
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ir.armor.tachidesk.server.internal
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.server.BuildConfig
|
||||||
|
|
||||||
|
data class AboutDataClass(
|
||||||
|
val version: String,
|
||||||
|
val revision: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
object About {
|
||||||
|
fun getAbout(): AboutDataClass {
|
||||||
|
return AboutDataClass(
|
||||||
|
BuildConfig.version,
|
||||||
|
BuildConfig.revision,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,15 +12,15 @@ import dorkbox.systemTray.SystemTray
|
|||||||
import dorkbox.systemTray.SystemTray.TrayType
|
import dorkbox.systemTray.SystemTray.TrayType
|
||||||
import dorkbox.util.CacheUtil
|
import dorkbox.util.CacheUtil
|
||||||
import dorkbox.util.Desktop
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.server.BuildConfig
|
||||||
|
import ir.armor.tachidesk.server.ServerConfig
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
import java.awt.event.ActionListener
|
import kotlin.system.exitProcess
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
fun openInBrowser() {
|
fun openInBrowser() {
|
||||||
try {
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,37 +32,34 @@ fun systemTray(): SystemTray? {
|
|||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
if (System.getProperty("os.name").startsWith("Windows"))
|
||||||
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||||
|
|
||||||
CacheUtil.clear()
|
CacheUtil.clear(BuildConfig.name)
|
||||||
|
|
||||||
val systemTray = SystemTray.get() ?: return null
|
val systemTray = SystemTray.get(BuildConfig.name) ?: return null
|
||||||
val mainMenu = systemTray.menu
|
val mainMenu = systemTray.menu
|
||||||
|
|
||||||
mainMenu.add(
|
mainMenu.add(
|
||||||
MenuItem(
|
MenuItem(
|
||||||
"Open Tachidesk",
|
"Open Tachidesk"
|
||||||
ActionListener {
|
) {
|
||||||
try {
|
openInBrowser()
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
|
||||||
systemTray.setImage(icon)
|
|
||||||
// systemTray.status = "No Mail"
|
|
||||||
|
|
||||||
systemTray.getMenu().add(
|
|
||||||
MenuItem("Quit") {
|
|
||||||
systemTray.shutdown()
|
|
||||||
System.exit(0)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
return systemTray
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.moowork.node") version "1.3.1"
|
id("com.github.node-gradle.node") version "3.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
node {
|
node {
|
||||||
workDir = file("${project.projectDir}/react/")
|
nodeProjectDir.set(file("${project.projectDir}/react/"))
|
||||||
nodeModulesDir = file("${project.projectDir}/react/")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named("yarn_build") {
|
tasks.named("yarn_build") {
|
||||||
dependsOn("yarn") // install node_moduels
|
dependsOn("yarn") // install node_modules
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyBuild") {
|
tasks.register<Copy>("copyBuild") {
|
||||||
|
|||||||
+17
-18
@@ -3,21 +3,19 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.2",
|
"@fontsource/roboto": "^4.3.0",
|
||||||
|
"@material-ui/core": "^4.11.4",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||||
"@testing-library/react": "^11.1.0",
|
|
||||||
"@testing-library/user-event": "^12.1.10",
|
|
||||||
"@types/react-lazyload": "^3.1.0",
|
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"file-selector": "^0.2.4",
|
"file-selector": "^0.2.4",
|
||||||
"fontsource-roboto": "^4.0.0",
|
"react": "^17.0.2",
|
||||||
"react": "^17.0.1",
|
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-lazyload": "^3.2.0",
|
"react-lazyload": "^3.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.3",
|
||||||
|
"react-virtuoso": "^1.8.6",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -39,17 +37,18 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.2",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-lazyload": "^3.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.11.0",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@typescript-eslint/parser": "4.11.0",
|
"@typescript-eslint/eslint-plugin": "4.23.0",
|
||||||
"eslint": "^7.16.0",
|
"@typescript-eslint/parser": "4.23.0",
|
||||||
"eslint-config-airbnb-typescript": "^12.0.0",
|
"eslint": "^7.26.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
"eslint-plugin-react": "^7.21.5",
|
"eslint-plugin-react": "^7.23.2",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"typescript": "^4.1.0"
|
"typescript": "^4.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Container } from '@material-ui/core';
|
|||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/navbar/NavBar';
|
||||||
import Sources from './screens/Sources';
|
import Sources from './screens/Sources';
|
||||||
import Extensions from './screens/Extensions';
|
import Extensions from './screens/Extensions';
|
||||||
import SourceMangas from './screens/SourceMangas';
|
import SourceMangas from './screens/SourceMangas';
|
||||||
|
|||||||
@@ -7,12 +7,17 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles, useTheme } from '@material-ui/core/styles';
|
||||||
import Card from '@material-ui/core/Card';
|
import Card from '@material-ui/core/Card';
|
||||||
import CardContent from '@material-ui/core/CardContent';
|
import CardContent from '@material-ui/core/CardContent';
|
||||||
import Button from '@material-ui/core/Button';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import Menu from '@material-ui/core/Menu';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import BookmarkIcon from '@material-ui/icons/Bookmark';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -21,6 +26,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
|
read: {
|
||||||
|
backgroundColor: theme.palette.type === 'dark' ? '#353535' : '#f0f0f0',
|
||||||
|
},
|
||||||
bullet: {
|
bullet: {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
margin: '0 2px',
|
margin: '0 2px',
|
||||||
@@ -42,46 +50,90 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
chapter: IChapter
|
chapter: IChapter
|
||||||
|
triggerChaptersUpdate: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChapterCard(props: IProps) {
|
export default function ChapterCard(props: IProps) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { chapter } = props;
|
const theme = useTheme();
|
||||||
|
const { chapter, triggerChaptersUpdate } = props;
|
||||||
|
|
||||||
const dateStr = chapter.date_upload && new Date(chapter.date_upload).toISOString().slice(0, 10);
|
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendChange = (key: string, value: any) => {
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append(key, value);
|
||||||
|
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
|
||||||
|
.then(() => triggerChaptersUpdate());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className={classes.root}>
|
<CardContent className={`${classes.root} ${chapter.read && classes.read}`}>
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<Typography variant="h5" component="h2">
|
|
||||||
{chapter.name}
|
|
||||||
{chapter.chapter_number > 0 && ` : ${chapter.chapter_number}`}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
|
||||||
{chapter.scanlator}
|
|
||||||
{chapter.scanlator && ' '}
|
|
||||||
{dateStr}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Link
|
<Link
|
||||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.chapterIndex}`}
|
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||||
style={{ textDecoration: 'none' }}
|
style={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<div style={{ display: 'flex' }}>
|
||||||
variant="outlined"
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
style={{ marginLeft: 20 }}
|
<Typography variant="h5" component="h2">
|
||||||
>
|
<span style={{ color: theme.palette.primary.dark }}>
|
||||||
open
|
{chapter.bookmarked && <BookmarkIcon />}
|
||||||
|
</span>
|
||||||
</Button>
|
{chapter.name}
|
||||||
|
{chapter.chapterNumber > 0 && ` : ${chapter.chapterNumber}`}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
|
{chapter.scanlator}
|
||||||
|
{chapter.scanlator && ' '}
|
||||||
|
{dateStr}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<IconButton aria-label="more" onClick={handleClick}>
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */}
|
||||||
|
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||||
|
{chapter.bookmarked && 'Remove bookmark'}
|
||||||
|
{!chapter.bookmarked && 'Bookmark'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
||||||
|
Mark as
|
||||||
|
{' '}
|
||||||
|
{chapter.read && 'unread'}
|
||||||
|
{!chapter.read && 'read'}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
||||||
|
Mark previous as Read
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import FilterListIcon from '@material-ui/icons/FilterList';
|
|||||||
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import { langCodeToName } from '../util/language';
|
import { langCodeToName } from '../util/language';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
const useStyles = makeStyles(() => createStyles({
|
const useStyles = makeStyles(() => createStyles({
|
||||||
paper: {
|
paper: {
|
||||||
@@ -54,7 +55,7 @@ export default function ExtensionLangSelect(props: IProps) {
|
|||||||
if (checked) {
|
if (checked) {
|
||||||
setMShownLangs([...mShownLangs, lang]);
|
setMShownLangs([...mShownLangs, lang]);
|
||||||
} else {
|
} else {
|
||||||
const clone = JSON.parse(JSON.stringify(mShownLangs));
|
const clone = cloneObject(mShownLangs);
|
||||||
clone.splice(clone.indexOf(lang), 1);
|
clone.splice(clone.indexOf(lang), 1);
|
||||||
setMShownLangs(clone);
|
setMShownLangs(clone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ export default function MangaDetails(props: IProps) {
|
|||||||
const { setAction } = useContext(NavbarContext);
|
const { setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const { manga } = props;
|
const { manga } = props;
|
||||||
|
if (manga.genre == null) {
|
||||||
|
manga.genre = '';
|
||||||
|
}
|
||||||
const [inLibrary, setInLibrary] = useState<string>(
|
const [inLibrary, setInLibrary] = useState<string>(
|
||||||
manga.inLibrary ? 'In Library' : 'Add To Library',
|
manga.inLibrary ? 'In Library' : 'Add To Library',
|
||||||
);
|
);
|
||||||
@@ -195,7 +198,7 @@ export default function MangaDetails(props: IProps) {
|
|||||||
<div className={classes.top}>
|
<div className={classes.top}>
|
||||||
<div className={classes.leftRight}>
|
<div className={classes.leftRight}>
|
||||||
<div className={classes.leftSide}>
|
<div className={classes.leftSide}>
|
||||||
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" />
|
<img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" />
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.rightSide}>
|
<div className={classes.rightSide}>
|
||||||
<h1>
|
<h1>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const useStyles = makeStyles({
|
|||||||
interface IProps {
|
interface IProps {
|
||||||
drawerOpen: boolean
|
drawerOpen: boolean
|
||||||
|
|
||||||
setDrawerOpen(state: boolean): void
|
setDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import React from 'react';
|
||||||
|
import Slide, { SlideProps } from '@material-ui/core/Slide';
|
||||||
|
import Snackbar from '@material-ui/core/Snackbar';
|
||||||
|
import MuiAlert, { AlertProps, Color as Severity } from '@material-ui/lab/Alert';
|
||||||
|
|
||||||
|
function removeToast(id: string) {
|
||||||
|
const container = document.querySelector(`#${id}`)!!;
|
||||||
|
ReactDOM.unmountComponentAtNode(container);
|
||||||
|
document.body.removeChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Transition(props: SlideProps) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <Slide {...props} direction="up" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Alert(props: AlertProps) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <MuiAlert elevation={6} variant="filled" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IToastProps{
|
||||||
|
message: string
|
||||||
|
severity: Severity
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toast(props: IToastProps) {
|
||||||
|
const { message, severity } = props;
|
||||||
|
const [open, setOpen] = React.useState(true);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Snackbar
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
autoHideDuration={3000}
|
||||||
|
TransitionComponent={Transition}
|
||||||
|
message="I love snacks"
|
||||||
|
>
|
||||||
|
<MuiAlert elevation={6} variant="filled" onClose={handleClose} severity={severity}>
|
||||||
|
{message}
|
||||||
|
</MuiAlert>
|
||||||
|
</Snackbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function makeToast(message: string, severity: Severity) {
|
||||||
|
const id = Math.floor(Math.random() * 1000);
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = `alert-${id}`;
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
ReactDOM.render(<Toast message={message} severity={severity} />, container);
|
||||||
|
|
||||||
|
setTimeout(() => removeToast(container.id), 3500);
|
||||||
|
}
|
||||||
+3
-3
@@ -12,9 +12,9 @@ import Toolbar from '@material-ui/core/Toolbar';
|
|||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
import NavBarContext from '../context/NavbarContext';
|
import NavBarContext from '../../context/NavbarContext';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../../context/DarkTheme';
|
||||||
import TemporaryDrawer from './TemporaryDrawer';
|
import TemporaryDrawer from '../TemporaryDrawer';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
+117
-107
@@ -23,13 +23,15 @@ import { Switch } from '@material-ui/core';
|
|||||||
import List from '@material-ui/core/List';
|
import List from '@material-ui/core/List';
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import Select from '@material-ui/core/Select';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
||||||
import Collapse from '@material-ui/core/Collapse';
|
import Collapse from '@material-ui/core/Collapse';
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../../context/DarkTheme';
|
||||||
import NavBarContext from '../context/NavbarContext';
|
import NavBarContext from '../../context/NavbarContext';
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
||||||
// main container and root div need to change classes...
|
// main container and root div need to change classes...
|
||||||
@@ -44,7 +46,7 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
|||||||
position: settings.staticNav ? 'sticky' : 'fixed',
|
position: settings.staticNav ? 'sticky' : 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
minWidth: '300px',
|
width: '300px',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
backgroundColor: '#0a0b0b',
|
backgroundColor: '#0a0b0b',
|
||||||
@@ -137,16 +139,12 @@ const useStyles = (settings: IReaderSettings) => makeStyles((theme: Theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface IReaderSettings{
|
|
||||||
staticNav: boolean
|
|
||||||
showPageNumber: boolean
|
|
||||||
continuesPageGap: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultReaderSettings = () => ({
|
export const defaultReaderSettings = () => ({
|
||||||
staticNav: false,
|
staticNav: false,
|
||||||
showPageNumber: true,
|
showPageNumber: true,
|
||||||
continuesPageGap: false,
|
continuesPageGap: false,
|
||||||
|
loadNextonEnding: false,
|
||||||
|
readerType: 'ContinuesVertical',
|
||||||
} as IReaderSettings);
|
} as IReaderSettings);
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -171,7 +169,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
|
const [drawerVisible, setDrawerVisible] = useState(false || settings.staticNav);
|
||||||
const [hideOpenButton, setHideOpenButton] = useState(false);
|
const [hideOpenButton, setHideOpenButton] = useState(false);
|
||||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
||||||
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(false);
|
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(true);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const classes = useStyles(settings)();
|
const classes = useStyles(settings)();
|
||||||
@@ -205,32 +203,31 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClickAwayListener onClickAway={() => (drawerVisible && setDrawerOpen(false))}>
|
<Slide
|
||||||
<Slide
|
direction="right"
|
||||||
direction="right"
|
in={drawerOpen}
|
||||||
in={drawerOpen}
|
timeout={200}
|
||||||
timeout={200}
|
appear={false}
|
||||||
appear={false}
|
mountOnEnter
|
||||||
mountOnEnter
|
unmountOnExit
|
||||||
unmountOnExit
|
onEntered={() => setDrawerVisible(true)}
|
||||||
onEntered={() => setDrawerVisible(true)}
|
onExited={() => setDrawerVisible(false)}
|
||||||
onExited={() => setDrawerVisible(false)}
|
>
|
||||||
>
|
<div className={classes.root}>
|
||||||
<div className={classes.root}>
|
<header>
|
||||||
<header>
|
<IconButton
|
||||||
<IconButton
|
edge="start"
|
||||||
edge="start"
|
color="inherit"
|
||||||
color="inherit"
|
aria-label="menu"
|
||||||
aria-label="menu"
|
disableRipple
|
||||||
disableRipple
|
onClick={() => history.push(`/manga/${manga.id}`)}
|
||||||
onClick={() => history.push(`/manga/${manga.id}`)}
|
>
|
||||||
>
|
<CloseIcon />
|
||||||
<CloseIcon />
|
</IconButton>
|
||||||
</IconButton>
|
<Typography variant="h1">
|
||||||
<Typography variant="h1">
|
{title}
|
||||||
{title}
|
</Typography>
|
||||||
</Typography>
|
{!settings.staticNav
|
||||||
{!settings.staticNav
|
|
||||||
&& (
|
&& (
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
@@ -242,74 +239,88 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
<KeyboardArrowLeftIcon />
|
<KeyboardArrowLeftIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
) }
|
) }
|
||||||
</header>
|
</header>
|
||||||
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
|
<ListItem ContainerComponent="div" className={classes.settingsCollapsseHeader}>
|
||||||
<ListItemText primary="Reader Settings" />
|
<ListItemText primary="Reader Settings" />
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="start"
|
edge="start"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
disableRipple
|
disableRipple
|
||||||
disableFocusRipple
|
disableFocusRipple
|
||||||
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
||||||
|
>
|
||||||
|
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
||||||
|
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Static Navigation" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.staticNav}
|
||||||
|
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Show page number" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.showPageNumber}
|
||||||
|
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Load next chapter at ending" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={settings.loadNextonEnding}
|
||||||
|
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Reader Type" />
|
||||||
|
<Select
|
||||||
|
value={settings.readerType}
|
||||||
|
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
||||||
>
|
>
|
||||||
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
<MenuItem value="SingleLTR">Left to right</MenuItem>
|
||||||
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
<MenuItem value="SingleRTL">Right to left(WIP)</MenuItem>
|
||||||
</IconButton>
|
<MenuItem value="SingleVertical">Vertical(WIP)</MenuItem>
|
||||||
</ListItemSecondaryAction>
|
<MenuItem value="Webtoon">Webtoon</MenuItem>
|
||||||
</ListItem>
|
<MenuItem value="ContinuesVertical">Continues Vertical</MenuItem>
|
||||||
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
<MenuItem value="ContinuesHorizontal">Horizontal(WIP)</MenuItem>
|
||||||
<List>
|
</Select>
|
||||||
<ListItem>
|
</ListItem>
|
||||||
<ListItemText primary="Static Navigation" />
|
</List>
|
||||||
<ListItemSecondaryAction>
|
</Collapse>
|
||||||
<Switch
|
<hr />
|
||||||
edge="end"
|
<div className={classes.navigation}>
|
||||||
checked={settings.staticNav}
|
<span>
|
||||||
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
Currently on page
|
||||||
/>
|
{' '}
|
||||||
</ListItemSecondaryAction>
|
{curPage + 1}
|
||||||
</ListItem>
|
{' '}
|
||||||
<ListItem>
|
of
|
||||||
<ListItemText primary="Show page number" />
|
{' '}
|
||||||
<ListItemSecondaryAction>
|
{chapter.pageCount}
|
||||||
<Switch
|
</span>
|
||||||
edge="end"
|
<div className={classes.navigationChapters}>
|
||||||
checked={settings.showPageNumber}
|
{chapter.index > 1
|
||||||
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary="Continues Page gap" />
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={settings.continuesPageGap}
|
|
||||||
onChange={(e) => setSettingValue('continuesPageGap', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Collapse>
|
|
||||||
<hr />
|
|
||||||
<div className={classes.navigation}>
|
|
||||||
<span>
|
|
||||||
Currently on page
|
|
||||||
{' '}
|
|
||||||
{curPage + 1}
|
|
||||||
{' '}
|
|
||||||
of
|
|
||||||
{' '}
|
|
||||||
{chapter.pageCount}
|
|
||||||
</span>
|
|
||||||
<div className={classes.navigationChapters}>
|
|
||||||
{chapter.chapterIndex > 1
|
|
||||||
&& (
|
&& (
|
||||||
<Link
|
<Link
|
||||||
style={{ gridArea: 'prev' }}
|
style={{ gridArea: 'prev' }}
|
||||||
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex - 1}`}
|
to={`/manga/${manga.id}/chapter/${chapter.index - 1}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -317,15 +328,15 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
>
|
>
|
||||||
Chapter
|
Chapter
|
||||||
{' '}
|
{' '}
|
||||||
{chapter.chapterIndex - 1}
|
{chapter.index - 1}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{chapter.chapterIndex < chapter.chapterCount
|
{chapter.index < chapter.chapterCount
|
||||||
&& (
|
&& (
|
||||||
<Link
|
<Link
|
||||||
style={{ gridArea: 'next' }}
|
style={{ gridArea: 'next' }}
|
||||||
to={`/manga/${manga.id}/chapter/${chapter.chapterIndex + 1}`}
|
to={`/manga/${manga.id}/chapter/${chapter.index + 1}`}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -333,15 +344,14 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
>
|
>
|
||||||
Chapter
|
Chapter
|
||||||
{' '}
|
{' '}
|
||||||
{chapter.chapterIndex + 1}
|
{chapter.index + 1}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Slide>
|
</div>
|
||||||
</ClickAwayListener>
|
</Slide>
|
||||||
<Zoom in={!drawerOpen}>
|
<Zoom in={!drawerOpen}>
|
||||||
<Fade in={!hideOpenButton}>
|
<Fade in={!hideOpenButton}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -11,23 +11,26 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import LazyLoad from 'react-lazyload';
|
||||||
import { IReaderSettings } from './ReaderNavBar';
|
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
loading: {
|
loading: {
|
||||||
margin: '100px auto',
|
margin: '100px auto',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
},
|
},
|
||||||
loadingImage: {
|
loadingImage: {
|
||||||
padding: settings.staticNav ? 'calc(50vh - 40px) calc(50vw - 340px)' : 'calc(50vh - 40px) calc(50vw - 40px)',
|
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
width: '200px',
|
width: '70vw',
|
||||||
|
padding: '50px calc(50% - 20px)',
|
||||||
backgroundColor: '#525252',
|
backgroundColor: '#525252',
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: settings.continuesPageGap ? '15px' : 0,
|
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
|
||||||
|
minWidth: '50vw',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ function LazyImage(props: IProps) {
|
|||||||
|
|
||||||
if (imageSrc.length === 0) {
|
if (imageSrc.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={classes.loadingImage}>
|
<div className={`${classes.image} ${classes.loadingImage}`}>
|
||||||
<CircularProgress thickness={5} />
|
<CircularProgress thickness={5} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -85,7 +88,6 @@ function LazyImage(props: IProps) {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={`Page #${index}`}
|
alt={`Page #${index}`}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -98,22 +100,12 @@ export default function Page(props: IProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: '0 auto' }}>
|
<div style={{ margin: '0 auto' }}>
|
||||||
<LazyLoad
|
<LazyImage
|
||||||
offset={window.innerHeight}
|
src={src}
|
||||||
placeholder={(
|
index={index}
|
||||||
<div className={classes.loading}>
|
setCurPage={setCurPage}
|
||||||
<CircularProgress thickness={5} />
|
settings={settings}
|
||||||
</div>
|
/>
|
||||||
)}
|
|
||||||
once
|
|
||||||
>
|
|
||||||
<LazyImage
|
|
||||||
src={src}
|
|
||||||
index={index}
|
|
||||||
setCurPage={setCurPage}
|
|
||||||
settings={settings}
|
|
||||||
/>
|
|
||||||
</LazyLoad>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form 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 { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
|
pageNumber: {
|
||||||
|
display: settings.showPageNumber ? 'block' : 'none',
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '50px',
|
||||||
|
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
||||||
|
width: '50px',
|
||||||
|
textAlign: 'center',
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
settings: IReaderSettings
|
||||||
|
curPage: number
|
||||||
|
pageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PageNumber(props: IProps) {
|
||||||
|
const { settings, curPage, pageCount } = props;
|
||||||
|
const classes = useStyles(settings)();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.pageNumber}>
|
||||||
|
{`${curPage + 1} / ${pageCount}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React from 'react';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
overflowX: 'scroll',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
pages: Array<IReaderPage>
|
||||||
|
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
settings: IReaderSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HorizontalPager(props: IProps) {
|
||||||
|
const { pages, settings, setCurPage } = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.reader}>
|
||||||
|
{
|
||||||
|
pages.map((page) => (
|
||||||
|
<Page
|
||||||
|
key={page.index}
|
||||||
|
index={page.index}
|
||||||
|
src={page.src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
height: '100vh',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function PagedReader(props: IReaderProps) {
|
||||||
|
const {
|
||||||
|
pages, settings, setCurPage, curPage, manga, chapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if (curPage < pages.length - 1) {
|
||||||
|
setCurPage(curPage + 1);
|
||||||
|
} else if (settings.loadNextonEnding) {
|
||||||
|
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() {
|
||||||
|
if (curPage > 0) { setCurPage(curPage - 1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardControl(e:KeyboardEvent) {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
nextPage();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
prevPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickControl(e:MouseEvent) {
|
||||||
|
if (e.clientX > window.innerWidth / 2) {
|
||||||
|
nextPage();
|
||||||
|
} else {
|
||||||
|
prevPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keyup', keyboardControl, false);
|
||||||
|
pageRef.current?.addEventListener('click', clickControl);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keyup', keyboardControl);
|
||||||
|
pageRef.current?.removeEventListener('click', clickControl);
|
||||||
|
};
|
||||||
|
}, [curPage, pageRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={pageRef} className={classes.reader}>
|
||||||
|
<Page
|
||||||
|
key={curPage}
|
||||||
|
index={curPage}
|
||||||
|
src={pages[curPage].src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Page from '../Page';
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
reader: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function VerticalReader(props: IReaderProps) {
|
||||||
|
const {
|
||||||
|
pages, settings, setCurPage, curPage, manga, chapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleLoadNextonEnding = () => {
|
||||||
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
|
setCurPage(0);
|
||||||
|
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.loadNextonEnding) { window.addEventListener('scroll', handleLoadNextonEnding); }
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleLoadNextonEnding);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.reader}>
|
||||||
|
{
|
||||||
|
pages.map((page) => (
|
||||||
|
<Page
|
||||||
|
key={page.index}
|
||||||
|
index={page.index}
|
||||||
|
src={page.src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
// roboto font
|
// roboto font
|
||||||
import 'fontsource-roboto';
|
import '@fontsource/roboto';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
interface IMangaCategory {
|
interface IMangaCategory {
|
||||||
category: ICategory
|
category: ICategory
|
||||||
@@ -98,7 +99,7 @@ export default function Library() {
|
|||||||
client.get(`/api/v1/category/${tab.category.id}`)
|
client.get(`/api/v1/category/${tab.category.id}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data: IManga[]) => {
|
.then((data: IManga[]) => {
|
||||||
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
const tabsClone = cloneObject(tabs);
|
||||||
tabsClone[index].mangas = data;
|
tabsClone[index].mangas = data;
|
||||||
tabsClone[index].isFetched = true;
|
tabsClone[index].isFetched = true;
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,15 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import ChapterCard from '../components/ChapterCard';
|
import ChapterCard from '../components/ChapterCard';
|
||||||
import MangaDetails from '../components/MangaDetails';
|
import MangaDetails from '../components/MangaDetails';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
import LoadingPlaceholder from '../components/LoadingPlaceholder';
|
import LoadingPlaceholder from '../components/LoadingPlaceholder';
|
||||||
|
import makeToast from '../components/Toast';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((theme: Theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -26,6 +27,8 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
chapters: {
|
chapters: {
|
||||||
listStyle: 'none',
|
listStyle: 'none',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
|
width: '100vw',
|
||||||
|
minHeight: '200px',
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up('md')]: {
|
||||||
width: '50vw',
|
width: '50vw',
|
||||||
height: 'calc(100vh - 64px)',
|
height: 'calc(100vh - 64px)',
|
||||||
@@ -43,40 +46,47 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const { setTitle } = useContext(NavbarContext);
|
const { setTitle } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
|
useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails
|
||||||
|
|
||||||
const { id } = useParams<{id: string}>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const [manga, setManga] = useState<IManga>();
|
const [manga, setManga] = useState<IManga>();
|
||||||
const [chapters, setChapters] = useState<IChapter[]>([]);
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
|
const [fetchedChapters, setFetchedChapters] = useState(false);
|
||||||
|
const [noChaptersFound, setNoChaptersFound] = useState(false);
|
||||||
|
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
|
||||||
|
|
||||||
|
function triggerChaptersUpdate() {
|
||||||
|
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.get(`/api/v1/manga/${id}/`)
|
if (manga === undefined || !manga.freshData) {
|
||||||
.then((response) => response.data)
|
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
|
||||||
.then((data: IManga) => {
|
.then((response) => response.data)
|
||||||
setManga(data);
|
.then((data: IManga) => {
|
||||||
setTitle(data.title);
|
setManga(data);
|
||||||
});
|
setTitle(data.title);
|
||||||
}, []);
|
});
|
||||||
|
}
|
||||||
|
}, [manga]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.get(`/api/v1/manga/${id}/chapters`)
|
const shouldFetchOnline = fetchedChapters && chapterUpdateTriggerer === 0;
|
||||||
|
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data) => setChapters(data));
|
.then((data) => {
|
||||||
}, []);
|
if (data.length === 0 && fetchedChapters) {
|
||||||
|
makeToast('No chapters found', 'warning');
|
||||||
const chapterCards = (
|
setNoChaptersFound(true);
|
||||||
<LoadingPlaceholder
|
}
|
||||||
shouldRender={chapters.length > 0}
|
setChapters(data);
|
||||||
>
|
})
|
||||||
<ol className={classes.chapters}>
|
.then(() => setFetchedChapters(true));
|
||||||
{chapters.map((chapter) => (<ChapterCard chapter={chapter} />))}
|
}, [chapters.length, fetchedChapters, chapterUpdateTriggerer]);
|
||||||
</ol>
|
|
||||||
</LoadingPlaceholder>
|
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
@@ -85,7 +95,27 @@ export default function Manga() {
|
|||||||
component={MangaDetails}
|
component={MangaDetails}
|
||||||
componentProps={{ manga }}
|
componentProps={{ manga }}
|
||||||
/>
|
/>
|
||||||
{chapterCards}
|
|
||||||
|
<LoadingPlaceholder
|
||||||
|
shouldRender={chapters.length > 0 || noChaptersFound}
|
||||||
|
>
|
||||||
|
<Virtuoso
|
||||||
|
style={{ // override Virtuoso default values and set them with class
|
||||||
|
height: 'undefined',
|
||||||
|
}}
|
||||||
|
className={classes.chapters}
|
||||||
|
totalCount={chapters.length}
|
||||||
|
itemContent={(index:number) => (
|
||||||
|
<ChapterCard
|
||||||
|
chapter={chapters[index]}
|
||||||
|
triggerChaptersUpdate={triggerChaptersUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
useWindowScroll={window.innerWidth < 960}
|
||||||
|
overscan={window.innerHeight * 0.5}
|
||||||
|
/>
|
||||||
|
</LoadingPlaceholder>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,38 +10,54 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import Page from '../components/Page';
|
import HorizontalPager from '../components/reader/pager/HorizontalPager';
|
||||||
import ReaderNavBar, { defaultReaderSettings, IReaderSettings } from '../components/ReaderNavBar';
|
import Page from '../components/reader/Page';
|
||||||
|
import PageNumber from '../components/reader/PageNumber';
|
||||||
|
import WebtoonPager from '../components/reader/pager/PagedPager';
|
||||||
|
import VerticalPager from '../components/reader/pager/VerticalPager';
|
||||||
|
import ReaderNavBar, { defaultReaderSettings } from '../components/navbar/ReaderNavBar';
|
||||||
import NavbarContext from '../context/NavbarContext';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
import client from '../util/client';
|
import client from '../util/client';
|
||||||
import useLocalStorage from '../util/useLocalStorage';
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
import cloneObject from '../util/cloneObject';
|
||||||
|
|
||||||
const useStyles = (settings: IReaderSettings) => makeStyles({
|
const useStyles = (settings: IReaderSettings) => makeStyles({
|
||||||
reader: {
|
root: {
|
||||||
display: 'flex',
|
width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw',
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
margin: '0 auto',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
margin: '50px auto',
|
margin: '50px auto',
|
||||||
},
|
},
|
||||||
|
|
||||||
pageNumber: {
|
|
||||||
display: settings.showPageNumber ? 'block' : 'none',
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '50px',
|
|
||||||
right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
|
|
||||||
width: '50px',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getReaderComponent = (readerType: ReaderType) => {
|
||||||
|
switch (readerType) {
|
||||||
|
case 'ContinuesVertical':
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
case 'Webtoon':
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
case 'SingleVertical':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'SingleRTL':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'SingleLTR':
|
||||||
|
return WebtoonPager;
|
||||||
|
break;
|
||||||
|
case 'ContinuesHorizontal':
|
||||||
|
return HorizontalPager;
|
||||||
|
default:
|
||||||
|
return VerticalPager;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
const initialChapter = () => ({ pageCount: -1, chapterIndex: -1, chapterCount: 0 });
|
const initialChapter = () => ({ pageCount: -1, index: -1, chapterCount: 0 });
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
|
const [settings, setSettings] = useLocalStorage<IReaderSettings>('readerSettings', defaultReaderSettings);
|
||||||
@@ -50,13 +66,26 @@ export default function Reader() {
|
|||||||
|
|
||||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const { chapterIndex, mangaId } = useParams<{chapterIndex: string, mangaId: string}>();
|
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>();
|
||||||
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
||||||
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
|
const [chapter, setChapter] = useState<IChapter | IPartialChpter>(initialChapter());
|
||||||
const [curPage, setCurPage] = useState<number>(0);
|
const [curPage, setCurPage] = useState<number>(0);
|
||||||
|
|
||||||
const { setOverride, setTitle } = useContext(NavbarContext);
|
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// make sure settings has all the keys
|
||||||
|
const settingsClone = cloneObject(settings) as any;
|
||||||
|
const defualtSettings = defaultReaderSettings();
|
||||||
|
let shouldUpdateSettings = false;
|
||||||
|
Object.keys(defualtSettings).forEach((key) => {
|
||||||
|
const keyOf = key as keyof IReaderSettings;
|
||||||
|
if (settings[keyOf] === undefined) {
|
||||||
|
settingsClone[keyOf] = defualtSettings[keyOf];
|
||||||
|
shouldUpdateSettings = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (shouldUpdateSettings) { setSettings(settingsClone); }
|
||||||
|
|
||||||
|
// set the custom navbar
|
||||||
setOverride(
|
setOverride(
|
||||||
{
|
{
|
||||||
status: true,
|
status: true,
|
||||||
@@ -88,6 +117,7 @@ export default function Reader() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChapter(initialChapter);
|
setChapter(initialChapter);
|
||||||
|
setCurPage(0);
|
||||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
@@ -102,20 +132,30 @@ export default function Reader() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pages = range(chapter.pageCount).map((index) => ({
|
||||||
|
index,
|
||||||
|
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ReaderComponent = getReaderComponent(settings.readerType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.reader}>
|
<div className={classes.root}>
|
||||||
<div className={classes.pageNumber}>
|
<PageNumber
|
||||||
{`${curPage + 1} / ${chapter.pageCount}`}
|
settings={settings}
|
||||||
</div>
|
curPage={curPage}
|
||||||
{range(chapter.pageCount).map((index) => (
|
pageCount={chapter.pageCount}
|
||||||
<Page
|
/>
|
||||||
key={index}
|
<ReaderComponent
|
||||||
index={index}
|
pages={pages}
|
||||||
src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`}
|
pageCount={chapter.pageCount}
|
||||||
setCurPage={setCurPage}
|
setCurPage={setCurPage}
|
||||||
settings={settings}
|
curPage={curPage}
|
||||||
/>
|
settings={settings}
|
||||||
))}
|
manga={manga}
|
||||||
|
chapter={chapter}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function SearchSingle() {
|
|||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{ sourceId: string }>();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default function SourceMangas(props: { popular: boolean }) {
|
|||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{ sourceId: string }>();
|
||||||
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
const [mangas, setMangas] = useState<IMangaCard[]>([]);
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||||
|
|||||||
Vendored
+39
-4
@@ -50,24 +50,29 @@ interface IManga {
|
|||||||
|
|
||||||
inLibrary: boolean
|
inLibrary: boolean
|
||||||
source: ISource
|
source: ISource
|
||||||
|
|
||||||
|
freshData: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChapter {
|
interface IChapter {
|
||||||
id: number
|
id: number
|
||||||
url: string
|
url: string
|
||||||
name: string
|
name: string
|
||||||
date_upload: number
|
uploadDate: number
|
||||||
chapter_number: number
|
chapterNumber: number
|
||||||
scanlator: String
|
scanlator: String
|
||||||
mangaId: number
|
mangaId: number
|
||||||
chapterIndex: number
|
read: boolean
|
||||||
|
bookmarked: boolean
|
||||||
|
lastPageRead: number
|
||||||
|
index: number
|
||||||
chapterCount: number
|
chapterCount: number
|
||||||
pageCount: number
|
pageCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPartialChpter {
|
interface IPartialChpter {
|
||||||
pageCount: number
|
pageCount: number
|
||||||
chapterIndex: number
|
index: number
|
||||||
chapterCount: number
|
chapterCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,3 +87,33 @@ interface INavbarOverride {
|
|||||||
status: boolean
|
status: boolean
|
||||||
value: any
|
value: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReaderType =
|
||||||
|
'ContinuesVertical'|
|
||||||
|
'Webtoon' |
|
||||||
|
'SingleVertical' |
|
||||||
|
'SingleRTL' |
|
||||||
|
'SingleLTR' |
|
||||||
|
'ContinuesHorizontal';
|
||||||
|
|
||||||
|
interface IReaderSettings{
|
||||||
|
staticNav: boolean
|
||||||
|
showPageNumber: boolean
|
||||||
|
loadNextonEnding: boolean
|
||||||
|
readerType: ReaderType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReaderPage {
|
||||||
|
index: number
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IReaderProps {
|
||||||
|
pages: Array<IReaderPage>
|
||||||
|
pageCount: number
|
||||||
|
setCurPage: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
curPage: number
|
||||||
|
settings: IReaderSettings
|
||||||
|
manga: IMangaCard | IManga
|
||||||
|
chapter: IChapter | IPartialChpter
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form 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/. */
|
||||||
|
|
||||||
|
export default function cloneObject<T extends object>(obj: T) {
|
||||||
|
return JSON.parse(JSON.stringify(obj)) as T;
|
||||||
|
}
|
||||||
+1822
-1780
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user