Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af1c34fba5 | |||
| 7b7d93786f | |||
| 7c1c504482 | |||
| 33b22fcab6 | |||
| ab0566dcba | |||
| c4f2cc7189 | |||
| 4626d99590 | |||
| 6465ca8a19 | |||
| 15b9d151df | |||
| dd1b6c86cd | |||
| 9613cda79a | |||
| 648b8e5960 | |||
| ce545b1fd5 | |||
| 9151034fbc | |||
| 312a8baa13 | |||
| 18b6168cd1 | |||
| 9a282c3bf4 | |||
| 2bbebe4c30 | |||
| 162961b560 | |||
| f1cc37d0db | |||
| 5a9d216fb7 | |||
| bf37d3be7c | |||
| 7fd57aaed8 | |||
| d996c44b24 | |||
| 6f3052dd1b | |||
| d2b1bfdcdd | |||
| 945fb99594 | |||
| 09d624a4e2 | |||
| eb90db7ce6 | |||
| b56f9391b8 | |||
| c181478909 | |||
| 76b31e734c | |||
| ed8bd76d95 | |||
| 3051a72d7f | |||
| 3a33bf3a5d | |||
| 7959ba2664 | |||
| fe6568b82c | |||
| c228648bb6 | |||
| fdaeb6d1fa | |||
| ba45e18399 | |||
| 3e2bf877d4 | |||
| c80d344046 | |||
| 2364f10d8d | |||
| 2602275c20 | |||
| d113311f4e | |||
| 8d95701e8e | |||
| 0d2c54a5ed | |||
| 6506c84b85 | |||
| 69bb38b487 | |||
| 95e17f2b50 | |||
| 9625da9221 | |||
| c1659f1cf2 | |||
| c46ee764ac | |||
| 7aada85f76 | |||
| 145cbe3e4f | |||
| cb8dd8259d | |||
| b8e721fd27 | |||
| 7917b5384c | |||
| 087b7554bf | |||
| fb5f851a2a | |||
| 7ac51f8c2a | |||
| e5e40a986c | |||
| 7a27436868 | |||
| a5bab7425d | |||
| 93d5ab3739 | |||
| 3146fefb55 | |||
| 1ea51bb9df | |||
| 98bd664ab6 | |||
| 61aee2e784 | |||
| 22bf49078f | |||
| 7284e0d4ae | |||
| d39d075b1a | |||
| 0f6749b0c1 | |||
| 771030b911 | |||
| 8d5744a2cf | |||
| a58aab9004 | |||
| 61bd32f7f0 | |||
| 63a444bd81 | |||
| 8f28c3b74b | |||
| d766206343 | |||
| 172f83f5b3 | |||
| 9e308025c3 | |||
| aaa6a16778 | |||
| 2a21da2210 | |||
| d1cd2cfc8c | |||
| 832c224ed4 | |||
| 99316f4bd5 | |||
| 9caae5f1e5 |
@@ -1,10 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cp ../master/repo/* .
|
git lfs install
|
||||||
new_build=$(ls | tail -1)
|
#git lfs track "*.zip"
|
||||||
echo "New build file name: $new_build"
|
|
||||||
|
|
||||||
cp -f $new_build Tachidesk-latest.jar
|
cp ../master/repo/* .
|
||||||
|
new_jar_build=$(ls *.jar| tail -1)
|
||||||
|
echo "last jar build file name: $new_jar_build"
|
||||||
|
|
||||||
|
new_win32_build=$(ls *.zip| tail -1)
|
||||||
|
echo "last win32 build file name: $new_win32_build"
|
||||||
|
|
||||||
|
cp -f $new_jar_build Tachidesk-latest.jar
|
||||||
|
cp -f $new_win32_build Tachidesk-latest-win32.zip
|
||||||
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Get last commit message
|
# Get last commit message
|
||||||
last_commit_log=$(git log -1 --pretty=format:"%s")
|
#last_commit_log=$(git log -1 --pretty=format:"%s")
|
||||||
echo "last commit log: $last_commit_log"
|
#echo "last commit log: $last_commit_log"
|
||||||
|
#
|
||||||
|
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
|
||||||
|
#echo "count is: $filter_count"
|
||||||
|
|
||||||
filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
|
mkdir -p repo/
|
||||||
echo "count is: $filter_count"
|
cp server/build/Tachidesk-*.jar repo/
|
||||||
|
cp server/build/Tachidesk-*.zip repo/
|
||||||
|
|
||||||
if [ "$filter_count" -gt 0 ]; then
|
ls repo
|
||||||
mkdir -p repo/
|
pwd
|
||||||
cp server/build/Tachidesk-*.jar repo/
|
|
||||||
fi
|
#if [ "$filter_count" -gt 0 ]; then
|
||||||
|
# cp server/build/Tachidesk-*.jar repo/
|
||||||
|
# cp server/build/Tachidesk-*.zip repo/
|
||||||
|
#fi
|
||||||
+11
-24
@@ -48,37 +48,24 @@ jobs:
|
|||||||
mkdir -p ~/.gradle
|
mkdir -p ~/.gradle
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- name: Download and process android.jar
|
- name: Download android.jar
|
||||||
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
|
|
||||||
run: |
|
run: |
|
||||||
cd master
|
cd master
|
||||||
./scripts/getAndroid.sh
|
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||||
|
|
||||||
- name: Build the Jar
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/react/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/react/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Build Jar and launch4j
|
||||||
uses: eskatos/gradle-command-action@v1
|
uses: eskatos/gradle-command-action@v1
|
||||||
with:
|
with:
|
||||||
build-root-directory: master
|
build-root-directory: master
|
||||||
wrapper-directory: master
|
wrapper-directory: master
|
||||||
arguments: :server:shadowJar --stacktrace
|
arguments: :server:windowsPackage --stacktrace
|
||||||
wrapper-cache-enabled: true
|
wrapper-cache-enabled: true
|
||||||
dependencies-cache-enabled: true
|
dependencies-cache-enabled: true
|
||||||
configuration-cache-enabled: true
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
- name: Create repo artifacts
|
|
||||||
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
|
|
||||||
run: |
|
|
||||||
cd master
|
|
||||||
./.github/scripts/create-repo.sh
|
|
||||||
|
|
||||||
- name: Checkout repo branch
|
|
||||||
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: repo
|
|
||||||
path: repo
|
|
||||||
|
|
||||||
- name: Deploy repo
|
|
||||||
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
|
|
||||||
run: |
|
|
||||||
cd repo
|
|
||||||
../master/.github/scripts/commit-repo.sh
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_wrapper:
|
||||||
|
name: Validate Gradle Wrapper
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build FatJar
|
||||||
|
needs: check_wrapper
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.5.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Checkout master branch
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
path: master
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 1.8
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 1.8
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Download android.jar
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
curl https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar -o AndroidCompat/lib/android.jar
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/react/node_modules
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
|
||||||
|
- name: Build Jar and launch4j
|
||||||
|
uses: eskatos/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
build-root-directory: master
|
||||||
|
wrapper-directory: master
|
||||||
|
arguments: :server:windowsPackage --stacktrace
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
|
||||||
|
- name: Create repo artifacts
|
||||||
|
run: |
|
||||||
|
cd master
|
||||||
|
./.github/scripts/create-repo.sh
|
||||||
|
|
||||||
|
- name: Upload Release
|
||||||
|
uses: xresloader/upload-to-github-release@master
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
file: "master/repo/*"
|
||||||
|
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 }}
|
||||||
@@ -5,28 +5,64 @@ Tachidesk is as multi-platform as you can get. Any platform that runs java and/o
|
|||||||
|
|
||||||
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
Ability to read and write Tachiyomi compatible backups and syncing is a planned feature.
|
||||||
|
|
||||||
## How do I run the thing?
|
## Is this application usable? Should I test it?
|
||||||
|
Here is a list of current features:
|
||||||
|
|
||||||
|
- Installing and executing Tachiyomi's Extensions, So you'll get the same sources.
|
||||||
|
- A library to save your mangas and categories to put them into.
|
||||||
|
- Searching and browsing installed sources.
|
||||||
|
- A minimal chapter reader.
|
||||||
|
- Ability to download Mangas for offline read(This partially works)
|
||||||
|
|
||||||
|
**Note:** Keep in mind that Tachidesk is alpha software and can break rarely and/or with each update, so you may have to delete your data to fix it. See [General troubleshooting](#general-troubleshooting) and [Support and help](#support-and-help) if it happens.
|
||||||
|
|
||||||
|
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented in more detail.
|
||||||
|
|
||||||
|
## Downloading and Running the app
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
You should have java 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 (if you're not planning to use the Windows specific build) and a modern browser installed. Also an internet connection is required as almost everything this app does is downloading stuff.
|
||||||
|
|
||||||
|
#### Download the app
|
||||||
|
Download the latest jar or windows(win32) release from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
||||||
|
|
||||||
#### Running pre-built jar packages
|
#### Running pre-built jar packages
|
||||||
Download the latest (or a working more stable) release from [the repo branch](https://github.com/AriaMoradi/Tachidesk/tree/repo) or obtain it from [the releases section](https://github.com/AriaMoradi/Tachidesk/releases).
|
Double click on the jar file or run `java -jar Tachidesk-vX.Y.Z-rxxx.jar` from a Terminal/Command Prompt window to run the app which will open a new browser window automatically. Also the System Tray Icon is your friend if you need to open the browser window again or close Tachidesk.
|
||||||
|
|
||||||
Double click on the jar file or run `java -jar Tachidesk-latest.jar` or `java -jar Tachidesk-vX.Y.Z-rxxx.jar`
|
#### Running pre-built Windows packages
|
||||||
|
Windows specific builds have java bundled inside them, so you don't have to install java to use it. Unzip `Tachidesk-vX.Y.Z-rxxx-win32.zip` and run `server.exe`, the rest will work like the jar release.
|
||||||
The server will be running on `http://localhost:4567` open this url in your browser.
|
|
||||||
|
|
||||||
#### Running on Docker
|
#### Running on Docker
|
||||||
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
|
||||||
|
|
||||||
|
## General troubleshooting
|
||||||
|
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
|
||||||
|
|
||||||
|
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
|
||||||
|
|
||||||
|
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
|
||||||
|
|
||||||
|
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
|
||||||
|
|
||||||
|
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
|
||||||
|
|
||||||
|
## Support and help
|
||||||
|
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
This project has two components:
|
||||||
|
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||||
|
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
### Get Android stubs jar
|
### Get Android stubs jar
|
||||||
#### Manual download
|
#### Manual download
|
||||||
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
Download [android.jar](https://raw.githubusercontent.com/AriaMoradi/Tachidesk/android-jar/android.jar) and put it under `AndroidCompat/lib`.
|
||||||
#### Building from source(needs `bash`, `curl`, `base64`, `zip` to work)
|
#### Automated download(needs `bash`, `curl`, `base64`, `zip` to work)
|
||||||
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
Run `scripts/getAndroid.sh` from project's root directory to download and rebuild the jar file from Google's repository.
|
||||||
### building the jar
|
### building the jar
|
||||||
Run `./gradlew shadowJar` the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
Run `./gradlew shadowJar`, the resulting built jar file will be `server/build/Tachidesk-vX.Y.Z-rxxx.jar`.
|
||||||
|
### building the Windows package
|
||||||
|
Run `./gradlew windowsPackage`, the resulting built zip package file will be `server/build/Tachidesk-vX.Y.Z-rxxx-win32.zip`.
|
||||||
## Running for development purposes
|
## Running for development purposes
|
||||||
### `server` module
|
### `server` module
|
||||||
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
|
||||||
@@ -37,26 +73,14 @@ How to do it is described in `webUI/react/README.md` but for short,
|
|||||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||||
and supports HMR and all the other goodies you'll need.
|
and supports HMR and all the other goodies you'll need.
|
||||||
|
|
||||||
## Is this application usable? Should I test it?
|
|
||||||
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
|
|
||||||
|
|
||||||
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
|
|
||||||
|
|
||||||
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
|
|
||||||
|
|
||||||
## How does it work?
|
|
||||||
This project has two components:
|
|
||||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
|
||||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
|
||||||
|
|
||||||
## Credit
|
## Credit
|
||||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
Parts of [tachiyomi](https://github.com/tachiyomiorg/tachiyomi) is adopted into this codebase, also licensed under `Apache License Version 2.0`.
|
||||||
|
|
||||||
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
You can obtain a copy of `Apache License Version 2.0` from http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
You can obtain a copy of the license from http://www.apache.org/licenses/LICENSE-2.0
|
Changes to both codebases is licensed under `MPL v. 2.0` as the rest of this project.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+69
-7
@@ -6,9 +6,10 @@ plugins {
|
|||||||
application
|
application
|
||||||
id("com.github.johnrengelman.shadow") version "6.1.0"
|
id("com.github.johnrengelman.shadow") version "6.1.0"
|
||||||
id("org.jmailen.kotlinter") version "3.3.0"
|
id("org.jmailen.kotlinter") version "3.3.0"
|
||||||
|
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.1.2"
|
val TachideskVersion = "v0.2.3"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -76,10 +77,14 @@ dependencies {
|
|||||||
|
|
||||||
// Exposed ORM
|
// Exposed ORM
|
||||||
val exposed_version = "0.28.1"
|
val exposed_version = "0.28.1"
|
||||||
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
|
||||||
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
|
||||||
implementation ("org.xerial:sqlite-jdbc:3.30.1")
|
implementation("com.h2database:h2:1.4.199")
|
||||||
|
|
||||||
|
// tray icon
|
||||||
|
implementation("com.dorkbox:SystemTray:3.17")
|
||||||
|
|
||||||
|
|
||||||
// AndroidCompat
|
// AndroidCompat
|
||||||
implementation(project(":AndroidCompat"))
|
implementation(project(":AndroidCompat"))
|
||||||
@@ -90,8 +95,8 @@ dependencies {
|
|||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val name = "ir.armor.tachidesk.Main"
|
||||||
application {
|
application {
|
||||||
val name = "ir.armor.tachidesk.Main"
|
|
||||||
mainClass.set(name)
|
mainClass.set(name)
|
||||||
|
|
||||||
// Required by ShadowJar.
|
// Required by ShadowJar.
|
||||||
@@ -115,7 +120,7 @@ val TachideskRevision = Runtime
|
|||||||
it.bufferedReader().use(BufferedReader::readText)
|
it.bufferedReader().use(BufferedReader::readText)
|
||||||
}
|
}
|
||||||
process.destroy()
|
process.destroy()
|
||||||
"r"+output.trim()
|
"r" + output.trim()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +143,63 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
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> {
|
tasks.withType<ShadowJar> {
|
||||||
destinationDir = File("$rootDir/server/build")
|
destinationDir = File("$rootDir/server/build")
|
||||||
dependsOn("lintKotlin")
|
dependsOn("lintKotlin")
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.CookieJar
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class MemoryCookieJar : CookieJar {
|
||||||
|
private val cache = mutableSetOf<WrappedCookie>()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
|
val cookiesToRemove = mutableSetOf<WrappedCookie>()
|
||||||
|
val validCookies = mutableSetOf<WrappedCookie>()
|
||||||
|
|
||||||
|
cache.forEach { cookie ->
|
||||||
|
if (cookie.isExpired()) {
|
||||||
|
cookiesToRemove.add(cookie)
|
||||||
|
} else if (cookie.matches(url)) {
|
||||||
|
validCookies.add(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.removeAll(cookiesToRemove)
|
||||||
|
|
||||||
|
return validCookies.toList().map(WrappedCookie::unwrap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
|
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
|
||||||
|
|
||||||
|
cache.removeAll(cookiesToAdd)
|
||||||
|
cache.addAll(cookiesToAdd)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun clear() {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WrappedCookie private constructor(val cookie: Cookie) {
|
||||||
|
fun unwrap() = cookie
|
||||||
|
|
||||||
|
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||||
|
|
||||||
|
fun matches(url: HttpUrl) = cookie.matches(url)
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is WrappedCookie) return false
|
||||||
|
|
||||||
|
return other.cookie.name == cookie.name &&
|
||||||
|
other.cookie.domain == cookie.domain &&
|
||||||
|
other.cookie.path == cookie.path &&
|
||||||
|
other.cookie.secure == cookie.secure &&
|
||||||
|
other.cookie.hostOnly == cookie.hostOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var hash = 17
|
||||||
|
hash = 31 * hash + cookie.name.hashCode()
|
||||||
|
hash = 31 * hash + cookie.domain.hashCode()
|
||||||
|
hash = 31 * hash + cookie.path.hashCode()
|
||||||
|
hash = 31 * hash + if (cookie.secure) 0 else 1
|
||||||
|
hash = 31 * hash + if (cookie.hostOnly) 0 else 1
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun wrap(cookie: Cookie) = WrappedCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,14 +19,17 @@ class NetworkHelper(context: Context) {
|
|||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
// val cookieManager = AndroidCookieJar()
|
val cookieManager = MemoryCookieJar()
|
||||||
|
|
||||||
val client by lazy {
|
val client by lazy {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
// .cookieJar(cookieManager)
|
.cookieJar(cookieManager)
|
||||||
// .cache(Cache(cacheDir, cacheSize))
|
// .cache(Cache(cacheDir, cacheSize))
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(5, TimeUnit.MINUTES)
|
||||||
|
.writeTimeout(5, TimeUnit.MINUTES)
|
||||||
|
// .dispatcher(Dispatcher(Executors.newFixedThreadPool(1)))
|
||||||
|
|
||||||
// .addInterceptor(UserAgentInterceptor())
|
// .addInterceptor(UserAgentInterceptor())
|
||||||
|
|
||||||
// if (BuildConfig.DEBUG) {
|
// if (BuildConfig.DEBUG) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
call.cancel()
|
// call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean {
|
override fun isUnsubscribed(): Boolean {
|
||||||
@@ -80,17 +80,18 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable().doOnNext { response ->
|
return asObservable()
|
||||||
if (!response.isSuccessful) {
|
.doOnNext { response ->
|
||||||
response.close()
|
if (!response.isSuccessful) {
|
||||||
throw Exception("HTTP error ${response.code}")
|
response.close()
|
||||||
|
throw Exception("HTTP error ${response.code}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
// fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
// val progressClient = newBuilder()
|
// val progressClient = newBuilder()
|
||||||
// .cache(null)
|
// .cache(nasObservableSuccessull)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
@@ -104,7 +105,7 @@ fun Call.asObservableSuccess(): Observable<Response> {
|
|||||||
|
|
||||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
// .cache(null)
|
||||||
// .addNetworkInterceptor { chain ->
|
// .addNetworkInterceptor { chain ->
|
||||||
// val originalResponse = chain.proceed(chain.request())
|
// val originalResponse = chain.proceed(chain.request())
|
||||||
// originalResponse.newBuilder()
|
// originalResponse.newBuilder()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
*/
|
*/
|
||||||
protected val network: NetworkHelper by injectLazy()
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Preferences that a source may need.
|
// * Preferences that a source may need.
|
||||||
@@ -311,7 +311,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
protected open fun imageRequest(page: Page): Request {
|
open fun imageRequest(page: Page): Request {
|
||||||
return GET(page.imageUrl!!, headers)
|
return GET(page.imageUrl!!, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ import net.harawata.appdirs.AppDirsFactory
|
|||||||
object Config {
|
object Config {
|
||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
|
val mangaRoot = "$dataRoot/manga"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,36 @@ package ir.armor.tachidesk
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
|
import ir.armor.tachidesk.util.addMangaToCategory
|
||||||
|
import ir.armor.tachidesk.util.addMangaToLibrary
|
||||||
import ir.armor.tachidesk.util.applicationSetup
|
import ir.armor.tachidesk.util.applicationSetup
|
||||||
|
import ir.armor.tachidesk.util.createCategory
|
||||||
|
import ir.armor.tachidesk.util.getCategoryList
|
||||||
|
import ir.armor.tachidesk.util.getCategoryMangaList
|
||||||
|
import ir.armor.tachidesk.util.getChapter
|
||||||
import ir.armor.tachidesk.util.getChapterList
|
import ir.armor.tachidesk.util.getChapterList
|
||||||
|
import ir.armor.tachidesk.util.getExtensionIcon
|
||||||
import ir.armor.tachidesk.util.getExtensionList
|
import ir.armor.tachidesk.util.getExtensionList
|
||||||
|
import ir.armor.tachidesk.util.getLibraryMangas
|
||||||
import ir.armor.tachidesk.util.getManga
|
import ir.armor.tachidesk.util.getManga
|
||||||
|
import ir.armor.tachidesk.util.getMangaCategories
|
||||||
import ir.armor.tachidesk.util.getMangaList
|
import ir.armor.tachidesk.util.getMangaList
|
||||||
import ir.armor.tachidesk.util.getPages
|
import ir.armor.tachidesk.util.getPageImage
|
||||||
import ir.armor.tachidesk.util.getSource
|
import ir.armor.tachidesk.util.getSource
|
||||||
import ir.armor.tachidesk.util.getSourceList
|
import ir.armor.tachidesk.util.getSourceList
|
||||||
|
import ir.armor.tachidesk.util.getThumbnail
|
||||||
import ir.armor.tachidesk.util.installAPK
|
import ir.armor.tachidesk.util.installAPK
|
||||||
|
import ir.armor.tachidesk.util.openInBrowser
|
||||||
|
import ir.armor.tachidesk.util.removeCategory
|
||||||
|
import ir.armor.tachidesk.util.removeExtension
|
||||||
|
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||||
|
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
||||||
|
import ir.armor.tachidesk.util.reorderCategory
|
||||||
import ir.armor.tachidesk.util.sourceFilters
|
import ir.armor.tachidesk.util.sourceFilters
|
||||||
import ir.armor.tachidesk.util.sourceGlobalSearch
|
import ir.armor.tachidesk.util.sourceGlobalSearch
|
||||||
import ir.armor.tachidesk.util.sourceSearch
|
import ir.armor.tachidesk.util.sourceSearch
|
||||||
|
import ir.armor.tachidesk.util.systemTray
|
||||||
|
import ir.armor.tachidesk.util.updateCategory
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
import org.kodein.di.conf.global
|
import org.kodein.di.conf.global
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
@@ -44,6 +62,7 @@ class Main {
|
|||||||
|
|
||||||
// make sure everything we need exists
|
// make sure everything we need exists
|
||||||
applicationSetup()
|
applicationSetup()
|
||||||
|
val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
|
||||||
|
|
||||||
registerConfigModules()
|
registerConfigModules()
|
||||||
|
|
||||||
@@ -54,32 +73,57 @@ class Main {
|
|||||||
// start app
|
// start app
|
||||||
androidCompat.startApp(App())
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
var hasWebUiBundled: Boolean = false
|
||||||
|
|
||||||
val app = Javalin.create { config ->
|
val app = Javalin.create { config ->
|
||||||
try {
|
try {
|
||||||
this::class.java.classLoader.getResource("/react/index.html")
|
this::class.java.classLoader.getResource("/react/index.html")
|
||||||
|
hasWebUiBundled = true
|
||||||
config.addStaticFiles("/react")
|
config.addStaticFiles("/react")
|
||||||
config.addSinglePageRoot("/", "/react/index.html")
|
config.addSinglePageRoot("/", "/react/index.html")
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
println("Warning: react build files are missing.")
|
println("Warning: react build files are missing.")
|
||||||
|
hasWebUiBundled = false
|
||||||
}
|
}
|
||||||
|
config.enableCorsForAllOrigins()
|
||||||
}.start(4567)
|
}.start(4567)
|
||||||
|
if (hasWebUiBundled) {
|
||||||
app.before() { ctx ->
|
openInBrowser()
|
||||||
// allow the client which is running on another port
|
|
||||||
ctx.header("Access-Control-Allow-Origin", "*")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// app.before() { ctx ->
|
||||||
|
// // allow the client which is running on another port
|
||||||
|
// ctx.header("Access-Control-Allow-Origin", "*")
|
||||||
|
// }
|
||||||
|
|
||||||
app.get("/api/v1/extension/list") { ctx ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(getExtensionList())
|
ctx.json(getExtensionList())
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
app.get("/api/v1/extension/install/:apkName") { ctx ->
|
||||||
val apkName = ctx.pathParam("apkName")
|
val apkName = ctx.pathParam("apkName")
|
||||||
println(apkName)
|
println("installing $apkName")
|
||||||
|
|
||||||
ctx.status(
|
ctx.status(
|
||||||
installAPK(apkName)
|
installAPK(apkName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/uninstall/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
println("uninstalling $apkName")
|
||||||
|
removeExtension(apkName)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
val result = getExtensionIcon(apkName)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/source/list") { ctx ->
|
app.get("/api/v1/source/list") { ctx ->
|
||||||
ctx.json(getSourceList())
|
ctx.json(getSourceList())
|
||||||
}
|
}
|
||||||
@@ -105,6 +149,50 @@ class Main {
|
|||||||
ctx.json(getManga(mangaId))
|
ctx.json(getManga(mangaId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get("api/v1/manga/:mangaId/thumbnail") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val result = getThumbnail(mangaId)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds the manga to library
|
||||||
|
app.get("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
addMangaToLibrary(mangaId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the library
|
||||||
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
removeMangaFromLibrary(mangaId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds the manga to category
|
||||||
|
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
ctx.json(getMangaCategories(mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
// adds the manga to category
|
||||||
|
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
addMangaToCategory(mangaId, categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removes the manga from the category
|
||||||
|
app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
removeMangaFromCategory(mangaId, categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getChapterList(mangaId))
|
ctx.json(getChapterList(mangaId))
|
||||||
@@ -113,7 +201,17 @@ class Main {
|
|||||||
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId") { ctx ->
|
||||||
val chapterId = ctx.pathParam("chapterId").toInt()
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
ctx.json(getPages(chapterId, mangaId))
|
ctx.json(getChapter(chapterId, mangaId))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/manga/:mangaId/chapter/:chapterId/page/:index") { ctx ->
|
||||||
|
val chapterId = ctx.pathParam("chapterId").toInt()
|
||||||
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
|
val index = ctx.pathParam("index").toInt()
|
||||||
|
val result = getPageImage(mangaId, chapterId, index)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// global search
|
// global search
|
||||||
@@ -135,6 +233,54 @@ class Main {
|
|||||||
val sourceId = ctx.pathParam("sourceId").toLong()
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
ctx.json(sourceFilters(sourceId))
|
ctx.json(sourceFilters(sourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lists mangas that have no category assigned
|
||||||
|
app.get("/api/v1/library/") { ctx ->
|
||||||
|
ctx.json(getLibraryMangas())
|
||||||
|
}
|
||||||
|
|
||||||
|
// category list
|
||||||
|
app.get("/api/v1/category/") { ctx ->
|
||||||
|
ctx.json(getCategoryList())
|
||||||
|
}
|
||||||
|
|
||||||
|
// category create
|
||||||
|
app.post("/api/v1/category/") { ctx ->
|
||||||
|
val name = ctx.formParam("name")!!
|
||||||
|
createCategory(name)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category modification
|
||||||
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
||||||
|
val name = ctx.formParam("name")
|
||||||
|
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||||
|
updateCategory(categoryId, name, isLanding)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category re-ordering
|
||||||
|
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
val from = ctx.formParam("from")!!.toInt()
|
||||||
|
val to = ctx.formParam("to")!!.toInt()
|
||||||
|
reorderCategory(categoryId, from, to)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// category delete
|
||||||
|
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
removeCategory(categoryId)
|
||||||
|
ctx.status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the manga list associated with a category
|
||||||
|
app.get("/api/v1/category/:categoryId") { ctx ->
|
||||||
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
ctx.json(getCategoryMangaList(categoryId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ package ir.armor.tachidesk.database
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.Config
|
import ir.armor.tachidesk.Config
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
@@ -15,18 +18,25 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
Database.connect("jdbc:sqlite:${Config.dataRoot}/database.db", "org.sqlite.JDBC")
|
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeDataBaseTables() {
|
fun makeDataBaseTables() {
|
||||||
// mention db object to connect
|
// mention db object to connect
|
||||||
DBMangaer.db
|
DBMangaer.db
|
||||||
|
// val db = DBMangaer.db
|
||||||
|
// db.useNestedTransactions = true
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(ExtensionsTable)
|
SchemaUtils.createMissingTablesAndColumns(
|
||||||
SchemaUtils.create(SourceTable)
|
ExtensionTable,
|
||||||
SchemaUtils.create(MangaTable)
|
SourceTable,
|
||||||
SchemaUtils.create(ChapterTable)
|
MangaTable,
|
||||||
|
ChapterTable,
|
||||||
|
PageTable,
|
||||||
|
CategoryTable,
|
||||||
|
CategoryMangaTable,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-9
@@ -1,13 +1,12 @@
|
|||||||
|
package ir.armor.tachidesk.database.dataclass
|
||||||
|
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React from 'react';
|
data class CategoryDataClass(
|
||||||
|
val id: Int,
|
||||||
export default function Home() {
|
val order: Int,
|
||||||
return (
|
val name: String,
|
||||||
<h1>
|
val isLanding: Boolean
|
||||||
Home
|
)
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,4 +12,5 @@ data class ChapterDataClass(
|
|||||||
val chapter_number: Float,
|
val chapter_number: Float,
|
||||||
val scanlator: String?,
|
val scanlator: String?,
|
||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
|
val pageCount: Int? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ data class MangaDataClass(
|
|||||||
val author: String? = null,
|
val author: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val genre: String? = null,
|
val genre: String? = null,
|
||||||
val status: String = MangaStatus.UNKNOWN.name
|
val status: String = MangaStatus.UNKNOWN.name,
|
||||||
|
val inLibrary: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PagedMangaListDataClass(
|
data class PagedMangaListDataClass(
|
||||||
|
|||||||
@@ -4,22 +4,22 @@ package ir.armor.tachidesk.database.entity
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import org.jetbrains.exposed.dao.IntEntity
|
import org.jetbrains.exposed.dao.IntEntity
|
||||||
import org.jetbrains.exposed.dao.IntEntityClass
|
import org.jetbrains.exposed.dao.IntEntityClass
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
|
|
||||||
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
|
||||||
companion object : IntEntityClass<ExtensionEntity>(ExtensionsTable)
|
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
|
||||||
|
|
||||||
var name by ExtensionsTable.name
|
var name by ExtensionTable.name
|
||||||
var pkgName by ExtensionsTable.pkgName
|
var pkgName by ExtensionTable.pkgName
|
||||||
var versionName by ExtensionsTable.versionName
|
var versionName by ExtensionTable.versionName
|
||||||
var versionCode by ExtensionsTable.versionCode
|
var versionCode by ExtensionTable.versionCode
|
||||||
var lang by ExtensionsTable.lang
|
var lang by ExtensionTable.lang
|
||||||
var isNsfw by ExtensionsTable.isNsfw
|
var isNsfw by ExtensionTable.isNsfw
|
||||||
var apkName by ExtensionsTable.apkName
|
var apkName by ExtensionTable.apkName
|
||||||
var iconUrl by ExtensionsTable.iconUrl
|
var iconUrl by ExtensionTable.iconUrl
|
||||||
var installed by ExtensionsTable.installed
|
var installed by ExtensionTable.installed
|
||||||
var classFQName by ExtensionsTable.classFQName
|
var classFQName by ExtensionTable.classFQName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
object CategoryMangaTable : IntIdTable() {
|
||||||
|
val category = reference("category", CategoryTable)
|
||||||
|
val manga = reference("manga", MangaTable)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
|
object CategoryTable : IntIdTable() {
|
||||||
|
val name = varchar("name", 64)
|
||||||
|
val isLanding = bool("is_landing").default(false)
|
||||||
|
val order = integer("order").default(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
|
categoryEntry[CategoryTable.id].value,
|
||||||
|
categoryEntry[CategoryTable.order],
|
||||||
|
categoryEntry[CategoryTable.name],
|
||||||
|
categoryEntry[CategoryTable.isLanding],
|
||||||
|
)
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* 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
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* 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
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object ExtensionsTable : IntIdTable() {
|
object ExtensionTable : IntIdTable() {
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val pkgName = varchar("pkg_name", 128)
|
val pkgName = varchar("pkg_name", 128)
|
||||||
val versionName = varchar("version_name", 16)
|
val versionName = varchar("version_name", 16)
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* 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 eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
|
import ir.armor.tachidesk.util.proxyThumbnailUrl
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
|
|
||||||
object MangaTable : IntIdTable() {
|
object MangaTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
val url = varchar("url", 2048)
|
||||||
@@ -17,10 +24,32 @@ object MangaTable : IntIdTable() {
|
|||||||
val status = integer("status").default(SManga.UNKNOWN)
|
val status = integer("status").default(SManga.UNKNOWN)
|
||||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
|
val inLibrary = bool("in_library").default(false)
|
||||||
|
val defaultCategory = bool("default_category").default(true)
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
// source is used by some ancestor of IntIdTable
|
||||||
val sourceReference = reference("source", SourceTable)
|
val sourceReference = reference("source", SourceTable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
|
MangaDataClass(
|
||||||
|
mangaEntry[MangaTable.id].value,
|
||||||
|
mangaEntry[sourceReference].value,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.url],
|
||||||
|
mangaEntry[MangaTable.title],
|
||||||
|
proxyThumbnailUrl(mangaEntry[MangaTable.id].value),
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.initialized],
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.artist],
|
||||||
|
mangaEntry[MangaTable.author],
|
||||||
|
mangaEntry[MangaTable.description],
|
||||||
|
mangaEntry[MangaTable.genre],
|
||||||
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
|
mangaEntry[MangaTable.inLibrary]
|
||||||
|
)
|
||||||
|
|
||||||
enum class MangaStatus(val status: Int) {
|
enum class MangaStatus(val status: Int) {
|
||||||
UNKNOWN(0),
|
UNKNOWN(0),
|
||||||
ONGOING(1),
|
ONGOING(1),
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* 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 PageTable : IntIdTable() {
|
||||||
|
val index = integer("index")
|
||||||
|
val url = varchar("url", 2048)
|
||||||
|
val imageUrl = varchar("imageUrl", 2048).nullable()
|
||||||
|
|
||||||
|
val chapter = reference("chapter", ChapterTable)
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* 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.IdTable
|
import org.jetbrains.exposed.dao.id.IdTable
|
||||||
|
|
||||||
object SourceTable : IdTable<Long>() {
|
object SourceTable : IdTable<Long>() {
|
||||||
override val id = long("id").entityId()
|
override val id = long("id").entityId()
|
||||||
val name = varchar("name", 128)
|
val name = varchar("name", 128)
|
||||||
val lang = varchar("lang", 10)
|
val lang = varchar("lang", 10)
|
||||||
val extension = reference("extension", ExtensionsTable)
|
val extension = reference("extension", ExtensionTable)
|
||||||
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
val partOfFactorySource = bool("part_of_factory_source").default(false)
|
||||||
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
val positionInFactorySource = integer("position_in_factory_source").nullable()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
|
import ir.armor.tachidesk.database.table.toDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
fun createCategory(name: String) {
|
||||||
|
transaction {
|
||||||
|
val count = CategoryTable.selectAll().count()
|
||||||
|
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||||
|
CategoryTable.insert {
|
||||||
|
it[CategoryTable.name] = name
|
||||||
|
it[CategoryTable.order] = count.toInt() + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||||
|
transaction {
|
||||||
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
|
if (name != null) it[CategoryTable.name] = name
|
||||||
|
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||||
|
transaction {
|
||||||
|
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||||
|
categories.add(to - 1, categories.removeAt(from - 1))
|
||||||
|
categories.forEachIndexed { index, cat ->
|
||||||
|
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||||
|
it[CategoryTable.order] = index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCategory(categoryId: Int) {
|
||||||
|
transaction {
|
||||||
|
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategoryList(): List<CategoryDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
|
CategoryTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.toDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||||
|
transaction {
|
||||||
|
if (CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.firstOrNull() == null) {
|
||||||
|
CategoryMangaTable.insert {
|
||||||
|
it[CategoryMangaTable.category] = categoryId
|
||||||
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
it[MangaTable.defaultCategory] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||||
|
transaction {
|
||||||
|
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||||
|
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
it[MangaTable.defaultCategory] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryMangaTable.innerJoin(MangaTable).select { CategoryMangaTable.category eq categoryId }.map {
|
||||||
|
MangaTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
|
||||||
|
return transaction {
|
||||||
|
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
|
CategoryTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@ package ir.armor.tachidesk.util
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
import ir.armor.tachidesk.database.dataclass.ChapterDataClass
|
||||||
import ir.armor.tachidesk.database.dataclass.PageDataClass
|
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -57,14 +57,14 @@ fun getChapterList(mangaId: Int): List<ChapterDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageDataClass>> {
|
fun getChapter(chapterId: Int, mangaId: Int): ChapterDataClass {
|
||||||
return transaction {
|
return transaction {
|
||||||
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
val chapterEntry = ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!!
|
||||||
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
assert(mangaId == chapterEntry[ChapterTable.manga].value) // sanity check
|
||||||
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
val mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
|
||||||
val pagesList = source.fetchPageList(
|
val pageList = source.fetchPageList(
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
url = chapterEntry[ChapterTable.url]
|
url = chapterEntry[ChapterTable.url]
|
||||||
name = chapterEntry[ChapterTable.name]
|
name = chapterEntry[ChapterTable.name]
|
||||||
@@ -78,22 +78,24 @@ fun getPages(chapterId: Int, mangaId: Int): Pair<ChapterDataClass, List<PageData
|
|||||||
chapterEntry[ChapterTable.date_upload],
|
chapterEntry[ChapterTable.date_upload],
|
||||||
chapterEntry[ChapterTable.chapter_number],
|
chapterEntry[ChapterTable.chapter_number],
|
||||||
chapterEntry[ChapterTable.scanlator],
|
chapterEntry[ChapterTable.scanlator],
|
||||||
mangaId
|
mangaId,
|
||||||
|
pageList.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
val pages = pagesList.map {
|
pageList.forEach { page ->
|
||||||
PageDataClass(
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
|
||||||
it.index,
|
if (pageEntry == null) {
|
||||||
getTrueImageUrl(it, source)
|
transaction {
|
||||||
)
|
PageTable.insert {
|
||||||
|
it[index] = page.index
|
||||||
|
it[url] = page.url
|
||||||
|
it[imageUrl] = page.imageUrl
|
||||||
|
it[this.chapter] = chapterId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@transaction Pair(chapter, pages)
|
return@transaction chapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
|
||||||
return if (page.imageUrl == null) {
|
|
||||||
source.fetchImageUrl(page).toBlocking().first()!!
|
|
||||||
} else page.imageUrl!!
|
|
||||||
}
|
|
||||||
|
|||||||
+45
-5
@@ -6,23 +6,26 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.APKExtractor
|
import ir.armor.tachidesk.APKExtractor
|
||||||
import ir.armor.tachidesk.Config
|
import ir.armor.tachidesk.Config
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
|
||||||
@@ -32,8 +35,8 @@ fun installAPK(apkName: String): Int {
|
|||||||
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
||||||
|
|
||||||
// check if we don't have the dex file already downloaded
|
// check if we don't have the dex file already downloaded
|
||||||
val dexPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
if (!File(dexPath).exists()) {
|
if (!File(jarPath).exists()) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val api = ExtensionGithubApi()
|
val api = ExtensionGithubApi()
|
||||||
val apkToDownload = api.getApkUrl(extensionRecord)
|
val apkToDownload = api.getApkUrl(extensionRecord)
|
||||||
@@ -60,7 +63,7 @@ fun installAPK(apkName: String): Int {
|
|||||||
val instance = classToLoad.newInstance()
|
val instance = classToLoad.newInstance()
|
||||||
|
|
||||||
val extensionId = transaction {
|
val extensionId = transaction {
|
||||||
return@transaction ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
|
return@transaction ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance is HttpSource) { // single source
|
if (instance is HttpSource) { // single source
|
||||||
@@ -107,7 +110,7 @@ fun installAPK(apkName: String): Int {
|
|||||||
|
|
||||||
// update extension info
|
// update extension info
|
||||||
transaction {
|
transaction {
|
||||||
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
|
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||||
it[installed] = true
|
it[installed] = true
|
||||||
it[classFQName] = className
|
it[classFQName] = className
|
||||||
}
|
}
|
||||||
@@ -130,3 +133,40 @@ private fun downloadAPKFile(url: String, apkPath: String) {
|
|||||||
sink.writeAll(response.body!!.source())
|
sink.writeAll(response.body!!.source())
|
||||||
sink.close()
|
sink.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeExtension(pkgName: String) {
|
||||||
|
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
|
||||||
|
val fileNameWithoutType = pkgName.substringBefore(".apk")
|
||||||
|
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
|
transaction {
|
||||||
|
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||||
|
|
||||||
|
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
|
||||||
|
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
|
||||||
|
it[ExtensionTable.installed] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File(jarPath).exists()) {
|
||||||
|
File(jarPath).delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
|
val saveDir = "${Config.extensionsRoot}/icon"
|
||||||
|
val fileName = apkName
|
||||||
|
|
||||||
|
return getCachedResponse(saveDir, fileName) {
|
||||||
|
network.client.newCall(
|
||||||
|
GET(iconUrl)
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
|
return "http://127.0.0.1:4567/api/v1/extension/icon/$apkName"
|
||||||
|
}
|
||||||
@@ -7,9 +7,8 @@ package ir.armor.tachidesk.util
|
|||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
import ir.armor.tachidesk.database.dataclass.ExtensionDataClass
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
||||||
import org.jetbrains.exposed.sql.insert
|
import org.jetbrains.exposed.sql.insert
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
@@ -22,7 +21,7 @@ private object Data {
|
|||||||
|
|
||||||
private fun extensionDatabaseIsEmtpy(): Boolean {
|
private fun extensionDatabaseIsEmtpy(): Boolean {
|
||||||
return transaction {
|
return transaction {
|
||||||
return@transaction ExtensionsTable.selectAll().count() == 0L
|
return@transaction ExtensionTable.selectAll().count() == 0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +36,10 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
foundExtensions = api.findExtensions()
|
foundExtensions = api.findExtensions()
|
||||||
transaction {
|
transaction {
|
||||||
foundExtensions.forEach { foundExtension ->
|
foundExtensions.forEach { foundExtension ->
|
||||||
val extensionRecord = ExtensionsTable.select { ExtensionsTable.name eq foundExtension.name }.firstOrNull()
|
val extensionRecord = ExtensionTable.select { ExtensionTable.name eq foundExtension.name }.firstOrNull()
|
||||||
if (extensionRecord != null) {
|
if (extensionRecord != null) {
|
||||||
// update the record
|
// update the record
|
||||||
ExtensionsTable.update({ ExtensionsTable.name eq foundExtension.name }) {
|
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
|
||||||
it[name] = foundExtension.name
|
it[name] = foundExtension.name
|
||||||
it[pkgName] = foundExtension.pkgName
|
it[pkgName] = foundExtension.pkgName
|
||||||
it[versionName] = foundExtension.versionName
|
it[versionName] = foundExtension.versionName
|
||||||
@@ -52,7 +51,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// insert new record
|
// insert new record
|
||||||
ExtensionsTable.insert {
|
ExtensionTable.insert {
|
||||||
it[name] = foundExtension.name
|
it[name] = foundExtension.name
|
||||||
it[pkgName] = foundExtension.pkgName
|
it[pkgName] = foundExtension.pkgName
|
||||||
it[versionName] = foundExtension.versionName
|
it[versionName] = foundExtension.versionName
|
||||||
@@ -66,21 +65,23 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println("used cached extension list")
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction {
|
return transaction {
|
||||||
return@transaction ExtensionsTable.selectAll().map {
|
return@transaction ExtensionTable.selectAll().map {
|
||||||
ExtensionDataClass(
|
ExtensionDataClass(
|
||||||
it[ExtensionsTable.name],
|
it[ExtensionTable.name],
|
||||||
it[ExtensionsTable.pkgName],
|
it[ExtensionTable.pkgName],
|
||||||
it[ExtensionsTable.versionName],
|
it[ExtensionTable.versionName],
|
||||||
it[ExtensionsTable.versionCode],
|
it[ExtensionTable.versionCode],
|
||||||
it[ExtensionsTable.lang],
|
it[ExtensionTable.lang],
|
||||||
it[ExtensionsTable.isNsfw],
|
it[ExtensionTable.isNsfw],
|
||||||
it[ExtensionsTable.apkName],
|
it[ExtensionTable.apkName],
|
||||||
it[ExtensionsTable.iconUrl],
|
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||||
it[ExtensionsTable.installed],
|
it[ExtensionTable.installed],
|
||||||
it[ExtensionsTable.classFQName]
|
it[ExtensionTable.classFQName]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.BufferedSource
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
// fun writeStream(fileStream: InputStream, path: String) {
|
||||||
|
// Files.newOutputStream(Paths.get(path)).use { os ->
|
||||||
|
// val buffer = ByteArray(128 * 1024)
|
||||||
|
// var len: Int
|
||||||
|
// while (fileStream.read(buffer).also { len = it } > 0) {
|
||||||
|
// os.write(buffer, 0, len)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun pathToInputStream(path: String): InputStream {
|
||||||
|
return BufferedInputStream(FileInputStream(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
||||||
|
File(directoryPath).listFiles().forEach { file ->
|
||||||
|
if (file.name.startsWith(fileName))
|
||||||
|
return "$directoryPath/${file.name}"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the given source to an output stream and closes both resources.
|
||||||
|
*
|
||||||
|
* @param stream the stream where the source is copied.
|
||||||
|
*/
|
||||||
|
private fun BufferedSource.saveTo(stream: OutputStream) {
|
||||||
|
use { input ->
|
||||||
|
stream.sink().buffer().use {
|
||||||
|
it.writeAll(input)
|
||||||
|
it.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
val filePath = "$saveDir/$fileName"
|
||||||
|
if (cachedFile != null) {
|
||||||
|
val fileType = cachedFile.substringAfter(filePath)
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(cachedFile),
|
||||||
|
"image/$fileType"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = fetcher()
|
||||||
|
|
||||||
|
if (response.code == 200) {
|
||||||
|
val contentType = response.headers["content-type"]!!
|
||||||
|
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||||
|
|
||||||
|
Files.newOutputStream(Paths.get(fullPath)).use { os ->
|
||||||
|
response.body!!.source().saveTo(os)
|
||||||
|
}
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(fullPath),
|
||||||
|
contentType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw Exception("request error! ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.toDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.deleteWhere
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
fun addMangaToLibrary(mangaId: Int) {
|
||||||
|
val manga = getManga(mangaId)
|
||||||
|
if (!manga.inLibrary) {
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
|
it[inLibrary] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMangaFromLibrary(mangaId: Int) {
|
||||||
|
val manga = getManga(mangaId)
|
||||||
|
if (manga.inLibrary) {
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
|
it[inLibrary] = false
|
||||||
|
it[defaultCategory] = true
|
||||||
|
}
|
||||||
|
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLibraryMangas(): List<MangaDataClass> {
|
||||||
|
return transaction {
|
||||||
|
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||||
|
MangaTable.toDataClass(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,77 +4,100 @@ package ir.armor.tachidesk.util
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import ir.armor.tachidesk.Config
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
import ir.armor.tachidesk.database.table.MangaTable
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
fun getManga(mangaId: Int): MangaDataClass {
|
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
||||||
return transaction {
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
|
||||||
|
|
||||||
return@transaction if (mangaEntry[MangaTable.initialized]) {
|
return if (mangaEntry[MangaTable.initialized]) {
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
mangaId,
|
mangaId,
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
mangaEntry[MangaTable.sourceReference].value,
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
mangaEntry[MangaTable.url],
|
||||||
mangaEntry[MangaTable.title],
|
mangaEntry[MangaTable.title],
|
||||||
mangaEntry[MangaTable.thumbnail_url],
|
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url],
|
||||||
|
|
||||||
true,
|
true,
|
||||||
|
|
||||||
mangaEntry[MangaTable.artist],
|
mangaEntry[MangaTable.artist],
|
||||||
mangaEntry[MangaTable.author],
|
mangaEntry[MangaTable.author],
|
||||||
mangaEntry[MangaTable.description],
|
mangaEntry[MangaTable.description],
|
||||||
mangaEntry[MangaTable.genre],
|
mangaEntry[MangaTable.genre],
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
)
|
mangaEntry[MangaTable.inLibrary]
|
||||||
} else { // initialize manga
|
)
|
||||||
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
} else { // initialize manga
|
||||||
val fetchedManga = source.fetchMangaDetails(
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
SManga.create().apply {
|
val fetchedManga = source.fetchMangaDetails(
|
||||||
url = mangaEntry[MangaTable.url]
|
SManga.create().apply {
|
||||||
title = mangaEntry[MangaTable.title]
|
url = mangaEntry[MangaTable.url]
|
||||||
}
|
title = mangaEntry[MangaTable.title]
|
||||||
).toBlocking().first()
|
|
||||||
|
|
||||||
// update database
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
// it[url] = fetchedManga.url
|
|
||||||
// it[title] = fetchedManga.title
|
|
||||||
it[initialized] = true
|
|
||||||
|
|
||||||
it[artist] = fetchedManga.artist
|
|
||||||
it[author] = fetchedManga.author
|
|
||||||
it[description] = fetchedManga.description
|
|
||||||
it[genre] = fetchedManga.genre
|
|
||||||
it[status] = fetchedManga.status
|
|
||||||
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
|
||||||
it[thumbnail_url] = fetchedManga.thumbnail_url
|
|
||||||
}
|
}
|
||||||
|
).toBlocking().first()
|
||||||
|
|
||||||
mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!!
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
|
||||||
MangaDataClass(
|
it[MangaTable.initialized] = true
|
||||||
mangaId,
|
|
||||||
mangaEntry[MangaTable.sourceReference].value,
|
|
||||||
|
|
||||||
mangaEntry[MangaTable.url],
|
it[MangaTable.artist] = fetchedManga.artist
|
||||||
mangaEntry[MangaTable.title],
|
it[MangaTable.author] = fetchedManga.author
|
||||||
mangaEntry[MangaTable.thumbnail_url],
|
it[MangaTable.description] = fetchedManga.description
|
||||||
|
it[MangaTable.genre] = fetchedManga.genre
|
||||||
true,
|
it[MangaTable.status] = fetchedManga.status
|
||||||
|
if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty())
|
||||||
mangaEntry[MangaTable.artist],
|
it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url
|
||||||
mangaEntry[MangaTable.author],
|
}
|
||||||
mangaEntry[MangaTable.description],
|
|
||||||
mangaEntry[MangaTable.genre],
|
|
||||||
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
|
||||||
|
|
||||||
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
mangaEntry[MangaTable.sourceReference].value,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.url],
|
||||||
|
mangaEntry[MangaTable.title],
|
||||||
|
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail,
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
fetchedManga.artist,
|
||||||
|
fetchedManga.author,
|
||||||
|
fetchedManga.description,
|
||||||
|
fetchedManga.genre,
|
||||||
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val saveDir = Config.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
|
return getCachedResponse(saveDir, fileName) {
|
||||||
|
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||||
|
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||||
|
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
||||||
|
}
|
||||||
|
|
||||||
|
source.client.newCall(
|
||||||
|
GET(thumbnailUrl, source.headers)
|
||||||
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class MangaDexHelper(private val mangaDexSource: HttpSource) {
|
||||||
|
|
||||||
|
private fun clientBuilder(): OkHttpClient = clientBuilder(0)
|
||||||
|
|
||||||
|
private fun clientBuilder(
|
||||||
|
r18Toggle: Int,
|
||||||
|
okHttpClient: OkHttpClient = mangaDexSource.network.client
|
||||||
|
): OkHttpClient = okHttpClient.newBuilder()
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val originalCookies = chain.request().header("Cookie") ?: ""
|
||||||
|
val newReq = chain
|
||||||
|
.request()
|
||||||
|
.newBuilder()
|
||||||
|
.header("Cookie", "$originalCookies; ${cookiesHeader(r18Toggle)}")
|
||||||
|
.build()
|
||||||
|
chain.proceed(newReq)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
private fun cookiesHeader(r18Toggle: Int): String {
|
||||||
|
val cookies = mutableMapOf<String, String>()
|
||||||
|
cookies["mangadex_h_toggle"] = r18Toggle.toString()
|
||||||
|
return buildCookies(cookies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildCookies(cookies: Map<String, String>) =
|
||||||
|
cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||||
|
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun isLogged(): Boolean {
|
||||||
|
// val httpUrl = mangaDexSource.baseUrl.toHttpUrlOrNull()!!
|
||||||
|
// return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||||
|
// }
|
||||||
|
|
||||||
|
fun login(username: String, password: String, twoFactorCode: String = ""): Boolean {
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("login_username", username)
|
||||||
|
.add("login_password", password)
|
||||||
|
.add("no_js", "1")
|
||||||
|
.add("remember_me", "1")
|
||||||
|
|
||||||
|
twoFactorCode.let {
|
||||||
|
formBody.add("two_factor", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = clientBuilder().newCall(
|
||||||
|
POST(
|
||||||
|
"${mangaDexSource.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||||
|
mangaDexSource.headers,
|
||||||
|
formBody.build()
|
||||||
|
)
|
||||||
|
).execute()
|
||||||
|
return response.body!!.string().isEmpty()
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// fun logout(): Boolean {
|
||||||
|
// return withContext(Dispatchers.IO) {
|
||||||
|
// // https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||||
|
// val httpUrl = baseUrl.toHttpUrlOrNull()!!
|
||||||
|
// val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||||
|
// val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||||
|
// val token = cookie?.value
|
||||||
|
// if (token.isNullOrEmpty()) {
|
||||||
|
// return@withContext true
|
||||||
|
// }
|
||||||
|
// val result = clientBuilder().newCall(
|
||||||
|
// POSTWithCookie(
|
||||||
|
// "$baseUrl/ajax/actions.ajax.php?function=logout",
|
||||||
|
// REMEMBER_ME,
|
||||||
|
// token,
|
||||||
|
// headers
|
||||||
|
// )
|
||||||
|
// ).execute()
|
||||||
|
// val resultStr = result.body!!.string()
|
||||||
|
// if (resultStr.contains("success", true)) {
|
||||||
|
// network.cookieManager.remove(httpUrl)
|
||||||
|
// return@withContext true
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ import org.jetbrains.exposed.sql.insertAndGetId
|
|||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
|
return "http://127.0.0.1:4567/api/v1/manga/$mangaId/thumbnail"
|
||||||
|
}
|
||||||
|
|
||||||
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
|
||||||
val source = getHttpSource(sourceId.toLong())
|
val source = getHttpSource(sourceId.toLong())
|
||||||
val mangasPage = if (popular) {
|
val mangasPage = if (popular) {
|
||||||
@@ -31,8 +35,8 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|||||||
val mangaList = transaction {
|
val mangaList = transaction {
|
||||||
return@transaction mangasPage.mangas.map { manga ->
|
return@transaction mangasPage.mangas.map { manga ->
|
||||||
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
|
||||||
var mangaEntityId = if (mangaEntry == null) { // create manga entry
|
if (mangaEntry == null) { // create manga entry
|
||||||
MangaTable.insertAndGetId {
|
val mangaId = MangaTable.insertAndGetId {
|
||||||
it[url] = manga.url
|
it[url] = manga.url
|
||||||
it[title] = manga.title
|
it[title] = manga.title
|
||||||
|
|
||||||
@@ -41,30 +45,47 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
|
|||||||
it[description] = manga.description
|
it[description] = manga.description
|
||||||
it[genre] = manga.genre
|
it[genre] = manga.genre
|
||||||
it[status] = manga.status
|
it[status] = manga.status
|
||||||
it[thumbnail_url] = manga.genre
|
it[thumbnail_url] = manga.thumbnail_url
|
||||||
|
|
||||||
it[sourceReference] = sourceId
|
it[sourceReference] = sourceId
|
||||||
}.value
|
}.value
|
||||||
|
|
||||||
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
sourceId,
|
||||||
|
|
||||||
|
manga.url,
|
||||||
|
manga.title,
|
||||||
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
|
manga.initialized,
|
||||||
|
|
||||||
|
manga.artist,
|
||||||
|
manga.author,
|
||||||
|
manga.description,
|
||||||
|
manga.genre,
|
||||||
|
MangaStatus.valueOf(manga.status).name
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
mangaEntry[MangaTable.id].value
|
val mangaId = mangaEntry[MangaTable.id].value
|
||||||
|
MangaDataClass(
|
||||||
|
mangaId,
|
||||||
|
sourceId,
|
||||||
|
|
||||||
|
manga.url,
|
||||||
|
manga.title,
|
||||||
|
proxyThumbnailUrl(mangaId),
|
||||||
|
|
||||||
|
true,
|
||||||
|
|
||||||
|
mangaEntry[MangaTable.artist],
|
||||||
|
mangaEntry[MangaTable.author],
|
||||||
|
mangaEntry[MangaTable.description],
|
||||||
|
mangaEntry[MangaTable.genre],
|
||||||
|
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
||||||
|
mangaEntry[MangaTable.inLibrary]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MangaDataClass(
|
|
||||||
mangaEntityId,
|
|
||||||
sourceId,
|
|
||||||
|
|
||||||
manga.url,
|
|
||||||
manga.title,
|
|
||||||
manga.thumbnail_url,
|
|
||||||
|
|
||||||
manga.initialized,
|
|
||||||
|
|
||||||
manga.artist,
|
|
||||||
manga.author,
|
|
||||||
manga.description,
|
|
||||||
manga.genre,
|
|
||||||
MangaStatus.valueOf(manga.status).name,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return PagedMangaListDataClass(
|
return PagedMangaListDataClass(
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import ir.armor.tachidesk.Config
|
||||||
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
|
import ir.armor.tachidesk.database.table.MangaTable
|
||||||
|
import ir.armor.tachidesk.database.table.PageTable
|
||||||
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
|
import org.jetbrains.exposed.sql.and
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||||
|
if (page.imageUrl == null) {
|
||||||
|
page.imageUrl = source.fetchImageUrl(page).toBlocking().first()!!
|
||||||
|
}
|
||||||
|
return page.imageUrl!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
|
||||||
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
|
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq index) }.firstOrNull()!! }
|
||||||
|
|
||||||
|
val tachiPage = Page(
|
||||||
|
pageEntry[PageTable.index],
|
||||||
|
pageEntry[PageTable.url],
|
||||||
|
pageEntry[PageTable.imageUrl]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pageEntry[PageTable.imageUrl] == null) {
|
||||||
|
transaction {
|
||||||
|
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq index) }) {
|
||||||
|
it[imageUrl] = getTrueImageUrl(tachiPage, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveDir = getChapterDir(mangaId, chapterId)
|
||||||
|
File(saveDir).mkdirs()
|
||||||
|
val fileName = index.toString()
|
||||||
|
|
||||||
|
return getCachedResponse(saveDir, fileName) {
|
||||||
|
source.fetchImage(tachiPage).toBlocking().first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||||
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
|
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||||
|
val source = getHttpSource(sourceId)
|
||||||
|
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
|
||||||
|
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
|
||||||
|
|
||||||
|
val chapterDir = when {
|
||||||
|
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
|
else -> chapterEntry[ChapterTable.name]
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaTitle = mangaEntry[MangaTable.title]
|
||||||
|
val sourceName = source.toString()
|
||||||
|
|
||||||
|
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||||
|
// make sure dirs exist
|
||||||
|
File(mangaDir).mkdirs()
|
||||||
|
return mangaDir
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import ir.armor.tachidesk.Config
|
|||||||
import ir.armor.tachidesk.database.dataclass.SourceDataClass
|
import ir.armor.tachidesk.database.dataclass.SourceDataClass
|
||||||
import ir.armor.tachidesk.database.entity.ExtensionEntity
|
import ir.armor.tachidesk.database.entity.ExtensionEntity
|
||||||
import ir.armor.tachidesk.database.entity.SourceEntity
|
import ir.armor.tachidesk.database.entity.SourceEntity
|
||||||
import ir.armor.tachidesk.database.table.ExtensionsTable
|
import ir.armor.tachidesk.database.table.ExtensionTable
|
||||||
import ir.armor.tachidesk.database.table.SourceTable
|
import ir.armor.tachidesk.database.table.SourceTable
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
@@ -78,7 +78,7 @@ fun getSourceList(): List<SourceDataClass> {
|
|||||||
it[SourceTable.id].value.toString(),
|
it[SourceTable.id].value.toString(),
|
||||||
it[SourceTable.name],
|
it[SourceTable.name],
|
||||||
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
|
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
|
||||||
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
|
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ fun getSource(sourceId: Long): SourceDataClass {
|
|||||||
source[SourceTable.id].value.toString(),
|
source[SourceTable.id].value.toString(),
|
||||||
source[SourceTable.name],
|
source[SourceTable.name],
|
||||||
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
|
Locale(source[SourceTable.lang]).getDisplayLanguage(Locale(source[SourceTable.lang])),
|
||||||
ExtensionsTable.select { ExtensionsTable.id eq source[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
|
ExtensionTable.select { ExtensionTable.id eq source[SourceTable.extension] }.first()[ExtensionTable.iconUrl],
|
||||||
getHttpSource(source[SourceTable.id].value).supportsLatest
|
getHttpSource(source[SourceTable.id].value).supportsLatest
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,77 @@ package ir.armor.tachidesk.util
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import dorkbox.systemTray.MenuItem
|
||||||
|
import dorkbox.systemTray.SystemTray
|
||||||
|
import dorkbox.systemTray.SystemTray.TrayType
|
||||||
|
import dorkbox.util.CacheUtil
|
||||||
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.Config
|
import ir.armor.tachidesk.Config
|
||||||
|
import ir.armor.tachidesk.Main
|
||||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||||
|
import java.awt.event.ActionListener
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
// make dirs we need
|
// make dirs we need
|
||||||
File(Config.dataRoot).mkdirs()
|
File(Config.dataRoot).mkdirs()
|
||||||
File(Config.extensionsRoot).mkdirs()
|
File(Config.extensionsRoot).mkdirs()
|
||||||
|
File("${Config.extensionsRoot}/icon").mkdirs()
|
||||||
|
File(Config.thumbnailsRoot).mkdirs()
|
||||||
|
|
||||||
makeDataBaseTables()
|
makeDataBaseTables()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openInBrowser() {
|
||||||
|
try {
|
||||||
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
|
} catch (e1: IOException) {
|
||||||
|
e1.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
|
fun systemTray(): SystemTray? {
|
||||||
|
try {
|
||||||
|
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
||||||
|
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
|
||||||
|
if (System.getProperty("os.name").startsWith("Windows"))
|
||||||
|
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
|
||||||
|
|
||||||
|
CacheUtil.clear()
|
||||||
|
|
||||||
|
val systemTray = SystemTray.get() ?: return null
|
||||||
|
val mainMenu = systemTray.menu
|
||||||
|
|
||||||
|
mainMenu.add(
|
||||||
|
MenuItem(
|
||||||
|
"Open Tachidesk",
|
||||||
|
ActionListener {
|
||||||
|
try {
|
||||||
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// systemTray.setTooltip("Tachidesk")
|
||||||
|
systemTray.setImage(icon)
|
||||||
|
// systemTray.status = "No Mail"
|
||||||
|
|
||||||
|
systemTray.getMenu().add(
|
||||||
|
MenuItem("Quit") {
|
||||||
|
systemTray.shutdown()
|
||||||
|
System.exit(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return systemTray
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"systemParams": "linux-x64-88",
|
|
||||||
"modulesFolders": [],
|
|
||||||
"flags": [],
|
|
||||||
"linkedModules": [],
|
|
||||||
"topLevelPatterns": [],
|
|
||||||
"lockfileEntries": {},
|
|
||||||
"files": [],
|
|
||||||
"artifacts": {}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.vscode
|
.vscode
|
||||||
|
.env
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"fontsource-roboto": "^4.0.0",
|
"fontsource-roboto": "^4.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.1",
|
"react-scripts": "4.0.1",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
@@ -2,14 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
<link rel="icon" href="%PUBLIC_URL%/faviconlogo.ico"/>
|
||||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
|
||||||
<meta name="theme-color" content="#000000"/>
|
<meta name="theme-color" content="#000000"/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
content="A manga reader that runs tachiyomi's extensions"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/faviconlogo.png"/>
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>React App</title>
|
<title>Tachidesk</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
+25
-11
@@ -4,22 +4,24 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router, Route, Switch,
|
BrowserRouter as Router, Redirect, Route, Switch,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { Container } from '@material-ui/core';
|
import { Container } from '@material-ui/core';
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import NavBar from './components/NavBar';
|
import NavBar from './components/NavBar';
|
||||||
import Home from './screens/Home';
|
|
||||||
import Sources from './screens/Sources';
|
import Sources from './screens/Sources';
|
||||||
import Extensions from './screens/Extensions';
|
import Extensions from './screens/Extensions';
|
||||||
import MangaList from './screens/MangaList';
|
import SourceMangas from './screens/SourceMangas';
|
||||||
import Manga from './screens/Manga';
|
import Manga from './screens/Manga';
|
||||||
import Reader from './screens/Reader';
|
import Reader from './screens/Reader';
|
||||||
import Search from './screens/SearchSingle';
|
import Search from './screens/SearchSingle';
|
||||||
import NavBarTitle from './context/NavbarTitle';
|
import NavBarTitle from './context/NavbarTitle';
|
||||||
import DarkTheme from './context/DarkTheme';
|
import DarkTheme from './context/DarkTheme';
|
||||||
|
import Library from './screens/Library';
|
||||||
|
import Settings from './screens/Settings';
|
||||||
|
import Categories from './screens/settings/Categories';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
@@ -53,13 +55,10 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
|
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NavBarTitle.Provider value={navTitleContext}>
|
<NavBarTitle.Provider value={navTitleContext}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<DarkTheme.Provider value={darkThemeContext}>
|
<NavBar />
|
||||||
<NavBar />
|
|
||||||
</DarkTheme.Provider>
|
|
||||||
<Container maxWidth={false} disableGutters>
|
<Container maxWidth={false} disableGutters>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/sources/:sourceId/search/">
|
<Route path="/sources/:sourceId/search/">
|
||||||
@@ -69,10 +68,10 @@ export default function App() {
|
|||||||
<Extensions />
|
<Extensions />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/sources/:sourceId/popular/">
|
<Route path="/sources/:sourceId/popular/">
|
||||||
<MangaList popular />
|
<SourceMangas popular />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/sources/:sourceId/latest/">
|
<Route path="/sources/:sourceId/latest/">
|
||||||
<MangaList popular={false} />
|
<SourceMangas popular={false} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/sources">
|
<Route path="/sources">
|
||||||
<Sources />
|
<Sources />
|
||||||
@@ -83,9 +82,24 @@ export default function App() {
|
|||||||
<Route path="/manga/:id">
|
<Route path="/manga/:id">
|
||||||
<Manga />
|
<Manga />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/library">
|
||||||
<Home />
|
<Library />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/settings/categories">
|
||||||
|
<Categories />
|
||||||
|
</Route>
|
||||||
|
<Route path="/settings">
|
||||||
|
<DarkTheme.Provider value={darkThemeContext}>
|
||||||
|
<Settings />
|
||||||
|
</DarkTheme.Provider>
|
||||||
|
</Route>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/"
|
||||||
|
render={() => (
|
||||||
|
<Redirect to="/library" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Container>
|
</Container>
|
||||||
</NavBarTitle.Provider>
|
</NavBarTitle.Provider>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import Checkbox from '@material-ui/core/Checkbox';
|
||||||
|
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||||
|
import FormGroup from '@material-ui/core/FormGroup';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => createStyles({
|
||||||
|
paper: {
|
||||||
|
maxHeight: 435,
|
||||||
|
width: '80%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (value: boolean) => void
|
||||||
|
mangaId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategoryInfo {
|
||||||
|
category: ICategory
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CategorySelect(props: IProps) {
|
||||||
|
const classes = useStyles();
|
||||||
|
const { open, setOpen, mangaId } = props;
|
||||||
|
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
|
||||||
|
|
||||||
|
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||||
|
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let tmpCategoryInfos: ICategoryInfo[] = [];
|
||||||
|
fetch('http://127.0.0.1:4567/api/v1/category/')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: ICategory[]) => {
|
||||||
|
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: ICategory[]) => {
|
||||||
|
data.forEach((category) => {
|
||||||
|
tmpCategoryInfos[category.order - 1].selected = true;
|
||||||
|
});
|
||||||
|
setCategoryInfos(tmpCategoryInfos);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [updateTriggerHolder, open]);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
|
||||||
|
const { checked } = event.target as HTMLInputElement;
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, {
|
||||||
|
method: checked ? 'GET' : 'DELETE', mode: 'cors',
|
||||||
|
})
|
||||||
|
.then(() => triggerUpdate());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
classes={classes}
|
||||||
|
maxWidth="xs"
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<DialogTitle>Set categories</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<FormGroup>
|
||||||
|
{categoryInfos.map((categoryInfo) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={(
|
||||||
|
<Checkbox
|
||||||
|
checked={categoryInfo.selected}
|
||||||
|
onChange={(e) => handleChange(e, categoryInfo.category.id)}
|
||||||
|
color="default"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
label={categoryInfo.category.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleOk} color="primary">
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,10 +43,10 @@ interface IProps {
|
|||||||
export default function ExtensionCard(props: IProps) {
|
export default function ExtensionCard(props: IProps) {
|
||||||
const {
|
const {
|
||||||
extension: {
|
extension: {
|
||||||
name, lang, versionName, iconUrl, installed, apkName,
|
name, lang, versionName, installed, apkName, iconUrl,
|
||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
const [installedState, setInstalledState] = useState<string>((installed ? 'installed' : 'install'));
|
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||||
@@ -54,10 +54,25 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
function install() {
|
function install() {
|
||||||
setInstalledState('installing');
|
setInstalledState('installing');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
|
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
|
||||||
setInstalledState('installed');
|
setInstalledState('uninstall');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uninstall() {
|
||||||
|
setInstalledState('uninstalling');
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => {
|
||||||
|
setInstalledState('install');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonClick() {
|
||||||
|
if (installedState === 'install') {
|
||||||
|
install();
|
||||||
|
} else {
|
||||||
|
uninstall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className={classes.root}>
|
<CardContent className={classes.root}>
|
||||||
@@ -80,7 +95,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="outlined" onClick={() => install()}>{installedState}</Button>
|
<Button variant="outlined" onClick={() => handleButtonClick()}>{installedState}</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,20 +2,70 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React from 'react';
|
import { Button, createStyles, makeStyles } from '@material-ui/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import CategorySelect from './CategorySelect';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => createStyles({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
'& button': {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
manga: IManga | undefined
|
manga: IManga
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MangaDetails(props: IProps) {
|
export default function MangaDetails(props: IProps) {
|
||||||
|
const classes = useStyles();
|
||||||
const { manga } = props;
|
const { manga } = props;
|
||||||
|
const [inLibrary, setInLibrary] = useState<string>(
|
||||||
|
manga.inLibrary ? 'In Library' : 'Not In Library',
|
||||||
|
);
|
||||||
|
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
function addToLibrary() {
|
||||||
|
setInLibrary('adding');
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||||
|
setInLibrary('In Library');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromLibrary() {
|
||||||
|
setInLibrary('removing');
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`, { method: 'DELETE', mode: 'cors' }).then(() => {
|
||||||
|
setInLibrary('Not In Library');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonClick() {
|
||||||
|
if (inLibrary === 'Not In Library') {
|
||||||
|
addToLibrary();
|
||||||
|
} else {
|
||||||
|
removeFromLibrary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
{manga && manga.title}
|
{manga && manga.title}
|
||||||
</h1>
|
</h1>
|
||||||
</>
|
<div className={classes.root}>
|
||||||
|
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
|
||||||
|
{inLibrary === 'In Library'
|
||||||
|
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<CategorySelect
|
||||||
|
open={categoryDialogOpen}
|
||||||
|
setOpen={setCategoryDialogOpen}
|
||||||
|
mangaId={manga.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
// TODO: remove above!
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
@@ -45,7 +47,7 @@ export default function NavBar() {
|
|||||||
const { title } = useContext(NavBarTitle);
|
const { title } = useContext(NavBarTitle);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
const { darkTheme } = useContext(DarkTheme);
|
||||||
|
|
||||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -72,15 +74,15 @@ export default function NavBar() {
|
|||||||
<Typography variant="h6" className={classes.title}>
|
<Typography variant="h6" className={classes.title}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton
|
{/* <IconButton
|
||||||
onClick={handleMenu}
|
onClick={handleMenu}
|
||||||
aria-label="display more actions"
|
aria-label="display more actions"
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<MoreIcon />
|
<MoreIcon />
|
||||||
</IconButton>
|
</IconButton> */}
|
||||||
<Menu
|
{/* <Menu
|
||||||
id="menu-appbar"
|
id="menu-appbar"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
@@ -107,7 +109,7 @@ export default function NavBar() {
|
|||||||
Light Theme
|
Light Theme
|
||||||
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu> */}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
onKeyDown={() => setDrawerOpen(false)}
|
onKeyDown={() => setDrawerOpen(false)}
|
||||||
>
|
>
|
||||||
<List>
|
<List>
|
||||||
|
<Link to="/library" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="Library">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Library" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
<Link to="/extensions" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="Extensions">
|
<ListItem button key="Extensions">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
@@ -52,6 +60,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
|||||||
<ListItemText primary="Sources" />
|
<ListItemText primary="Sources" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
|
<ListItem button key="settings">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Settings" />
|
||||||
|
</ListItem>
|
||||||
|
</Link>
|
||||||
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||||
<ListItem button key="Search">
|
<ListItem button key="Search">
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/* 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 { Tab, Tabs } from '@material-ui/core';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import MangaGrid from '../components/MangaGrid';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
|
interface IMangaCategory {
|
||||||
|
category: ICategory
|
||||||
|
mangas: IManga[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
index: any;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanel(props: TabPanelProps) {
|
||||||
|
const {
|
||||||
|
children, value, index,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`simple-tabpanel-${index}`}
|
||||||
|
>
|
||||||
|
{value === index && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Library() {
|
||||||
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
|
||||||
|
const [tabNum, setTabNum] = useState<number>(0);
|
||||||
|
|
||||||
|
// a hack so MangaGrid doesn't stop working. I won't change it in case
|
||||||
|
// if I do manga pagination for library..
|
||||||
|
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||||
|
useEffect(() => {
|
||||||
|
setTitle('Library');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
const fetchAndSetMangas = (tabs: IMangaCategory[], tab: IMangaCategory, index: number) => {
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/category/${tab.category.id}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: IManga[]) => {
|
||||||
|
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
||||||
|
tabsClone[index].mangas = data;
|
||||||
|
setTabs(tabsClone); // clone the object
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabChange = (newTab: number) => {
|
||||||
|
setTabNum(newTab);
|
||||||
|
tabs.forEach((tab, index) => {
|
||||||
|
if (tab.category.order === newTab && tab.mangas.length === 0) {
|
||||||
|
// mangas are empty, fetch the mangas
|
||||||
|
fetchAndSetMangas(tabs, tab, index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('http://127.0.0.1:4567/api/v1/library')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: IManga[]) => {
|
||||||
|
// if some manga with no category exist, they will be added under a virtual category
|
||||||
|
if (data.length > 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
category: {
|
||||||
|
name: 'Default', isLanding: true, order: 0, id: -1,
|
||||||
|
},
|
||||||
|
mangas: data,
|
||||||
|
},
|
||||||
|
]; // will set state on the next fetch
|
||||||
|
}
|
||||||
|
|
||||||
|
// no default category so the first tab is 1
|
||||||
|
setTabNum(1);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
(newTabs: IMangaCategory[]) => {
|
||||||
|
fetch('http://127.0.0.1:4567/api/v1/category')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: ICategory[]) => {
|
||||||
|
const mangaCategories = data.map((category) => ({
|
||||||
|
category,
|
||||||
|
mangas: [] as IManga[],
|
||||||
|
}));
|
||||||
|
const newNewTabs = [...newTabs, ...mangaCategories];
|
||||||
|
setTabs(newNewTabs);
|
||||||
|
|
||||||
|
// if no default category, we must fetch the first tab now...
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
if (newTabs.length === 0) { fetchAndSetMangas(newNewTabs, newNewTabs[0], 0); }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let toRender;
|
||||||
|
if (tabs.length > 1) {
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));
|
||||||
|
|
||||||
|
const tabBodies = tabs.map((tab) => (
|
||||||
|
<TabPanel value={tabNum} index={tab.category.order}>
|
||||||
|
<MangaGrid
|
||||||
|
mangas={tab.mangas}
|
||||||
|
hasNextPage={false}
|
||||||
|
lastPageNum={lastPageNum}
|
||||||
|
setLastPageNum={setLastPageNum}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
));
|
||||||
|
|
||||||
|
// 160px is min-width for viewport width of >600
|
||||||
|
const scrollableTabs = window.innerWidth < tabs.length * 160;
|
||||||
|
toRender = (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
value={tabNum}
|
||||||
|
onChange={(e, newTab) => handleTabChange(newTab)}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
centered={!scrollableTabs}
|
||||||
|
variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
|
||||||
|
scrollButtons="on"
|
||||||
|
>
|
||||||
|
{tabDefines}
|
||||||
|
</Tabs>
|
||||||
|
{tabBodies}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
|
||||||
|
toRender = (
|
||||||
|
<MangaGrid
|
||||||
|
mangas={mangas}
|
||||||
|
hasNextPage={false}
|
||||||
|
lastPageNum={lastPageNum}
|
||||||
|
setLastPageNum={setLastPageNum}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toRender;
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ export default function Manga() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MangaDetails manga={manga} />
|
{manga && <MangaDetails manga={manga} />}
|
||||||
{chapterCards}
|
{chapterCards}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,44 +14,36 @@ const style = {
|
|||||||
backgroundColor: '#343a40',
|
backgroundColor: '#343a40',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
interface IPage {
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
index: number
|
|
||||||
imageUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IData {
|
|
||||||
first: IChapter
|
|
||||||
second: IPage[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
|
||||||
const [pages, setPages] = useState<IPage[]>([]);
|
const [pageCount, setPageCount] = useState<number>(-1);
|
||||||
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
const { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data:IData) => {
|
.then((data:IChapter) => {
|
||||||
setTitle(data.first.name);
|
setTitle(data.name);
|
||||||
setPages(data.second);
|
setPageCount(data.pageCount);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
pages.sort((a, b) => (a.index - b.index));
|
if (pageCount === -1) {
|
||||||
|
return (
|
||||||
let mapped;
|
<div style={style}>
|
||||||
if (pages.length === 0) {
|
<h3>wait</h3>
|
||||||
mapped = <h3>wait</h3>;
|
|
||||||
} else {
|
|
||||||
mapped = pages.map(({ imageUrl }) => (
|
|
||||||
<div style={{ margin: '0 auto' }}>
|
|
||||||
<img src={imageUrl} alt="f" style={{ maxWidth: '100%' }} />
|
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapped = range(pageCount).map((index) => (
|
||||||
|
<div style={{ margin: '0 auto' }}>
|
||||||
|
<img src={`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="f" style={{ maxWidth: '100%' }} />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
{mapped}
|
{mapped}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import List from '@material-ui/core/List';
|
||||||
|
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
|
||||||
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
|
import InboxIcon from '@material-ui/icons/Inbox';
|
||||||
|
import Brightness6Icon from '@material-ui/icons/Brightness6';
|
||||||
|
import { ListItemSecondaryAction, Switch } from '@material-ui/core';
|
||||||
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
|
||||||
|
function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return <ListItem button component="a" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
setTitle('Settings');
|
||||||
|
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<List component="nav" style={{ padding: 0 }}>
|
||||||
|
<ListItemLink href="/settings/categories">
|
||||||
|
<ListItemIcon>
|
||||||
|
<InboxIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Categories" />
|
||||||
|
</ListItemLink>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Brightness6Icon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Dark Theme" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
edge="end"
|
||||||
|
checked={darkTheme}
|
||||||
|
onChange={() => setDarkTheme(!darkTheme)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavBarTitle from '../context/NavbarTitle';
|
||||||
|
|
||||||
export default function MangaList(props: { popular: boolean }) {
|
export default function SourceMangas(props: { popular: boolean }) {
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-shadow */
|
||||||
|
/* eslint-disable react/destructuring-assignment */
|
||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import React, { useState, useContext, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemIcon,
|
||||||
|
IconButton,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||||
|
import DragHandleIcon from '@material-ui/icons/DragHandle';
|
||||||
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
|
import { useTheme } from '@material-ui/core/styles';
|
||||||
|
import Fab from '@material-ui/core/Fab';
|
||||||
|
import AddIcon from '@material-ui/icons/Add';
|
||||||
|
import DeleteIcon from '@material-ui/icons/Delete';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import TextField from '@material-ui/core/TextField';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||||
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
|
import NavBarTitle from '../../context/NavbarTitle';
|
||||||
|
|
||||||
|
const getItemStyle = (isDragging, draggableStyle, palette) => ({
|
||||||
|
// styles we need to apply on draggables
|
||||||
|
...draggableStyle,
|
||||||
|
|
||||||
|
...(isDragging && {
|
||||||
|
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Categories() {
|
||||||
|
const { setTitle } = useContext(NavBarTitle);
|
||||||
|
setTitle('Categories');
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
|
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
||||||
|
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||||
|
const [dialogValue, setDialogValue] = useState('');
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||||
|
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogOpen) {
|
||||||
|
fetch('http://127.0.0.1:4567/api/v1/category/')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => setCategories(data));
|
||||||
|
}
|
||||||
|
}, [updateTriggerHolder]);
|
||||||
|
|
||||||
|
const categoryReorder = (list, from, to) => {
|
||||||
|
const category = list[from];
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('from', from + 1);
|
||||||
|
formData.append('to', to + 1);
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
mode: 'cors',
|
||||||
|
body: formData,
|
||||||
|
}).finally(() => triggerUpdate());
|
||||||
|
|
||||||
|
// also move it in local state to avoid jarring moving behviour...
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(from, 1);
|
||||||
|
result.splice(to, 0, removed);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (result) => {
|
||||||
|
// dropped outside the list?
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCategories(categoryReorder(
|
||||||
|
categories,
|
||||||
|
result.source.index,
|
||||||
|
result.destination.index,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogOpen = () => {
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDialog = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setDialogValue('');
|
||||||
|
setCategoryToEdit(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogCancel = () => {
|
||||||
|
resetDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = () => {
|
||||||
|
resetDialog();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', dialogValue);
|
||||||
|
|
||||||
|
if (categoryToEdit === -1) {
|
||||||
|
fetch('http://127.0.0.1:4567/api/v1/category/', {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
body: formData,
|
||||||
|
}).finally(() => triggerUpdate());
|
||||||
|
} else {
|
||||||
|
const category = categories[categoryToEdit];
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
mode: 'cors',
|
||||||
|
body: formData,
|
||||||
|
}).finally(() => triggerUpdate());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategory = (index) => {
|
||||||
|
const category = categories[index];
|
||||||
|
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
mode: 'cors',
|
||||||
|
}).finally(() => triggerUpdate());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{(provided) => (
|
||||||
|
<List ref={provided.innerRef}>
|
||||||
|
{categories.map((item, index) => (
|
||||||
|
<Draggable
|
||||||
|
key={item.id}
|
||||||
|
draggableId={item.id.toString()}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<ListItem
|
||||||
|
ContainerComponent="li"
|
||||||
|
ContainerProps={{ ref: provided.innerRef }}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={getItemStyle(
|
||||||
|
snapshot.isDragging,
|
||||||
|
provided.draggableProps.style,
|
||||||
|
theme.palette,
|
||||||
|
)}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DragHandleIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={item.name}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
setCategoryToEdit(index);
|
||||||
|
handleDialogOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
deleteCategory(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
<Fab
|
||||||
|
color="primary"
|
||||||
|
aria-label="add"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: theme.spacing(2),
|
||||||
|
right: theme.spacing(2),
|
||||||
|
}}
|
||||||
|
onClick={handleDialogOpen}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</Fab>
|
||||||
|
<Dialog open={dialogOpen} onClose={handleDialogCancel} aria-labelledby="form-dialog-title">
|
||||||
|
<DialogTitle id="form-dialog-title">
|
||||||
|
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
Enter new category name.
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="name"
|
||||||
|
label="Category Name"
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
value={dialogValue}
|
||||||
|
onChange={(e) => setDialogValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleDialogCancel} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleDialogSubmit} color="primary">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
Vendored
+10
@@ -9,6 +9,7 @@ interface IExtension {
|
|||||||
iconUrl: string
|
iconUrl: string
|
||||||
installed: boolean
|
installed: boolean
|
||||||
apkName: string
|
apkName: string
|
||||||
|
pkgName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISource {
|
interface ISource {
|
||||||
@@ -24,6 +25,7 @@ interface IManga {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
|
inLibrary?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IChapter {
|
interface IChapter {
|
||||||
@@ -34,4 +36,12 @@ interface IChapter {
|
|||||||
chapter_number: number
|
chapter_number: number
|
||||||
scanlator: String
|
scanlator: String
|
||||||
mangaId: number
|
mangaId: number
|
||||||
|
pageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICategory {
|
||||||
|
id: number
|
||||||
|
order: number
|
||||||
|
name: String
|
||||||
|
isLanding: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-3
@@ -3744,6 +3744,13 @@ css-blank-pseudo@^0.1.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.5"
|
postcss "^7.0.5"
|
||||||
|
|
||||||
|
css-box-model@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||||
|
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||||
|
dependencies:
|
||||||
|
tiny-invariant "^1.0.6"
|
||||||
|
|
||||||
css-color-names@0.0.4, css-color-names@^0.0.4:
|
css-color-names@0.0.4, css-color-names@^0.0.4:
|
||||||
version "0.0.4"
|
version "0.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
|
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
|
||||||
@@ -7344,6 +7351,11 @@ media-typer@0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
|
memoize-one@^5.1.1:
|
||||||
|
version "5.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||||
|
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||||
|
|
||||||
memory-fs@^0.4.1:
|
memory-fs@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||||
@@ -9208,6 +9220,11 @@ querystringify@^2.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
||||||
|
|
||||||
|
raf-schd@^4.0.2:
|
||||||
|
version "4.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||||
|
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
|
||||||
|
|
||||||
raf@^3.4.1:
|
raf@^3.4.1:
|
||||||
version "3.4.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||||
@@ -9257,6 +9274,19 @@ react-app-polyfill@^2.0.0:
|
|||||||
regenerator-runtime "^0.13.7"
|
regenerator-runtime "^0.13.7"
|
||||||
whatwg-fetch "^3.4.1"
|
whatwg-fetch "^3.4.1"
|
||||||
|
|
||||||
|
react-beautiful-dnd@^13.0.0:
|
||||||
|
version "13.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
|
||||||
|
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.8.4"
|
||||||
|
css-box-model "^1.2.0"
|
||||||
|
memoize-one "^5.1.1"
|
||||||
|
raf-schd "^4.0.2"
|
||||||
|
react-redux "^7.1.1"
|
||||||
|
redux "^4.0.4"
|
||||||
|
use-memo-one "^1.1.1"
|
||||||
|
|
||||||
react-dev-utils@^11.0.1:
|
react-dev-utils@^11.0.1:
|
||||||
version "11.0.1"
|
version "11.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
|
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
|
||||||
@@ -9301,7 +9331,7 @@ react-error-overlay@^6.0.8:
|
|||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
|
||||||
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
|
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
|
||||||
|
|
||||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
@@ -9311,6 +9341,17 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||||
|
|
||||||
|
react-redux@^7.1.1:
|
||||||
|
version "7.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
|
||||||
|
integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.12.1"
|
||||||
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^16.13.1"
|
||||||
|
|
||||||
react-refresh@^0.8.3:
|
react-refresh@^0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
@@ -9518,6 +9559,14 @@ redent@^3.0.0:
|
|||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
|
redux@^4.0.4:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||||
|
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
symbol-observable "^1.2.0"
|
||||||
|
|
||||||
regenerate-unicode-properties@^8.2.0:
|
regenerate-unicode-properties@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
|
||||||
@@ -10672,7 +10721,7 @@ svgo@^1.0.0, svgo@^1.2.2:
|
|||||||
unquote "~1.1.1"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
util.promisify "~1.0.0"
|
||||||
|
|
||||||
symbol-observable@1.2.0:
|
symbol-observable@1.2.0, symbol-observable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||||
@@ -10823,7 +10872,7 @@ timsort@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||||
@@ -11176,6 +11225,11 @@ url@^0.11.0:
|
|||||||
punycode "1.3.2"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
querystring "0.2.0"
|
||||||
|
|
||||||
|
use-memo-one@^1.1.1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||||
|
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user