Compare commits

..

121 Commits

Author SHA1 Message Date
Aria Moradi 2f55460ffb unzip the jre
CI Publish / Validate Gradle Wrapper (push) Successful in 14s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 23:33:54 +04:30
Aria Moradi fbc5bd4642 bump to v0.3.4 2021-05-17 23:29:36 +04:30
Aria Moradi 5e0c7d3c9d ditch packr because it can't load extension jars 2021-05-17 23:28:23 +04:30
Aria Moradi 083996a48d wating on: https://github.com/Kotlin/kotlinx.coroutines/issues/261
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 17s
2021-05-17 14:30:59 +04:30
Aria Moradi 9d38f478e3 fix slow manga thumbnails issue, next manga reset page issue 2021-05-17 14:22:24 +04:30
Aria Moradi 57274a0a01 remove unused electron script 2021-05-17 12:32:15 +04:30
Aria Moradi b3b56b7fc8 also build windows package for publish 2021-05-17 12:30:15 +04:30
Forgenn 0b690577da Load next chapter when getting to the last page (#84)
* Load next chapter when scrolling to the bottom (Webtoon, Continues Vertical)

* Load next chapter when scrolling to the bottom (Paged reader)

* Added missing types to IReaderProps

* Move load next chapter when at last page to VerticalReader

* Dependency fix

* Use react history for loading next page
2021-05-17 12:27:14 +04:30
Aria Moradi e9683a3a37 bump to v0.3.3 2021-05-17 12:02:01 +04:30
Aria Moradi f8f67b3eba finish up 2021-05-17 11:59:59 +04:30
Aria Moradi 7b16b082d8 needs kt 2021-05-17 11:55:49 +04:30
Aria Moradi 2a783f0d8e btter folder name 2021-05-17 11:54:33 +04:30
Aria Moradi 42ae32de33 give the correct path 2021-05-17 11:40:38 +04:30
Aria Moradi cec7ddc486 update file permissions 2021-05-17 11:28:26 +04:30
Aria Moradi 9c55fc3868 make windows package with packr 2021-05-17 11:20:24 +04:30
Syer10 104c5a8d83 Code cleanup (#85)
* GC Unused or only used once objects

* Move things around a bit

* Revert some changes

* Fix imports

* Revert about change

* Put back logger

* Private logger

* Revert systemtray

* Move import
2021-05-17 02:48:01 +04:30
Aria Moradi 7450b16742 - Reader -> Pager
- add cloneObject
- add missing copyright notices
2021-05-17 01:38:59 +04:30
Aria Moradi 3ecd0931a1 missing from the previous commit 2021-05-17 01:37:25 +04:30
Aria Moradi 2f2a52ae2f Move navbars 2021-05-17 01:36:31 +04:30
Aria Moradi f464087c30 also handle Errors from java 2021-05-16 23:58:32 +04:30
Aria Moradi 2364960388 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-16 22:15:35 +04:30
Aria Moradi 76be4d64cd update launch4j's jre and make it 64bit 2021-05-16 12:39:05 +04:30
Aria Moradi 7d98e8ce47 [SKIP CI] correct link 2021-05-16 01:53:35 +04:30
Aria Moradi 40831fc681 [SKIP CI] new links 2021-05-16 01:52:01 +04:30
Aria Moradi e38e7ccf26 [SKIP CI] troubleshooting from the wiki 2021-05-16 01:48:49 +04:30
Aria Moradi 98b9e2f2cf [SKIP CI] no need to delete data anymore... 2021-05-16 01:47:33 +04:30
Aria Moradi 4bf3c12f76 fixed when spinner stops just after first offline chapter fetch 2021-05-16 01:07:48 +04:30
Aria Moradi bab25f9ad9 bump version
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 23:24:25 +04:30
Aria Moradi a62ee8f8c3 handle reader types 2021-05-15 23:22:37 +04:30
Aria Moradi 5f23691e20 add toast lib 2021-05-15 20:37:07 +04:30
Aria Moradi 3de9ccc62f stop spinning if chapter list is empty 2021-05-15 18:51:38 +04:30
Aria Moradi 1896f7f37b remove lazy load 2021-05-15 18:26:40 +04:30
Aria Moradi 490643dc02 proof of concept readers 2021-05-15 18:17:12 +04:30
Aria Moradi 9808976088 restructure the reader 2021-05-15 17:18:57 +04:30
Aria Moradi 5a73068a10 bump to v0.3.1
CI Publish / Validate Gradle Wrapper (push) Successful in 17s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-15 14:53:44 +04:30
Aria Moradi 01d5c2540d chapter updates when pressing UI buttons 2021-05-15 13:43:26 +04:30
Aria Moradi 866b01f865 support bookmarked and isRead in webUI 2021-05-14 19:22:10 +04:30
Aria Moradi da6a953099 exposed error 2021-05-14 17:31:07 +04:30
Aria Moradi bce8d58845 make it look nicer? 2021-05-14 02:01:28 +04:30
Aria Moradi 3cfce2db04 fix chapters not shown on movbile 2021-05-13 21:40:42 +04:30
Aria Moradi 327aae5dd9 Merge pull request #79 from Suwayomi/read-category
Support more chapter parameters
2021-05-13 18:34:27 +04:30
Aria Moradi 1bdfde7032 no longer TODO 2021-05-13 18:33:24 +04:30
Aria Moradi 295a0817b0 fix wrong chapters being removed, fix da css 2021-05-13 18:29:20 +04:30
Aria Moradi a02dc02d52 remove console prints 2021-05-13 17:49:33 +04:30
Aria Moradi dc012edf7d staisfying results? with chapters scrolling 2021-05-13 17:46:40 +04:30
Aria Moradi 1e2eb11c13 update dependencies 2021-05-13 15:01:37 +04:30
Aria Moradi 3a825f4f25 fix manga thumbnail loading bug 2021-05-12 10:39:21 +04:30
Aria Moradi b9ea8c5f74 code cleanup 2021-05-12 10:20:01 +04:30
Aria Moradi 320d7e2536 reformat 2021-05-11 18:55:09 +04:30
Aria Moradi c200785479 handle front, handle orphans 2021-05-11 18:45:53 +04:30
Aria Moradi 8abb132ad6 chapter new parameters get endpoint 2021-05-11 15:50:18 +04:30
Aria Moradi 8bb2269f36 [SKIP CI] fix typo 2021-05-08 20:33:53 +04:30
Aria Moradi 9d17b26283 [SKIP CI] up for debate 2021-05-07 19:41:34 +04:30
Aria Moradi 5909f15db7 [SKIP CI] code of conduct 2021-05-07 19:39:24 +04:30
Aria Moradi 11672ca576 [SKIP CI] 2021-05-07 19:35:11 +04:30
Aria Moradi e09773def3 [SKIP CI] potential contributors 2021-05-07 19:32:32 +04:30
Aria Moradi f6d4432e6f [SKIP CI] typo 2021-05-07 19:13:48 +04:30
Aria Moradi 45a6abc5c2 [SKIP CI] improve structure 2021-05-07 19:13:04 +04:30
Aria Moradi dc5e677a38 [SKIP CI] simplify 2021-05-07 19:05:11 +04:30
Aria Moradi a82549dc17 [SKIP CI] why a web app? 2021-05-07 19:02:56 +04:30
Aria Moradi a002e19d9d [SKIP CI] move info to CONTRIBUTING.md 2021-05-07 18:58:09 +04:30
Aria Moradi cdf1f98d28 [SKIP CI] move troubleshooting to wiki 2021-05-07 18:51:47 +04:30
Aria Moradi 0ff1ebdeb7 [SKIP CI] not alternative 2021-05-07 18:36:32 +04:30
Aria Moradi 17f4a396f8 [SKIP CI] one must know friend from foe! 2021-05-07 18:29:01 +04:30
Aria Moradi 8aa3cf4368 [SKIP CI] move stuff around 2021-05-07 18:27:10 +04:30
Aria Moradi 0136c5e493 [SKIP CI] alternative ui projects 2021-05-07 18:21:32 +04:30
Aria Moradi 8b94b9ee80 [SKIP CI] add contributing.md 2021-05-07 18:17:55 +04:30
Aria Moradi bed63f19f2 [SKIP CI] move technical info to contributing.md 2021-05-07 18:04:51 +04:30
Aria Moradi e2a6545a84 [SKIP CI] 2021-05-07 15:28:30 +04:30
Aria Moradi e3d3ec6895 fix sed output 2021-05-07 14:41:19 +04:30
Aria Moradi 7ba476bd79 include v 2021-05-07 14:39:15 +04:30
Aria Moradi 2dd41ebd27 Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-07 14:25:11 +04:30
Aria Moradi 038df78171 new preview version format 2021-05-07 14:24:28 +04:30
Aria Moradi 6e5ff2b508 [SKIP CI] it be "chrome OS" 2021-05-07 13:45:31 +04:30
Aria Moradi ec8d1e8680 [SKIP CI] more housekeeping! 2021-05-07 13:42:02 +04:30
Aria Moradi 1f0f0c33b7 [SKIP CI] housekeeping 2021-05-07 13:32:42 +04:30
Aria Moradi 825940fcac [SKIP CI] add running instructions 2021-05-07 13:17:15 +04:30
Aria Moradi 4618834af2 [SKIP CI] Make it simpler 2021-05-07 11:24:40 +04:30
Aria Moradi 55d968df5e [SKIP CI] add some doc comments 2021-05-06 23:06:35 +04:30
Aria Moradi 63a078cf7d Add migrations (#76)
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build FatJar (push) Failing after 16s
2021-05-06 18:46:12 +04:30
Aria Moradi 5304917e53 fix multiple version warning 2021-05-06 14:14:04 +04:30
Aria Moradi 831b74d2ec bintray is dead 2021-05-06 13:07:40 +04:30
Aria Moradi 1bad9dcd69 [SKIP CI] 2021-05-05 15:54:05 +04:30
Aria Moradi dd43716851 [SKIP CI] more info 2021-05-05 15:26:52 +04:30
Aria Moradi f2e55e95a2 reaffirmation of what Tachidesk is for some Twitter users... 2021-05-05 15:22:46 +04:30
Aria Moradi 14658a0c4d be fancy about the manifest info 😎 2021-05-04 00:15:31 +04:30
Aria Moradi 4195e7056b fix windowsPackge not building the jar 2021-05-03 23:51:48 +04:30
Aria Moradi 1d29e8b248 fix webUI not working with gradle 7.0 2021-05-03 23:21:37 +04:30
Syer10 b718c718df Download directly to file instead of a dir (#70) 2021-05-03 23:00:18 +04:30
Aria Moradi a3601cf1b5 fix pull request build 2021-05-03 22:57:54 +04:30
Aria Moradi 0236a9639b rename vals, comments 2021-05-03 22:30:43 +04:30
Syer10 5f4c7454ee Update everything (#68)
* Update everything, cleanup build.gradle.kts's

* Make requested changes
2021-05-03 22:19:09 +04:30
Aria Moradi 773120c96a make the app build 2021-05-03 20:50:09 +04:30
Aria Moradi 4b273c6bf9 add a bit of docs 2021-05-03 20:48:29 +04:30
Aria Moradi b626aa66ba Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-03 20:41:46 +04:30
Syer10 1dd029559e Stop Javalin properly on shutdown (#69) 2021-05-03 20:40:02 +04:30
Aria Moradi 59cbe5d5bc [SKIP CI] Yes, we do use git 2021-04-30 06:15:56 +04:30
Aria Moradi 40d1173653 put DBManager where it should be 2021-04-30 06:10:41 +04:30
Aria Moradi bf6a0aba5d Rework the version endpoint 2021-04-30 06:07:29 +04:30
Kolby Moroz Liebl 34d8feacdd Add version api endpoint (#66) 2021-04-30 05:47:23 +04:30
Kolby Moroz Liebl 1ea821584c [SKIP CI] Remove IE Requirement in ps1 script (#65) 2021-04-29 01:52:24 +04:30
Aria Moradi 3d2fee19bb refactor 2021-04-28 10:25:01 +04:30
Forgenn 449d12779a Truncate manga description if it's too long (#63)
* Manga description changed from 4096 to 8192

* Check that the description of a manga is not longer than 4096, trim otherwise

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