Compare commits

..

75 Commits

Author SHA1 Message Date
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
Aria Moradi 648b8e5960 bump version: v0.2.2
Publish / Validate Gradle Wrapper (push) Successful in 15s
Publish / Build FatJar (push) Failing after 16s
2021-02-21 04:42:33 +03:30
Aria Moradi ce545b1fd5 fix some bugs 2021-02-21 04:41:56 +03:30
Aria Moradi 9151034fbc category done! 2021-02-21 04:27:41 +03:30
Aria Moradi 312a8baa13 hide menu button for now 2021-02-20 02:59:32 +03:30
Aria Moradi 18b6168cd1 theme select in settings 2021-02-20 02:57:52 +03:30
Aria Moradi 9a282c3bf4 redirect / to library 2021-02-20 02:41:30 +03:30
Aria Moradi 2bbebe4c30 fix removing manga from library not working 2021-02-20 02:34:26 +03:30
Aria Moradi 162961b560 fix tabs 2021-02-20 02:28:55 +03:30
Aria Moradi f1cc37d0db finished the category screen 2021-02-20 01:23:52 +03:30
Aria Moradi 5a9d216fb7 bump version
Publish / Validate Gradle Wrapper (push) Successful in 10s
Publish / Build FatJar (push) Failing after 2m8s
2021-02-14 23:18:14 +03:30
Aria Moradi bf37d3be7c fix syntax 2021-02-14 22:51:22 +03:30
Aria Moradi 7fd57aaed8 try new release action 2021-02-14 22:49:40 +03:30
Aria Moradi d996c44b24 try publish wiht draft 2021-02-14 22:20:50 +03:30
Aria Moradi 6f3052dd1b category backend 2021-02-14 01:10:43 +03:30
Aria Moradi d2b1bfdcdd Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-13 22:45:18 +03:30
Aria Moradi 945fb99594 Update README.md 2021-02-13 21:25:49 +03:30
Aria Moradi 09d624a4e2 add library 2021-02-13 21:12:18 +03:30
Aria Moradi eb90db7ce6 Update README.md 2021-02-13 17:18:31 +03:30
Aria Moradi b56f9391b8 Update README.md 2021-02-13 17:07:39 +03:30
Aria Moradi c181478909 Update README.md 2021-02-13 17:06:37 +03:30
Aria Moradi 76b31e734c Update README.md 2021-02-13 17:06:16 +03:30
Aria Moradi ed8bd76d95 dummy file to trigger actions 2021-02-13 15:34:17 +03:30
Aria Moradi 3051a72d7f add node_modules cache 2021-02-13 15:30:15 +03:30
Aria Moradi 3a33bf3a5d just download android.jar to improve build time 2021-02-13 15:18:57 +03:30
Aria Moradi 7959ba2664 [RELEASE CI] test new release 2021-02-13 14:50:46 +03:30
Aria Moradi fe6568b82c [RELEASE CI] test new release 2021-02-13 14:39:16 +03:30
Aria Moradi c228648bb6 [RELEASE CI] test new release 2021-02-13 14:15:38 +03:30
Aria Moradi fdaeb6d1fa [RELEASE CI] test new release 2021-02-13 14:01:01 +03:30
Aria Moradi ba45e18399 [RELEASE CI] test new release 2021-02-13 13:39:52 +03:30
Aria Moradi 3e2bf877d4 [RELEASE CI] test new release 2021-02-13 13:32:59 +03:30
Aria Moradi c80d344046 [RELEASE CI] test new release 2021-02-13 13:21:13 +03:30
Aria Moradi 2364f10d8d [RELEASE CI] test new release 2021-02-13 13:13:15 +03:30
Aria Moradi 2602275c20 [RELEASE CI] test new release 2021-02-13 13:12:40 +03:30
Aria Moradi d113311f4e [RELEASE CI] test new release 2021-02-13 12:57:01 +03:30
Aria Moradi 8d95701e8e [RELEASE CI] test new release 2021-02-13 12:55:57 +03:30
Aria Moradi 0d2c54a5ed [RELEASE CI] test new release 2021-02-13 12:54:36 +03:30
Aria Moradi 6506c84b85 publish? 2021-02-08 05:36:19 +03:30
Aria Moradi 69bb38b487 [CI RELEASE] do it 2021-02-08 05:12:13 +03:30
Aria Moradi 95e17f2b50 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-08 05:11:48 +03:30
Aria Moradi 9625da9221 [RLEASE CI] add upload release binaries action 2021-02-08 05:11:21 +03:30
Aria Moradi c1659f1cf2 refactor, add todos for library and category 2021-02-06 18:48:59 +03:30
Aria Moradi c46ee764ac Update README.md 2021-02-05 11:47:17 +03:30
Aria Moradi 7aada85f76 Update README.md 2021-02-05 11:46:29 +03:30
Aria Moradi 145cbe3e4f Update README.md 2021-02-05 11:45:56 +03:30
Aria Moradi cb8dd8259d Update README.md 2021-02-05 11:44:24 +03:30
Aria Moradi b8e721fd27 Update README.md 2021-02-05 01:48:59 +03:30
Aria Moradi 7917b5384c Update README.md 2021-02-05 01:17:03 +03:30
Aria Moradi 087b7554bf cleanup 2021-02-05 01:09:11 +03:30
Aria Moradi fb5f851a2a Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-05 00:57:16 +03:30
Aria Moradi 7ac51f8c2a Update README.md 2021-02-05 00:50:56 +03:30
Aria Moradi e5e40a986c Update README.md 2021-02-05 00:46:46 +03:30
Aria Moradi 7a27436868 now done with lfs track 2021-02-05 00:20:25 +03:30
Aria Moradi a5bab7425d [CI RELEASE] try lfs fix 2021-02-05 00:11:20 +03:30
Aria Moradi 93d5ab3739 [CI RELEASE] v.0.2.0 2021-02-04 23:55:01 +03:30
Aria Moradi 3146fefb55 change build scripts 2021-02-04 23:47:16 +03:30
Aria Moradi 1ea51bb9df add launch4j 2021-02-04 23:40:40 +03:30
Aria Moradi 98bd664ab6 Tray Icon 2021-02-04 18:02:46 +03:30
Aria Moradi 61aee2e784 hint added 2021-02-04 18:02:34 +03:30
Aria Moradi 22bf49078f cached response for source list iconUrl 2021-02-04 14:53:34 +03:30
Aria Moradi 7284e0d4ae cached extension icon 2021-02-04 14:47:27 +03:30
Aria Moradi d39d075b1a [CI RELEASE] dummy file to trigger CI 2021-02-04 04:55:36 +03:30
Aria Moradi 0f6749b0c1 now support backward writing! 2021-02-04 04:48:15 +03:30
Aria Moradi 771030b911 [CI RELEASE] v.0.1.5 2021-02-04 04:28:00 +03:30
Aria Moradi 8d5744a2cf fix chapter naming, db naming 2021-02-04 04:27:25 +03:30
50 changed files with 1577 additions and 265 deletions
+11 -4
View File
@@ -1,10 +1,17 @@
#!/bin/bash
cp ../master/repo/* .
new_build=$(ls | tail -1)
echo "New build file name: $new_build"
git lfs install
#git lfs track "*.zip"
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.name "github-actions[bot]"
+15 -8
View File
@@ -1,13 +1,20 @@
#!/bin/bash
# Get last commit message
last_commit_log=$(git log -1 --pretty=format:"%s")
echo "last commit log: $last_commit_log"
#last_commit_log=$(git log -1 --pretty=format:"%s")
#echo "last commit log: $last_commit_log"
#
#filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
#echo "count is: $filter_count"
filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
echo "count is: $filter_count"
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
cp server/build/Tachidesk-*.zip repo/
if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
fi
ls repo
pwd
#if [ "$filter_count" -gt 0 ]; then
# cp server/build/Tachidesk-*.jar repo/
# cp server/build/Tachidesk-*.zip repo/
#fi
+12 -25
View File
@@ -48,37 +48,24 @@ jobs:
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Download and process android.jar
if: github.event_name == 'push' && github.repository == 'AriaMoradi/Tachidesk'
- name: Download android.jar
run: |
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
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace
arguments: :server:windowsPackage --stacktrace
wrapper-cache-enabled: true
dependencies-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
configuration-cache-enabled: true
+115
View File
@@ -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 }}
+44 -23
View File
@@ -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.
## 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
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
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`
The server will be running on `http://localhost:4567` open this url in your browser.
#### 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.
#### Running on Docker
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
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
### Get Android stubs jar
#### Manual download
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.
### 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
### `server` module
Run `./gradlew :server:run -x :webUI:copyBuild --stacktrace` to run the server
@@ -37,21 +73,6 @@ 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
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.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## 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`.
+68 -7
View File
@@ -6,9 +6,10 @@ plugins {
application
id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
id("edu.sc.seis.launch4j") version "2.4.9"
}
val TachideskVersion = "v0.1.4"
val TachideskVersion = "v0.2.3"
repositories {
@@ -76,10 +77,13 @@ dependencies {
// Exposed ORM
val exposed_version = "0.28.1"
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("com.h2database:h2:1.4.199")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:1.4.199")
// tray icon
implementation("com.dorkbox:SystemTray:3.17")
// AndroidCompat
@@ -91,8 +95,8 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
val name = "ir.armor.tachidesk.Main"
application {
val name = "ir.armor.tachidesk.Main"
mainClass.set(name)
// Required by ShadowJar.
@@ -116,7 +120,7 @@ val TachideskRevision = Runtime
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r"+output.trim()
"r" + output.trim()
}
@@ -139,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> {
destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
@@ -6,21 +6,36 @@ package ir.armor.tachidesk
import eu.kanade.tachiyomi.App
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.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.getExtensionIcon
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getLibraryMangas
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaCategories
import ir.armor.tachidesk.util.getMangaList
import ir.armor.tachidesk.util.getPageImage
import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
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.sourceGlobalSearch
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.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
@@ -47,6 +62,7 @@ class Main {
// make sure everything we need exists
applicationSetup()
val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
registerConfigModules()
@@ -57,23 +73,29 @@ class Main {
// start app
androidCompat.startApp(App())
// Thread(getMangaUpdateQueueThread).start()
var hasWebUiBundled: Boolean = false
val app = Javalin.create { config ->
try {
this::class.java.classLoader.getResource("/react/index.html")
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(4567)
app.before() { ctx ->
// allow the client which is running on another port
ctx.header("Access-Control-Allow-Origin", "*")
if (hasWebUiBundled) {
openInBrowser()
}
// 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 ->
ctx.json(getExtensionList())
}
@@ -94,6 +116,14 @@ class Main {
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 ->
ctx.json(getSourceList())
}
@@ -127,6 +157,42 @@ class Main {
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 ->
val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(getChapterList(mangaId))
@@ -167,6 +233,54 @@ class Main {
val sourceId = ctx.pathParam("sourceId").toLong()
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,8 +5,10 @@ package ir.armor.tachidesk.database
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
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.ExtensionsTable
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
@@ -16,7 +18,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:h2:${Config.dataRoot}/database.h2", "org.h2.Driver")
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
}
}
@@ -27,10 +29,14 @@ fun makeDataBaseTables() {
// db.useNestedTransactions = true
transaction {
SchemaUtils.create(ExtensionsTable)
SchemaUtils.create(SourceTable)
SchemaUtils.create(MangaTable)
SchemaUtils.create(ChapterTable)
SchemaUtils.create(PageTable)
SchemaUtils.createMissingTablesAndColumns(
ExtensionTable,
SourceTable,
MangaTable,
ChapterTable,
PageTable,
CategoryTable,
CategoryMangaTable,
)
}
}
@@ -1,13 +1,12 @@
package ir.armor.tachidesk.database.dataclass
/* 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 from 'react';
export default function Home() {
return (
<h1>
Home
</h1>
);
}
data class CategoryDataClass(
val id: Int,
val order: Int,
val name: String,
val isLanding: Boolean
)
@@ -20,7 +20,8 @@ data class MangaDataClass(
val author: String? = null,
val description: 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(
@@ -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
* 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.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
class ExtensionEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ExtensionEntity>(ExtensionsTable)
companion object : IntEntityClass<ExtensionEntity>(ExtensionTable)
var name by ExtensionsTable.name
var pkgName by ExtensionsTable.pkgName
var versionName by ExtensionsTable.versionName
var versionCode by ExtensionsTable.versionCode
var lang by ExtensionsTable.lang
var isNsfw by ExtensionsTable.isNsfw
var apkName by ExtensionsTable.apkName
var iconUrl by ExtensionsTable.iconUrl
var installed by ExtensionsTable.installed
var classFQName by ExtensionsTable.classFQName
var name by ExtensionTable.name
var pkgName by ExtensionTable.pkgName
var versionName by ExtensionTable.versionName
var versionCode by ExtensionTable.versionCode
var lang by ExtensionTable.lang
var isNsfw by ExtensionTable.isNsfw
var apkName by ExtensionTable.apkName
var iconUrl by ExtensionTable.iconUrl
var installed by ExtensionTable.installed
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],
)
@@ -6,7 +6,7 @@ package ir.armor.tachidesk.database.table
import org.jetbrains.exposed.dao.id.IntIdTable
object ExtensionsTable : IntIdTable() {
object ExtensionTable : IntIdTable() {
val name = varchar("name", 128)
val pkgName = varchar("pkg_name", 128)
val versionName = varchar("version_name", 16)
@@ -5,7 +5,10 @@ package ir.armor.tachidesk.database.table
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
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.sql.ResultRow
object MangaTable : IntIdTable() {
val url = varchar("url", 2048)
@@ -21,10 +24,32 @@ object MangaTable : IntIdTable() {
val status = integer("status").default(SManga.UNKNOWN)
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
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) {
UNKNOWN(0),
ONGOING(1),
@@ -10,7 +10,7 @@ object SourceTable : IdTable<Long>() {
override val id = long("id").entityId()
val name = varchar("name", 128)
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 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)
}
}
}
@@ -6,12 +6,13 @@ package ir.armor.tachidesk.util
import com.googlecode.dex2jar.tools.Dex2jarCmd
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.APKExtractor
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 kotlinx.coroutines.runBlocking
import okhttp3.Request
@@ -24,6 +25,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
@@ -61,7 +63,7 @@ fun installAPK(apkName: String): Int {
val instance = classToLoad.newInstance()
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
@@ -108,7 +110,7 @@ fun installAPK(apkName: String): Int {
// update extension info
transaction {
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[installed] = true
it[classFQName] = className
}
@@ -137,11 +139,11 @@ fun removeExtension(pkgName: String) {
val fileNameWithoutType = pkgName.substringBefore(".apk")
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
val extensionId = ExtensionsTable.select { ExtensionsTable.name eq extensionRecord.name }.first()[ExtensionsTable.id]
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
SourceTable.deleteWhere { SourceTable.extension eq extensionId }
ExtensionsTable.update({ ExtensionsTable.name eq extensionRecord.name }) {
it[ExtensionsTable.installed] = false
ExtensionTable.update({ ExtensionTable.name eq extensionRecord.name }) {
it[ExtensionTable.installed] = false
}
}
@@ -149,3 +151,22 @@ fun removeExtension(pkgName: String) {
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.model.Extension
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 org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
@@ -22,7 +21,7 @@ private object Data {
private fun extensionDatabaseIsEmtpy(): Boolean {
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()
transaction {
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) {
// update the record
ExtensionsTable.update({ ExtensionsTable.name eq foundExtension.name }) {
ExtensionTable.update({ ExtensionTable.name eq foundExtension.name }) {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
it[versionName] = foundExtension.versionName
@@ -52,7 +51,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
}
} else {
// insert new record
ExtensionsTable.insert {
ExtensionTable.insert {
it[name] = foundExtension.name
it[pkgName] = foundExtension.pkgName
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 ExtensionsTable.selectAll().map {
return@transaction ExtensionTable.selectAll().map {
ExtensionDataClass(
it[ExtensionsTable.name],
it[ExtensionsTable.pkgName],
it[ExtensionsTable.versionName],
it[ExtensionsTable.versionCode],
it[ExtensionsTable.lang],
it[ExtensionsTable.isNsfw],
it[ExtensionsTable.apkName],
it[ExtensionsTable.iconUrl],
it[ExtensionsTable.installed],
it[ExtensionsTable.classFQName]
it[ExtensionTable.name],
it[ExtensionTable.pkgName],
it[ExtensionTable.versionName],
it[ExtensionTable.versionCode],
it[ExtensionTable.lang],
it[ExtensionTable.isNsfw],
it[ExtensionTable.apkName],
getExtensionIconUrl(it[ExtensionTable.apkName]),
it[ExtensionTable.installed],
it[ExtensionTable.classFQName]
)
}
}
@@ -4,6 +4,7 @@ package ir.armor.tachidesk.util
* 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
@@ -15,15 +16,15 @@ 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 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))
@@ -42,7 +43,7 @@ fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
private fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
@@ -50,3 +51,32 @@ fun BufferedSource.saveTo(stream: OutputStream) {
}
}
}
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)
}
}
}
@@ -10,11 +10,9 @@ import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
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 getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
@@ -36,6 +34,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
} else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value)
@@ -79,60 +78,26 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
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()!! }
var filePath = "${Config.thumbnailsRoot}/$mangaId."
val saveDir = Config.thumbnailsRoot
val fileName = mangaId.toString()
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
if (potentialCache != null) {
println("using cached thumbnail file")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter(filePath)}"
)
}
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!!
}
val sourceId = mangaEntry[MangaTable.sourceReference].value
println("getting source for $mangaId")
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
println(thumbnailUrl)
val response = source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
writeStream(response.body!!.byteStream(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
}
}
fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = sourceEntry[SourceTable.name]
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -64,7 +64,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
manga.author,
manga.description,
manga.genre,
MangaStatus.valueOf(manga.status).name,
MangaStatus.valueOf(manga.status).name
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
@@ -83,6 +83,7 @@ fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
mangaEntry[MangaTable.description],
mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary]
)
}
}
@@ -6,17 +6,18 @@ package ir.armor.tachidesk.util
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
import java.nio.file.Files
import java.nio.file.Paths
fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
@@ -45,37 +46,32 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
}
}
val saveDir = getMangaDir(mangaId) + "/" + chapterEntry[ChapterTable.chapter_number]
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
var filePath = "$saveDir/$index."
val fileName = index.toString()
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
if (potentialCache != null) {
println("using cached page file for $index")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter("$filePath")}"
)
}
val response = source.fetchImage(tachiPage).toBlocking().first()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(filePath)).use { os ->
response.body!!.source().saveTo(os)
}
// writeStream(response.body!!.source(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
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.entity.ExtensionEntity
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 org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
@@ -78,7 +78,7 @@ fun getSourceList(): List<SourceDataClass> {
it[SourceTable.id].value.toString(),
it[SourceTable.name],
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
)
}
@@ -93,7 +93,7 @@ fun getSource(sourceId: Long): SourceDataClass {
source[SourceTable.id].value.toString(),
source[SourceTable.name],
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
)
}
@@ -4,15 +4,77 @@ package ir.armor.tachidesk.util
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import dorkbox.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.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import java.awt.event.ActionListener
import java.io.File
import java.io.IOException
fun applicationSetup() {
// make dirs we need
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
File("${Config.extensionsRoot}/icon").mkdirs()
File(Config.thumbnailsRoot).mkdirs()
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
}
}
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

-10
View File
@@ -1,10 +0,0 @@
{
"systemParams": "linux-x64-88",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}
+1
View File
@@ -1,3 +1,4 @@
node_modules/
.eslintcache
.vscode
.env
+1
View File
@@ -10,6 +10,7 @@
"@testing-library/user-event": "^12.1.10",
"fontsource-roboto": "^4.0.0",
"react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"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

+4 -4
View File
@@ -2,14 +2,14 @@
<html lang="en">
<head>
<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="theme-color" content="#000000"/>
<meta
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
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.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Tachidesk</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
+25 -11
View File
@@ -4,22 +4,24 @@
import React, { useState } from 'react';
import {
BrowserRouter as Router, Route, Switch,
BrowserRouter as Router, Redirect, Route, Switch,
} from 'react-router-dom';
import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar';
import Home from './screens/Home';
import Sources from './screens/Sources';
import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList';
import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga';
import Reader from './screens/Reader';
import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
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() {
const [title, setTitle] = useState<string>('Tachidesk');
@@ -53,13 +55,10 @@ export default function App() {
return (
<Router>
<ThemeProvider theme={theme}>
<NavBarTitle.Provider value={navTitleContext}>
<CssBaseline />
<DarkTheme.Provider value={darkThemeContext}>
<NavBar />
</DarkTheme.Provider>
<NavBar />
<Container maxWidth={false} disableGutters>
<Switch>
<Route path="/sources/:sourceId/search/">
@@ -69,10 +68,10 @@ export default function App() {
<Extensions />
</Route>
<Route path="/sources/:sourceId/popular/">
<MangaList popular />
<SourceMangas popular />
</Route>
<Route path="/sources/:sourceId/latest/">
<MangaList popular={false} />
<SourceMangas popular={false} />
</Route>
<Route path="/sources">
<Sources />
@@ -83,9 +82,24 @@ export default function App() {
<Route path="/manga/:id">
<Manga />
</Route>
<Route path="/">
<Home />
<Route path="/library">
<Library />
</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>
</Container>
</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>
);
}
+1 -1
View File
@@ -43,7 +43,7 @@ interface IProps {
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, iconUrl, installed, apkName,
name, lang, versionName, installed, apkName, iconUrl,
},
} = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
+54 -4
View File
@@ -2,20 +2,70 @@
* 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 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{
manga: IManga | undefined
manga: IManga
}
export default function MangaDetails(props: IProps) {
const classes = useStyles();
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 (
<>
<div>
<h1>
{manga && manga.title}
</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>
);
}
+7 -5
View File
@@ -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
* 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/. */
@@ -45,7 +47,7 @@ export default function NavBar() {
const { title } = useContext(NavBarTitle);
const open = Boolean(anchorEl);
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
@@ -72,15 +74,15 @@ export default function NavBar() {
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
<IconButton
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<MoreIcon />
</IconButton>
<Menu
</IconButton> */}
{/* <Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
@@ -107,7 +109,7 @@ export default function NavBar() {
Light Theme
</MenuItem>
</Menu>
</Menu> */}
</Toolbar>
</AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
@@ -36,6 +36,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
onKeyDown={() => setDrawerOpen(false)}
>
<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' }}>
<ListItem button key="Extensions">
<ListItemIcon>
@@ -52,6 +60,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Sources" />
</ListItem>
</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' }}>
<ListItem button key="Search">
<ListItemIcon>
+157
View File
@@ -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;
}
+1 -1
View File
@@ -38,7 +38,7 @@ export default function Manga() {
return (
<>
<MangaDetails manga={manga} />
{manga && <MangaDetails manga={manga} />}
{chapterCards}
</>
);
+51
View File
@@ -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 NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) {
export default function SourceMangas(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle);
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>
</>
);
}
+9
View File
@@ -9,6 +9,7 @@ interface IExtension {
iconUrl: string
installed: boolean
apkName: string
pkgName: string
}
interface ISource {
@@ -24,6 +25,7 @@ interface IManga {
id: number
title: string
thumbnailUrl: string
inLibrary?: boolean
}
interface IChapter {
@@ -36,3 +38,10 @@ interface IChapter {
mangaId: number
pageCount: number
}
interface ICategory {
id: number
order: number
name: String
isLanding: boolean
}
+57 -3
View File
@@ -3744,6 +3744,13 @@ css-blank-pseudo@^0.1.4:
dependencies:
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:
version "0.0.4"
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"
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:
version "0.4.1"
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"
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:
version "3.4.1"
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"
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:
version "11.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
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"
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:
version "0.8.3"
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"
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:
version "8.2.0"
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"
util.promisify "~1.0.0"
symbol-observable@1.2.0:
symbol-observable@1.2.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
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"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2:
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@@ -11176,6 +11225,11 @@ url@^0.11.0:
punycode "1.3.2"
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:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
-4
View File
@@ -1,4 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1