Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84206a7074 | |||
| d1500baae1 | |||
| 045801dd1a | |||
| 14a2cbc793 | |||
| fd385017df | |||
| 9b05954cf2 | |||
| 6aaf636069 | |||
| d30e89e5ec | |||
| 7acc745478 | |||
| 5a9a2d816e | |||
| 105f11ed02 | |||
| 3021437a05 | |||
| 439602fc03 | |||
| 34e13b9589 | |||
| 2aab4ae918 | |||
| 7ef67671a4 | |||
| e8df84416c | |||
| be930bb68b | |||
| db52948865 | |||
| d2a72526f6 | |||
| 0a9f57b32b | |||
| 180f210536 | |||
| c1baa31eed | |||
| cacc97cec7 | |||
| d5691fd81c | |||
| 49dc9fe5f6 | |||
| c0b49c7428 | |||
| fa345af42d | |||
| db3cc786a1 | |||
| fe879ae51d | |||
| 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 |
@@ -25,3 +25,4 @@
|
|||||||
*.pyc binary
|
*.pyc binary
|
||||||
*.swp binary
|
*.swp binary
|
||||||
*.pdf binary
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
rm -rf preview/*.jar preview/*.zip
|
||||||
|
|
||||||
cp master/server/build/Tachidesk-*.jar preview
|
cp master/server/build/Tachidesk-*.jar preview
|
||||||
|
cp master/server/build/Tachidesk-*.zip preview
|
||||||
|
|
||||||
cd preview
|
cd preview
|
||||||
|
|
||||||
new_jar_build=$(ls *.jar| tail -1)
|
new_jar_build=$(ls Tachidesk-*.jar)
|
||||||
echo "last jar build file name: $new_jar_build"
|
echo "last jar build file name: $new_jar_build"
|
||||||
|
|
||||||
cp -f $new_jar_build Tachidesk-latest.jar
|
latest=$(echo $new_jar_build | sed -e's/Tachidesk-\|.jar//g')
|
||||||
|
|
||||||
rm -rf latest_pointer/*
|
|
||||||
cp $new_jar_build latest_pointer
|
|
||||||
|
|
||||||
latest=$(ls *.jar | tail -n1 | cut -d"-" -f3 | cut -d"." -f1)
|
|
||||||
echo "{ \"latest\": \"$latest\" }" > index.json
|
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"
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/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
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build FatJar
|
name: Build artifacts and deploy preview
|
||||||
needs: check_wrapper
|
needs: check_wrapper
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -59,22 +59,30 @@ jobs:
|
|||||||
**/react/node_modules
|
**/react/node_modules
|
||||||
key: ${{ runner.os }}-${{ hashFiles('**/react/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 packages
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows32-bundler.sh
|
||||||
|
./windows64-bundler.sh
|
||||||
|
|
||||||
- name: Checkout preview branch
|
- name: Checkout preview branch
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
ref: preview
|
repository: 'Suwayomi/Tachidesk-preview'
|
||||||
|
ref: main
|
||||||
path: preview
|
path: preview
|
||||||
|
token: ${{ secrets.DEPLOY_PREVIEW_TOKEN }}
|
||||||
|
|
||||||
- name: Deploy preview
|
- name: Deploy preview
|
||||||
run: |
|
run: |
|
||||||
./master/.github/scripts/commit-repo.sh
|
./master/.github/scripts/commit-preview.sh
|
||||||
@@ -56,54 +56,30 @@ 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 packages
|
||||||
|
run: |
|
||||||
|
cd master/scripts
|
||||||
|
./windows32-bundler.sh
|
||||||
|
./windows64-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/server/build/*.jar;master/server/build/*-win32.zip"
|
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 }}
|
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ build
|
|||||||
server/src/main/resources/react
|
server/src/main/resources/react
|
||||||
server/tmp/
|
server/tmp/
|
||||||
server/tachiserver-data/
|
server/tachiserver-data/
|
||||||
|
|
||||||
|
# OpenJDK downlaods
|
||||||
|
OpenJDK*
|
||||||
@@ -87,7 +87,6 @@ function Dedupe($path)
|
|||||||
}
|
}
|
||||||
|
|
||||||
Dedupe "AndroidCompat/src/main/java"
|
Dedupe "AndroidCompat/src/main/java"
|
||||||
Dedupe "server/src/main/java"
|
|
||||||
Dedupe "server/src/main/kotlin"
|
Dedupe "server/src/main/kotlin"
|
||||||
|
|
||||||
Write-Output "Copying Android.jar to library folder..."
|
Write-Output "Copying Android.jar to library folder..."
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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 ..
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ zip --delete android.jar javax/*
|
|||||||
echo "Removing java..."
|
echo "Removing java..."
|
||||||
zip --delete android.jar java/*
|
zip --delete android.jar java/*
|
||||||
|
|
||||||
echo "Removing overriden classes..."
|
echo "Removing overridden classes..."
|
||||||
zip --delete android.jar android/app/Application.class
|
zip --delete android.jar android/app/Application.class
|
||||||
zip --delete android.jar android/app/Service.class
|
zip --delete android.jar android/app/Service.class
|
||||||
zip --delete android.jar android/net/Uri.class
|
zip --delete android.jar android/net/Uri.class
|
||||||
@@ -68,12 +68,12 @@ zip --delete android.jar android/os/Environment.class
|
|||||||
zip --delete android.jar android/text/format/Formatter.class
|
zip --delete android.jar android/text/format/Formatter.class
|
||||||
zip --delete android.jar android/text/Html.class
|
zip --delete android.jar android/text/Html.class
|
||||||
|
|
||||||
# Dedup overriden Android classes
|
# Dedup overridden Android classes
|
||||||
ABS_JAR="$(realpath android.jar)"
|
ABS_JAR="$(realpath android.jar)"
|
||||||
function dedup() {
|
function dedup() {
|
||||||
pushd "$1"
|
pushd "$1"
|
||||||
CLASSES="$(find * -type f)"
|
CLASSES="$(find ./* -type f)"
|
||||||
echo "$CLASSES" | while read class
|
echo "$CLASSES" | while read -r class
|
||||||
do
|
do
|
||||||
NAME="${class%.*}"
|
NAME="${class%.*}"
|
||||||
echo "Processing class: $NAME"
|
echo "Processing class: $NAME"
|
||||||
@@ -82,13 +82,10 @@ function dedup() {
|
|||||||
popd
|
popd
|
||||||
}
|
}
|
||||||
|
|
||||||
pushd ..
|
popd
|
||||||
dedup AndroidCompat/src/main/java
|
dedup AndroidCompat/src/main/java
|
||||||
dedup server/src/main/java
|
|
||||||
dedup server/src/main/kotlin
|
dedup server/src/main/kotlin
|
||||||
popd
|
|
||||||
|
|
||||||
popd
|
|
||||||
echo "Copying Android.jar to library folder..."
|
echo "Copying Android.jar to library folder..."
|
||||||
mv tmp/android.jar AndroidCompat/lib
|
mv tmp/android.jar AndroidCompat/lib
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
First Build the jar, then cd into the `scripts` directory and run `./windows<bits>-bundler.sh` (or `./windows<bits>-bundler.ps1` if you are on windows), the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win64.zip`.
|
||||||
|
## Running in development mode
|
||||||
|
First satisfy [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 opened 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,19 +1,21 @@
|
|||||||
|
|
||||||
| Build | Stable | Preview | Support Server |
|
| Build | Stable | Preview | Support Server |
|
||||||
|-------|----------|---------|---------|
|
|-------|----------|---------|---------|
|
||||||
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/latest_pointer) | [](https://discord.gg/DDZdqZWaHA) |
|
|  | [](https://github.com/Suwayomi/Tachidesk/releases) | [](https://github.com/Suwayomi/Tachidesk/tree/preview/) | [](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"/>
|
<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 an independent Tachiyomi compatible software made by [@AriaMoradi AKA ArMor](https://github.com/AriaMoradi) and contributors and is **not a Fork of** Tachiyomi.
|
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.
|
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.
|
||||||
|
|
||||||
|
**Tachidesk needs serious front-end dev help for it's reader and other parts, if you like the app and want to see it become better please don't hesitate to contribute some code!**
|
||||||
|
|
||||||
## Is this application usable? Should I test it?
|
## Is this application usable? Should I test it?
|
||||||
Here is a list of current features:
|
Here is a list of current features:
|
||||||
|
|
||||||
@@ -24,22 +26,20 @@ Here is a list of current features:
|
|||||||
- 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
|
- 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 "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
Download the latest "Stable" jar release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases) or a preview jar build from [the preview branch](https://github.com/Suwayomi/Tachidesk/tree/preview).
|
||||||
|
|
||||||
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.
|
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 or win64 (depending on your system, usually you want win64) release from [the releases section](https://github.com/Suwayomi/Tachidesk/releases).
|
||||||
|
|
||||||
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`. The rest works like the previous section.
|
The Windows specific build has java bundled inside, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win64.zip` and run `Tachidesk Launcher.exe` or `Tachidesk Launcher.bat`. The rest works like the previous section.
|
||||||
|
|
||||||
### Arch Linux
|
### Arch Linux
|
||||||
You can install Tachidesk from the AUR
|
You can install Tachidesk from the AUR
|
||||||
@@ -50,52 +50,14 @@ yay -S tachidesk
|
|||||||
### 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, make sure that it's not running(right click on tray icon and quit or kill it through the way your Operating System provides), delete the directory below and re-run the app (**This procedure will delete all your data!**) and if the problem persists open an issue or ask for help on discord.
|
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/DDZdqZWaHA) to hang out with the community and to 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
|
|
||||||
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
|
|
||||||
- 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 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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
jre\bin\java -Dir.armor.tachidesk.debugLogsEnabled=true -jar Tachidesk.jar
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
start "" jre/bin/javaw -jar Tachidesk.jar
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
system("start jre\\bin\\javaw -jar Tachidesk.jar");
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Building `Tachidesk Launcher.exe`
|
||||||
|
1. compile `Tachidesk Launcher.c` statically using GCC MinGW: `gcc -o "Tachidesk Launcher.exe" "Tachidesk Launcher.c"`
|
||||||
|
2. Add `server/src/main/resources/icon/faviconlogo.ico` into the exe with `rcedit` from the electron project: `rcedit "Tachidesk Launcher.exe" --set-icon faviconlogo.ico`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
Write-Output "Downloading jre..."
|
||||||
|
|
||||||
|
$jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
if (!(Test-Path $jre)) {
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "creating windows bundle"
|
||||||
|
|
||||||
|
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||||
|
$release_name=$jar.BaseName + "-win32"
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
New-Item -ItemType Directory $release_name
|
||||||
|
|
||||||
|
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||||
|
|
||||||
|
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||||
|
|
||||||
|
Copy-Item "resources/Tachidesk Launcher-win32.exe" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
$zip_name="$release_name.zip"
|
||||||
|
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Remove-Item -Force -Recurse $release_name
|
||||||
|
|
||||||
|
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
echo "Downloading jre..."
|
||||||
|
|
||||||
|
jre="OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip"
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x86-32_windows_hotspot_8u292b10.zip" -o $jre
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win32
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
unzip $jre
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
mv jdk8u292-b10-jre $release_name/jre
|
||||||
|
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
|
||||||
|
cp "resources/Tachidesk Launcher-win32.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
Write-Output "Downloading jre..."
|
||||||
|
|
||||||
|
$jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
if (!(Test-Path $jre)) {
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -OutFile $jre -UseBasicParsing
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "creating windows bundle"
|
||||||
|
|
||||||
|
$jar=$(Get-ChildItem ../server/build/Tachidesk-*.jar)
|
||||||
|
$release_name=$jar.BaseName + "-win64"
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
New-Item -ItemType Directory $release_name
|
||||||
|
|
||||||
|
Expand-Archive $jre -DestinationPath "./" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
Move-Item "jdk8u292-b10-jre" "$release_name/jre"
|
||||||
|
|
||||||
|
Copy-Item $jar.FullName "$release_name/Tachidesk.jar"
|
||||||
|
|
||||||
|
Copy-Item "resources/Tachidesk Launcher-win64.exe" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
Copy-Item "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
$zip_name="$release_name.zip"
|
||||||
|
Compress-Archive -CompressionLevel Optimal -DestinationPath $zip_name -Path $release_name -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
Remove-Item -Force -Recurse $release_name
|
||||||
|
|
||||||
|
Move-Item $zip_name "../server/build/" -ErrorAction SilentlyContinue
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Copyright (C) Contributors to the Suwayomi project
|
||||||
|
#
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
echo "Downloading jre..."
|
||||||
|
|
||||||
|
jre="OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip"
|
||||||
|
if [ ! -f $jre ]; then
|
||||||
|
curl -L "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u292-b10/OpenJDK8U-jre_x64_windows_hotspot_8u292b10.zip" -o $jre
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "creating windows bundle"
|
||||||
|
|
||||||
|
jar=$(ls ../server/build/Tachidesk-*.jar)
|
||||||
|
jar_name=$(echo $jar | cut -d'/' -f4)
|
||||||
|
release_name=$(echo $jar_name | cut -d'.' -f4 --complement)-win64
|
||||||
|
|
||||||
|
# make release dir
|
||||||
|
mkdir $release_name
|
||||||
|
|
||||||
|
unzip $jre
|
||||||
|
|
||||||
|
# move jre
|
||||||
|
mv jdk8u292-b10-jre $release_name/jre
|
||||||
|
|
||||||
|
cp $jar $release_name/Tachidesk.jar
|
||||||
|
|
||||||
|
cp "resources/Tachidesk Launcher-win64.exe" "$release_name/Tachidesk Launcher.exe"
|
||||||
|
cp "resources/Tachidesk Launcher.bat" $release_name
|
||||||
|
cp "resources/Tachidesk Debug Launcher.bat" $release_name
|
||||||
|
|
||||||
|
zip_name=$release_name.zip
|
||||||
|
zip -9 -r $zip_name $release_name
|
||||||
|
|
||||||
|
rm -rf $release_name
|
||||||
|
|
||||||
|
mv $zip_name ../server/build/
|
||||||
+4
-69
@@ -8,7 +8,6 @@ plugins {
|
|||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||||
id("org.jmailen.kotlinter") version "3.4.3"
|
id("org.jmailen.kotlinter") version "3.4.3"
|
||||||
id("edu.sc.seis.launch4j") version "2.5.0"
|
|
||||||
id("de.fuerstenau.buildconfig") version "1.1.8"
|
id("de.fuerstenau.buildconfig") version "1.1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ dependencies {
|
|||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val MainClass = "ir.armor.tachidesk.Main"
|
val MainClass = "ir.armor.tachidesk.MainKt"
|
||||||
application {
|
application {
|
||||||
mainClass.set(MainClass)
|
mainClass.set(MainClass)
|
||||||
}
|
}
|
||||||
@@ -97,7 +96,7 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.3.0"
|
val tachideskVersion = "v0.3.7"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = Runtime
|
val tachideskRevision = Runtime
|
||||||
@@ -126,18 +125,8 @@ buildConfig {
|
|||||||
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
buildConfigField("boolean", "debug", project.hasProperty("debugApp").toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
launch4j { //used for windows
|
|
||||||
mainClassName = MainClass
|
|
||||||
bundledJrePath = "jre"
|
|
||||||
bundledJre64Bit = true
|
|
||||||
jreMinVersion = "8"
|
|
||||||
outputDir = "${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"
|
|
||||||
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
|
||||||
jar = "${projectDir}/build/${rootProject.name}-$tachideskVersion-$tachideskRevision.jar"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
jar {
|
shadowJar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -149,9 +138,6 @@ tasks {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
shadowJar {
|
|
||||||
manifest.inheritFrom(jar.get().manifest) //will make your shadowJar (produced by jar task) runnable
|
|
||||||
archiveBaseName.set(rootProject.name)
|
archiveBaseName.set(rootProject.name)
|
||||||
archiveVersion.set(tachideskVersion)
|
archiveVersion.set(tachideskVersion)
|
||||||
archiveClassifier.set(tachideskRevision)
|
archiveClassifier.set(tachideskRevision)
|
||||||
@@ -165,61 +151,11 @@ tasks {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnit()
|
useJUnit()
|
||||||
}
|
}
|
||||||
|
|
||||||
register<Zip>("windowsPackage") {
|
|
||||||
from(fileTree("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32"))
|
|
||||||
destinationDirectory.set(File("$buildDir"))
|
|
||||||
archiveFileName.set("${rootProject.name}-$tachideskVersion-$tachideskRevision-win32.zip")
|
|
||||||
dependsOn("windowsPackageWorkaround2")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("windowsPackageWorkaround2") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jre",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/lib",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/server.exe",
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32/Tachidesk-$tachideskVersion-$tachideskRevision-win32"
|
|
||||||
)
|
|
||||||
dependsOn("windowsPackageWorkaround")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("windowsPackageWorkaround") {
|
|
||||||
from("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
dependsOn("deleteUnwantedJreDir")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Delete>("deleteUnwantedJreDir") {
|
|
||||||
delete(
|
|
||||||
"$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32/jdk8u282-b08-jre"
|
|
||||||
)
|
|
||||||
dependsOn("addJreToDistributable")
|
|
||||||
}
|
|
||||||
|
|
||||||
register<Copy>("addJreToDistributable") {
|
|
||||||
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
|
|
||||||
into("$buildDir/${rootProject.name}-$tachideskVersion-$tachideskRevision-win32")
|
|
||||||
eachFile {
|
|
||||||
path = path.replace(".*-jre".toRegex(), "jre")
|
|
||||||
}
|
|
||||||
dependsOn("downloadJre")
|
|
||||||
dependsOn("createExe")
|
|
||||||
}
|
|
||||||
|
|
||||||
named("createExe") {
|
|
||||||
dependsOn("shadowJar")
|
|
||||||
}
|
|
||||||
|
|
||||||
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/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
|
|
||||||
overwrite(false)
|
|
||||||
onlyIfModified(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
withType<ShadowJar> {
|
withType<ShadowJar> {
|
||||||
destinationDirectory.set(File("$rootDir/server/build"))
|
destinationDirectory.set(File("$rootDir/server/build"))
|
||||||
dependsOn("formatKotlin", "lintKotlin")
|
dependsOn("formatKotlin", "lintKotlin")
|
||||||
@@ -242,4 +178,3 @@ tasks {
|
|||||||
source(files("src"))
|
source(files("src"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,39 @@ import ir.armor.tachidesk.impl.util.awaitSingle
|
|||||||
import ir.armor.tachidesk.model.database.table.ChapterTable
|
import ir.armor.tachidesk.model.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.model.database.table.MangaTable
|
import ir.armor.tachidesk.model.database.table.MangaTable
|
||||||
import ir.armor.tachidesk.model.database.table.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.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
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> {
|
||||||
|
return if (onlineFetch == true) {
|
||||||
|
getSourceChapters(mangaId)
|
||||||
|
} else {
|
||||||
|
transaction {
|
||||||
|
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
|
||||||
|
.map {
|
||||||
|
ChapterTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}.ifEmpty {
|
||||||
|
// If it was explicitly set to offline dont grab chapters
|
||||||
|
if (onlineFetch == null) {
|
||||||
|
getSourceChapters(mangaId)
|
||||||
|
} else emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
||||||
val mangaDetails = getManga(mangaId)
|
val mangaDetails = getManga(mangaId)
|
||||||
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
val source = getHttpSource(mangaDetails.sourceId.toLong())
|
||||||
|
|
||||||
val chapterList = source.fetchChapterList(
|
val chapterList = source.fetchChapterList(
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
title = mangaDetails.title
|
title = mangaDetails.title
|
||||||
@@ -38,7 +57,7 @@ object Chapter {
|
|||||||
|
|
||||||
val chapterCount = chapterList.count()
|
val chapterCount = chapterList.count()
|
||||||
|
|
||||||
return transaction {
|
transaction {
|
||||||
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
|
||||||
if (chapterEntry == null) {
|
if (chapterEntry == null) {
|
||||||
@@ -64,25 +83,50 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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 } }
|
||||||
}
|
|
||||||
|
|
||||||
chapterList.mapIndexed { index, it ->
|
dbChapterList.forEach {
|
||||||
ChapterDataClass(
|
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
|
||||||
it.url,
|
chapterList[it[ChapterTable.chapterIndex] - 1].url != it[ChapterTable.url]
|
||||||
it.name,
|
) {
|
||||||
it.date_upload,
|
transaction {
|
||||||
it.chapter_number,
|
PageTable.deleteWhere { PageTable.chapter eq it[ChapterTable.id] }
|
||||||
it.scanlator,
|
ChapterTable.deleteWhere { ChapterTable.id eq it[ChapterTable.id] }
|
||||||
mangaId,
|
}
|
||||||
chapterCount - index,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
it.url,
|
||||||
|
it.name,
|
||||||
|
it.date_upload,
|
||||||
|
it.chapter_number,
|
||||||
|
it.scanlator,
|
||||||
|
mangaId,
|
||||||
|
|
||||||
|
dbChapter[ChapterTable.isRead],
|
||||||
|
dbChapter[ChapterTable.isBookmarked],
|
||||||
|
dbChapter[ChapterTable.lastPageRead],
|
||||||
|
|
||||||
|
chapterCount - index,
|
||||||
|
chapterList.size
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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 */
|
||||||
@@ -90,9 +134,9 @@ object Chapter {
|
|||||||
val chapterEntry = transaction {
|
val chapterEntry = transaction {
|
||||||
ChapterTable.select {
|
ChapterTable.select {
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
}.firstOrNull()!!
|
}.first()
|
||||||
}
|
}
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val pageList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
@@ -103,7 +147,7 @@ object Chapter {
|
|||||||
).awaitSingle()
|
).awaitSingle()
|
||||||
|
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
val chapterCount = transaction { ChapterTable.selectAll().count() }
|
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
|
|
||||||
// update page list for this chapter
|
// update page list for this chapter
|
||||||
transaction {
|
transaction {
|
||||||
@@ -132,9 +176,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ object Extension {
|
|||||||
it[this.classFQName] = className
|
it[this.classFQName] = className
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!![ExtensionTable.id].value
|
val extensionId = ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first()[ExtensionTable.id].value
|
||||||
|
|
||||||
sources.forEach { httpSource ->
|
sources.forEach { httpSource ->
|
||||||
SourceTable.insert {
|
SourceTable.insert {
|
||||||
@@ -195,7 +195,7 @@ object Extension {
|
|||||||
fun uninstallExtension(pkgName: String) {
|
fun uninstallExtension(pkgName: String) {
|
||||||
logger.debug("Uninstalling $pkgName")
|
logger.debug("Uninstalling $pkgName")
|
||||||
|
|
||||||
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.firstOrNull()!! }
|
val extensionRecord = transaction { ExtensionTable.select { ExtensionTable.pkgName eq pkgName }.first() }
|
||||||
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
transaction {
|
transaction {
|
||||||
@@ -234,7 +234,7 @@ object Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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
|
||||||
@@ -35,17 +36,17 @@ object Manga {
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
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 }.first() }
|
||||||
|
|
||||||
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,
|
||||||
|
|
||||||
@@ -55,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])
|
||||||
@@ -76,13 +78,14 @@ object Manga {
|
|||||||
it[MangaTable.description] = truncate(fetchedManga.description, 4096)
|
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.orEmpty().isNotEmpty())
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
clearMangaThumbnail(mangaId)
|
||||||
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
|
||||||
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
@@ -90,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,
|
||||||
|
|
||||||
@@ -100,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 }.first() }
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearMangaThumbnail(mangaId: Int) {
|
||||||
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
|
clearCachedImage(saveDir, fileName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ object MangaList {
|
|||||||
val mangasPage = this
|
val mangasPage = this
|
||||||
val mangaList = transaction {
|
val mangaList = transaction {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
val mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||||
if (mangaEntry == null) { // create manga entry
|
if (mangaEntry == null) { // create manga entry
|
||||||
val mangaId = MangaTable.insertAndGetId {
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
it[url] = manga.url
|
it[url] = manga.url
|
||||||
|
|||||||
@@ -40,16 +40,16 @@ object Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
|
||||||
val chapterEntry = transaction {
|
val chapterEntry = transaction {
|
||||||
ChapterTable.select {
|
ChapterTable.select {
|
||||||
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
|
||||||
}.firstOrNull()!!
|
}.first()
|
||||||
}
|
}
|
||||||
val chapterId = chapterEntry[ChapterTable.id].value
|
val chapterId = chapterEntry[ChapterTable.id].value
|
||||||
|
|
||||||
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.first() }
|
||||||
|
|
||||||
val tachiPage = Page(
|
val tachiPage = Page(
|
||||||
pageEntry[PageTable.index],
|
pageEntry[PageTable.index],
|
||||||
@@ -78,11 +78,11 @@ object Page {
|
|||||||
// TODO: rewrite this to match tachiyomi
|
// TODO: rewrite this to match tachiyomi
|
||||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.first() }
|
||||||
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() }
|
||||||
|
|
||||||
val chapterDir = when {
|
val chapterDir = when {
|
||||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
|
|||||||
@@ -80,11 +80,11 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
return validationResult
|
return validationResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(jsonCategories: JsonElement) { // TODO
|
private fun restoreCategories(jsonCategories: JsonElement) {
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||||
val dbCategories = getCategoryList()
|
val dbCategories = getCategoryList()
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them and create missing categories
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
if (dbCategories.none { it.name == category.name }) {
|
if (dbCategories.none { it.name == category.name }) {
|
||||||
createCategory(category.name)
|
createCategory(category.name)
|
||||||
@@ -198,7 +198,7 @@ object LegacyBackupImport : LegacyBackupBase() {
|
|||||||
it[description] = fetchedManga.description
|
it[description] = fetchedManga.description
|
||||||
it[genre] = fetchedManga.genre
|
it[genre] = fetchedManga.genre
|
||||||
it[status] = fetchedManga.status
|
it[status] = fetchedManga.status
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url.orEmpty().isNotEmpty())
|
||||||
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ package ir.armor.tachidesk.impl.util
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import java.io.BufferedInputStream
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -19,12 +18,13 @@ import java.nio.file.Paths
|
|||||||
|
|
||||||
object CachedImageResponse {
|
object CachedImageResponse {
|
||||||
private fun pathToInputStream(path: String): InputStream {
|
private fun pathToInputStream(path: String): InputStream {
|
||||||
return BufferedInputStream(FileInputStream(path))
|
return FileInputStream(path).buffered()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
private fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
File(directoryPath).listFiles().forEach { file ->
|
val target = "$fileName."
|
||||||
if (file.name.startsWith(fileName))
|
File(directoryPath).listFiles().orEmpty().forEach { file ->
|
||||||
|
if (file.name.startsWith(target))
|
||||||
return "$directoryPath/${file.name}"
|
return "$directoryPath/${file.name}"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -56,12 +56,16 @@ object CachedImageResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Pair(
|
return pathToInputStream(fullPath) to contentType
|
||||||
pathToInputStream(fullPath),
|
|
||||||
contentType
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
throw Exception("request error! ${response.code}")
|
throw Exception("request error! ${response.code}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCachedImage(saveDir: String, fileName: String) {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
cachedFile?.also {
|
||||||
|
File(it).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ object GetHttpSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val sourceRecord = transaction {
|
val sourceRecord = transaction {
|
||||||
SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!!
|
SourceTable.select { SourceTable.id eq sourceId }.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
val extensionId = sourceRecord[SourceTable.extension]
|
val extensionId = sourceRecord[SourceTable.extension]
|
||||||
val extensionRecord = transaction {
|
val extensionRecord = transaction {
|
||||||
ExtensionTable.select { ExtensionTable.id eq extensionId }.firstOrNull()!!
|
ExtensionTable.select { ExtensionTable.id eq extensionId }.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
val apkName = extensionRecord[ExtensionTable.apkName]
|
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
@@ -26,7 +27,9 @@ suspend fun Call.await(): Response {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
continuation.resume(response)
|
continuation.resume(response) {
|
||||||
|
response.body?.closeQuietly()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ object PackageTools {
|
|||||||
if (handler.hasException()) {
|
if (handler.hasException()) {
|
||||||
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
val errorFile: Path = File(applicationDirs.extensionsRoot).toPath().resolve("$fileNameWithoutType-error.txt")
|
||||||
logger.error(
|
logger.error(
|
||||||
"Detail Error Information in File $errorFile\n" +
|
"""
|
||||||
"Please report this file to one of following link if possible (any one).\n" +
|
Detail Error Information in File $errorFile
|
||||||
" https://sourceforge.net/p/dex2jar/tickets/\n" +
|
Please report this file to one of following link if possible (any one).
|
||||||
" https://bitbucket.org/pxb1988/dex2jar/issues\n" +
|
https://sourceforge.net/p/dex2jar/tickets/
|
||||||
" https://github.com/pxb1988/dex2jar/issues\n" +
|
https://bitbucket.org/pxb1988/dex2jar/issues
|
||||||
" dex2jar@googlegroups.com"
|
https://github.com/pxb1988/dex2jar/issues
|
||||||
|
dex2jar@googlegroups.com
|
||||||
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
handler.dump(errorFile, emptyArray<String>())
|
handler.dump(errorFile, emptyArray<String>())
|
||||||
}
|
}
|
||||||
@@ -98,18 +100,21 @@ object PackageTools {
|
|||||||
applicationInfo.metaData = Bundle().apply {
|
applicationInfo.metaData = Bundle().apply {
|
||||||
val appTag = doc.getElementsByTagName("application").item(0)
|
val appTag = doc.getElementsByTagName("application").item(0)
|
||||||
|
|
||||||
appTag?.childNodes?.toList()?.filter {
|
appTag?.childNodes?.toList()
|
||||||
it.nodeType == Node.ELEMENT_NODE
|
.orEmpty()
|
||||||
}?.map {
|
.asSequence()
|
||||||
it as Element
|
.filter {
|
||||||
}?.filter {
|
it.nodeType == Node.ELEMENT_NODE
|
||||||
it.tagName == "meta-data"
|
}.map {
|
||||||
}?.map {
|
it as Element
|
||||||
putString(
|
}.filter {
|
||||||
it.attributes.getNamedItem("android:name").nodeValue,
|
it.tagName == "meta-data"
|
||||||
it.attributes.getNamedItem("android:value").nodeValue
|
}.forEach {
|
||||||
)
|
putString(
|
||||||
}
|
it.attributes.getNamedItem("android:name").nodeValue,
|
||||||
|
it.attributes.getNamedItem("android:value").nodeValue
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signatures = (
|
signatures = (
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ import org.kodein.di.DI
|
|||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import org.kodein.di.instance
|
import org.kodein.di.instance
|
||||||
|
|
||||||
object DBMangaer {
|
object DBManager {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||||
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
@@ -24,7 +24,7 @@ object DBMangaer {
|
|||||||
|
|
||||||
fun databaseUp() {
|
fun databaseUp() {
|
||||||
// must mention db object so the lazy block executes
|
// must mention db object so the lazy block executes
|
||||||
val db = DBMangaer.db
|
val db = DBManager.db
|
||||||
db.useNestedTransactions = true
|
db.useNestedTransactions = true
|
||||||
|
|
||||||
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
val migrations = loadMigrationsFrom("ir.armor.tachidesk.model.database.migration")
|
||||||
+95
-77
@@ -1,12 +1,5 @@
|
|||||||
package ir.armor.tachidesk.model.database.migration
|
package ir.armor.tachidesk.model.database.migration
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
*
|
*
|
||||||
@@ -14,103 +7,128 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import 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() {
|
class M0001_Initial : Migration() {
|
||||||
private object ExtensionTable : IntIdTable() {
|
private class ExtensionTable : IntIdTable() {
|
||||||
val apkName = varchar("apk_name", 1024)
|
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")
|
||||||
|
|
||||||
// default is the local source icon from tachiyomi
|
bool("is_installed").default(false)
|
||||||
val iconUrl = varchar("icon_url", 2048)
|
bool("has_update").default(false)
|
||||||
.default("https://raw.githubusercontent.com/tachiyomiorg/tachiyomi/64ba127e7d43b1d7e6d58a6f5c9b2bd5fe0543f7/app/src/main/res/mipmap-xxxhdpi/ic_local_source.webp")
|
bool("is_obsolete").default(false)
|
||||||
|
|
||||||
val name = varchar("name", 128)
|
varchar("class_name", 1024).default("") // fully qualified name
|
||||||
val pkgName = varchar("pkg_name", 128)
|
}
|
||||||
val versionName = varchar("version_name", 16)
|
|
||||||
val versionCode = integer("version_code")
|
|
||||||
val lang = varchar("lang", 10)
|
|
||||||
val isNsfw = bool("is_nsfw")
|
|
||||||
|
|
||||||
val isInstalled = bool("is_installed").default(false)
|
|
||||||
val hasUpdate = bool("has_update").default(false)
|
|
||||||
val isObsolete = bool("is_obsolete").default(false)
|
|
||||||
|
|
||||||
val classFQName = varchar("class_name", 1024).default("") // fully qualified name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object SourceTable : IdTable<Long>() {
|
private class SourceTable(extensionTable: ExtensionTable) : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name = varchar("name", 128)
|
init {
|
||||||
val lang = varchar("lang", 10)
|
varchar("name", 128)
|
||||||
val extension = reference("extension", ExtensionTable)
|
varchar("lang", 10)
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
reference("extension", extensionTable)
|
||||||
|
bool("part_of_factory_source").default(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object MangaTable : IntIdTable() {
|
private class MangaTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
init {
|
||||||
val title = varchar("title", 512)
|
varchar("url", 2048)
|
||||||
val initialized = bool("initialized").default(false)
|
varchar("title", 512)
|
||||||
|
bool("initialized").default(false)
|
||||||
|
|
||||||
val artist = varchar("artist", 64).nullable()
|
varchar("artist", 64).nullable()
|
||||||
val author = varchar("author", 64).nullable()
|
varchar("author", 64).nullable()
|
||||||
val description = varchar("description", 4096).nullable()
|
varchar("description", 4096).nullable()
|
||||||
val genre = varchar("genre", 1024).nullable()
|
varchar("genre", 1024).nullable()
|
||||||
|
|
||||||
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
// val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN)
|
||||||
val status = integer("status").default(SManga.UNKNOWN)
|
integer("status").default(SManga.UNKNOWN)
|
||||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
val inLibrary = bool("in_library").default(false)
|
bool("in_library").default(false)
|
||||||
val defaultCategory = bool("default_category").default(true)
|
bool("default_category").default(true)
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
// source is used by some ancestor of IntIdTable
|
||||||
val sourceReference = long("source")
|
long("source")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ChapterTable : IntIdTable() {
|
private class ChapterTable(mangaTable: MangaTable) : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
init {
|
||||||
val name = varchar("name", 512)
|
varchar("url", 2048)
|
||||||
val date_upload = long("date_upload").default(0)
|
varchar("name", 512)
|
||||||
val chapter_number = float("chapter_number").default(-1f)
|
long("date_upload").default(0)
|
||||||
val scanlator = varchar("scanlator", 128).nullable()
|
float("chapter_number").default(-1f)
|
||||||
|
varchar("scanlator", 128).nullable()
|
||||||
|
|
||||||
val isRead = bool("read").default(false)
|
bool("read").default(false)
|
||||||
val isBookmarked = bool("bookmark").default(false)
|
bool("bookmark").default(false)
|
||||||
val lastPageRead = integer("last_page_read").default(0)
|
integer("last_page_read").default(0)
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
integer("number_in_list")
|
||||||
|
reference("manga", mangaTable)
|
||||||
val manga = reference("manga", MangaTable)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object PageTable : IntIdTable() {
|
private class PageTable(chapterTable: ChapterTable) : IntIdTable() {
|
||||||
val index = integer("index")
|
init {
|
||||||
val url = varchar("url", 2048)
|
integer("index")
|
||||||
val imageUrl = varchar("imageUrl", 2048).nullable()
|
varchar("url", 2048)
|
||||||
|
varchar("imageUrl", 2048).nullable()
|
||||||
val chapter = reference("chapter", ChapterTable)
|
reference("chapter", chapterTable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CategoryTable : IntIdTable() {
|
private class CategoryTable : IntIdTable() {
|
||||||
val name = varchar("name", 64)
|
init {
|
||||||
val isLanding = bool("is_landing").default(false)
|
varchar("name", 64)
|
||||||
val order = integer("order").default(0)
|
bool("is_landing").default(false)
|
||||||
|
integer("order").default(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object CategoryMangaTable : IntIdTable() {
|
private class CategoryMangaTable : IntIdTable() {
|
||||||
val category = reference("category", ir.armor.tachidesk.model.database.table.CategoryTable)
|
init {
|
||||||
val manga = reference("manga", ir.armor.tachidesk.model.database.table.MangaTable)
|
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() {
|
override fun run() {
|
||||||
transaction {
|
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(
|
SchemaUtils.create(
|
||||||
ExtensionTable,
|
extensionTable,
|
||||||
ExtensionTable,
|
sourceTable,
|
||||||
SourceTable,
|
mangaTable,
|
||||||
MangaTable,
|
chapterTable,
|
||||||
ChapterTable,
|
pageTable,
|
||||||
PageTable,
|
categoryTable,
|
||||||
CategoryTable,
|
categoryMangaTable,
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -54,6 +54,7 @@ fun runMigrations(migrations: List<Migration>, database: Database = TransactionM
|
|||||||
logger.info { "Migrations finished successfully" }
|
logger.info { "Migrations finished successfully" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnstableApiUsage")
|
||||||
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
fun loadMigrationsFrom(classPath: String): List<Migration> {
|
||||||
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
return ClassPath.from(Thread.currentThread().contextClassLoader)
|
||||||
.getTopLevelClasses(classPath)
|
.getTopLevelClasses(classPath)
|
||||||
|
|||||||
@@ -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],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package ir.armor.tachidesk.model.database.table
|
|||||||
* 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.dataclass.ChapterDataClass
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
val url = varchar("url", 2048)
|
||||||
@@ -20,7 +22,22 @@ object ChapterTable : IntIdTable() {
|
|||||||
val isBookmarked = bool("bookmark").default(false)
|
val isBookmarked = bool("bookmark").default(false)
|
||||||
val lastPageRead = integer("last_page_read").default(0)
|
val lastPageRead = integer("last_page_read").default(0)
|
||||||
|
|
||||||
val chapterIndex = integer("number_in_list")
|
// index is reserved by a function
|
||||||
|
val chapterIndex = integer("index")
|
||||||
|
|
||||||
val manga = reference("manga", MangaTable)
|
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],
|
||||||
|
)
|
||||||
|
|||||||
@@ -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[MangaTable.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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
/** 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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -55,7 +55,8 @@ import kotlin.concurrent.thread
|
|||||||
|
|
||||||
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(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
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)
|
||||||
@@ -67,7 +68,7 @@ object JavalinSetup {
|
|||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
// if the bellow line throws an exception then webUI is not bundled
|
// if the bellow line throws an exception then webUI is not bundled
|
||||||
Main::class.java.getResource("/react/index.html")
|
this::class.java.getResource("/react/index.html")
|
||||||
|
|
||||||
// no exception so we can tell javalin to serve webUI
|
// no exception so we can tell javalin to serve webUI
|
||||||
hasWebUiBundled = true
|
hasWebUiBundled = true
|
||||||
@@ -95,13 +96,17 @@ object JavalinSetup {
|
|||||||
logger.error("NullPointerException while handling the request", e)
|
logger.error("NullPointerException while handling the request", e)
|
||||||
ctx.status(404)
|
ctx.status(404)
|
||||||
}
|
}
|
||||||
|
app.exception(NoSuchElementException::class.java) { e, ctx ->
|
||||||
|
logger.error("NoSuchElementException while handling the request", e)
|
||||||
|
ctx.status(404)
|
||||||
|
}
|
||||||
app.exception(IOException::class.java) { e, ctx ->
|
app.exception(IOException::class.java) { e, ctx ->
|
||||||
logger.error("IOException while handling the request", e)
|
logger.error("IOException while handling the request", e)
|
||||||
ctx.status(500)
|
ctx.status(500)
|
||||||
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 {
|
||||||
@@ -110,6 +115,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")
|
||||||
|
|
||||||
@@ -120,6 +126,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")
|
||||||
|
|
||||||
@@ -130,6 +137,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")
|
||||||
|
|
||||||
@@ -138,7 +146,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(
|
||||||
@@ -186,9 +194,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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -249,7 +259,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")?.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
|
||||||
@@ -259,6 +272,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()
|
||||||
@@ -273,7 +302,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))
|
||||||
|
|||||||
@@ -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,7 +9,6 @@ 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.database.databaseUp
|
||||||
import ir.armor.tachidesk.server.util.systemTray
|
import ir.armor.tachidesk.server.util.systemTray
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
@@ -42,6 +41,8 @@ val systemTray by lazy { systemTray() }
|
|||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
|
logger.info("Running Tachidesk ${BuildConfig.version} revision ${BuildConfig.revision}")
|
||||||
|
|
||||||
// Application dirs
|
// Application dirs
|
||||||
val applicationDirs = ApplicationDirs()
|
val applicationDirs = ApplicationDirs()
|
||||||
DI.global.addImport(
|
DI.global.addImport(
|
||||||
@@ -81,7 +82,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)
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ fun applicationSetup() {
|
|||||||
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 +110,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}")
|
||||||
|
|||||||
@@ -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.BuildConfig
|
||||||
|
import ir.armor.tachidesk.server.ServerConfig
|
||||||
import ir.armor.tachidesk.server.serverConfig
|
import ir.armor.tachidesk.server.serverConfig
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,11 +45,11 @@ fun systemTray(): SystemTray? {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
val icon = ServerConfig::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
// systemTray.setTooltip("Tachidesk")
|
||||||
systemTray.setImage(icon)
|
systemTray.setImage(icon)
|
||||||
// systemTray.status = "No Mail"
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
mainMenu.add(
|
mainMenu.add(
|
||||||
MenuItem("Quit") {
|
MenuItem("Quit") {
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,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%',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,11 +60,13 @@ function LazyImage(props: IProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('scroll', handleScroll);
|
if (settings.readerType === 'Webtoon' || settings.readerType === 'ContinuesVertical') {
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
|
} return () => {};
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,7 +78,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,35 +90,26 @@ function LazyImage(props: IProps) {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={`Page #${index}`}
|
alt={`Page #${index}`}
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page(props: IProps) {
|
const Page = React.forwardRef((props: IProps, ref: any) => {
|
||||||
const {
|
const {
|
||||||
src, index, setCurPage, settings,
|
src, index, setCurPage, settings,
|
||||||
} = props;
|
} = props;
|
||||||
const classes = useStyles(settings)();
|
const classes = useStyles(settings)();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: '0 auto' }}>
|
<div ref={ref} 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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -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, nextChapter,
|
||||||
|
} = 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) {
|
||||||
|
nextChapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,76 @@
|
|||||||
|
/* 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, useState } 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, nextChapter,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [initialScroll, setInitialScroll] = useState(-1);
|
||||||
|
const initialPageRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleLoadNextonEnding = () => {
|
||||||
|
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||||
|
nextChapter();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.loadNextonEnding) { window.addEventListener('scroll', handleLoadNextonEnding); }
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleLoadNextonEnding);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((chapter as IChapter).lastPageRead > -1) {
|
||||||
|
setInitialScroll((chapter as IChapter).lastPageRead);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialScroll > -1) {
|
||||||
|
initialPageRef.current?.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [initialScroll, initialPageRef.current]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.reader}>
|
||||||
|
{
|
||||||
|
pages.map((page) => (
|
||||||
|
<Page
|
||||||
|
key={page.index}
|
||||||
|
index={page.index}
|
||||||
|
src={page.src}
|
||||||
|
setCurPage={setCurPage}
|
||||||
|
settings={settings}
|
||||||
|
ref={page.index === initialScroll ? initialPageRef : null}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,54 +9,84 @@
|
|||||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
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 { useHistory, 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);
|
||||||
|
|
||||||
const classes = useStyles(settings)();
|
const classes = useStyles(settings)();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
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,
|
||||||
@@ -92,9 +122,24 @@ export default function Reader() {
|
|||||||
.then((response) => response.data)
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
setChapter(data);
|
setChapter(data);
|
||||||
|
setCurPage(data.lastPageRead);
|
||||||
});
|
});
|
||||||
}, [chapterIndex]);
|
}, [chapterIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (curPage !== -1) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('lastPageRead', curPage.toString());
|
||||||
|
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curPage === chapter.pageCount - 1) {
|
||||||
|
const formDataRead = new FormData();
|
||||||
|
formDataRead.append('read', 'true');
|
||||||
|
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formDataRead);
|
||||||
|
}
|
||||||
|
}, [curPage]);
|
||||||
|
|
||||||
if (chapter.pageCount === -1) {
|
if (chapter.pageCount === -1) {
|
||||||
return (
|
return (
|
||||||
<div className={classes.loading}>
|
<div className={classes.loading}>
|
||||||
@@ -102,20 +147,42 @@ export default function Reader() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextChapter = () => {
|
||||||
|
if (chapter.index < chapter.chapterCount) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('lastPageRead', `${chapter.pageCount - 1}`);
|
||||||
|
formData.append('read', 'true');
|
||||||
|
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
||||||
|
|
||||||
|
history.push(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
nextChapter={nextChapter}
|
||||||
|
/>
|
||||||
</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
+40
-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,34 @@ 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
|
||||||
|
nextChapter: () => void
|
||||||
|
}
|
||||||
|
|||||||
@@ -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