Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92ed48f7f6 | |||
| 13e84bc492 | |||
| 0ef86c34b7 | |||
| 7e1a4259d7 | |||
| c842c51fb6 | |||
| 6f2f228e08 | |||
| c78eaa8b96 | |||
| f9606526d2 | |||
| fe4cc9ea2c | |||
| 54d0c05fcc | |||
| 2f7df73a37 | |||
| cf19f3626b | |||
| ff2da5e59b | |||
| e03922e518 | |||
| 893fba5b8c | |||
| c1786f8e24 | |||
| a59f974537 | |||
| 7157e07328 | |||
| 954084bd82 | |||
| 0915ba40f6 | |||
| de30d55bcf | |||
| af1c34fba5 | |||
| 7b7d93786f | |||
| 7c1c504482 | |||
| 33b22fcab6 | |||
| ab0566dcba | |||
| c4f2cc7189 | |||
| 4626d99590 | |||
| 6465ca8a19 | |||
| 15b9d151df | |||
| dd1b6c86cd | |||
| 9613cda79a | |||
| 648b8e5960 | |||
| ce545b1fd5 | |||
| 9151034fbc | |||
| 312a8baa13 | |||
| 18b6168cd1 | |||
| 9a282c3bf4 | |||
| 2bbebe4c30 | |||
| 162961b560 | |||
| f1cc37d0db |
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: "🐞 Bug report"
|
||||||
|
title: "[Bug] <short description>"
|
||||||
|
about: "Report a bug"
|
||||||
|
labels: "bug"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app.
|
||||||
|
- I have tried the troubleshooting guide described in `README.md`
|
||||||
|
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
- If this is an issue with some extension not working properly, It does work inside Tachiyomi as intended.
|
||||||
|
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||||
|
- I will fill out the title and the information in this template
|
||||||
|
|
||||||
|
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Device information
|
||||||
|
- Tachidesk version: (Example: v0.2.3-r255-win32)
|
||||||
|
- Server Operating System: (Example: Ubuntu 20.04)
|
||||||
|
- Server JVM version: bundled with win32 or (Example: Java 8 Update 281 or OpenJDK 8u281)
|
||||||
|
- Client Operating System: <usually the same as above Server Operating System>
|
||||||
|
- Client Web Browser: (Example: Google Chrome 89.0.4389.82)
|
||||||
|
|
||||||
|
## Steps to reproduce
|
||||||
|
1. First Step
|
||||||
|
2. Second Step
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
Describe what should have happened
|
||||||
|
|
||||||
|
### Actual behavior
|
||||||
|
Describe what happens instead
|
||||||
|
|
||||||
|
## Other details
|
||||||
|
Describe additional details If necessary
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: "🌟 Feature request"
|
||||||
|
title: "[Feature Request] <short description>"
|
||||||
|
about: "Suggest a feature to improve the project"
|
||||||
|
labels: "enhancement"
|
||||||
|
---
|
||||||
|
|
||||||
|
**PLEASE READ THIS**
|
||||||
|
|
||||||
|
I acknowledge that:
|
||||||
|
|
||||||
|
- I have updated to the latest version of the app.
|
||||||
|
- I have tried the troubleshooting guide described in `README.md`
|
||||||
|
- If this is a request for adding/changing an extension it should be brought up to Tachiyomi: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
- If this is an issue with some extension not working properly, It does work in Tachiyomi application as intended.
|
||||||
|
- I have searched the existing issues and this is a new ticket **NOT** a duplicate or related to another open issue
|
||||||
|
- I will fill out the title and the information in this template
|
||||||
|
|
||||||
|
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||||
|
|
||||||
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What feature should be added to Tachidesk?
|
||||||
|
Explain What the feature is and how it should work in detail
|
||||||
|
|
||||||
|
## Why/Project's Benefit/Existing Problem
|
||||||
|
Explain why this should be added
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Issue closer
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
autoclose:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Autoclose issues
|
||||||
|
uses: arkon/issue-closer-action@v3.0
|
||||||
|
with:
|
||||||
|
repo-token: ${{ github.token }}
|
||||||
|
rules: |
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "title",
|
||||||
|
"regex": ".*<short description>*",
|
||||||
|
"message": "You did not fill out the description in the title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
|
"message": "The acknowledgment section was not removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "body",
|
||||||
|
"regex": "(Tachidesk version|Server Operating System|Server JVM version|Client Operating System|Client Web Browser):.*(\\(Example:|<usually).*",
|
||||||
|
"message": "The requested information was not filled out"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
// Config API
|
// Config API, moved to the global build.gradle
|
||||||
// implementation("com.typesafe:config:1.4.0")
|
// implementation("com.typesafe:config:1.4.0")
|
||||||
}
|
}
|
||||||
@@ -4,54 +4,55 @@ import com.typesafe.config.Config
|
|||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages app config.
|
* Manages app config.
|
||||||
*/
|
*/
|
||||||
open class ConfigManager {
|
open class ConfigManager {
|
||||||
private val generatedModules
|
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
|
||||||
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
|
||||||
|
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
|
||||||
val config by lazy { loadConfigs() }
|
val config by lazy { loadConfigs() }
|
||||||
|
|
||||||
//Public read-only view of modules
|
//Public read-only view of modules
|
||||||
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
|
||||||
get() = generatedModules
|
get() = generatedModules
|
||||||
|
|
||||||
open val configFolder: String
|
open val appConfigFile: String = "$dataRoot/server.conf"
|
||||||
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
|
|
||||||
|
|
||||||
val logger = KotlinLogging.logger {}
|
val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a config module
|
* Get a config module
|
||||||
*/
|
*/
|
||||||
inline fun <reified T : ConfigModule> module(): T
|
inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
|
||||||
= loadedModules[T::class.java] as T
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a config module (Java API)
|
* Get a config module (Java API)
|
||||||
*/
|
*/
|
||||||
fun <T : ConfigModule> module(type: Class<T>): T
|
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
|
||||||
= loadedModules[type] as T
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configs
|
* Load configs
|
||||||
*/
|
*/
|
||||||
fun loadConfigs(): Config {
|
fun loadConfigs(): Config {
|
||||||
val configs = mutableListOf<Config>()
|
//Load reference configs
|
||||||
|
val compatConfig = ConfigFactory.parseResources("compat-reference.conf")
|
||||||
|
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||||
|
|
||||||
//Load reference config
|
//Load user config
|
||||||
configs += ConfigFactory.parseResources("reference.conf")
|
val userConfig =
|
||||||
|
File(appConfigFile).let{
|
||||||
//Load custom configs from dir
|
|
||||||
File(configFolder).listFiles()?.map {
|
|
||||||
ConfigFactory.parseFile(it)
|
ConfigFactory.parseFile(it)
|
||||||
}?.filterNotNull()?.forEach {
|
|
||||||
configs += it.withFallback(configs.last())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val config = configs.last().resolve()
|
val config = ConfigFactory.empty()
|
||||||
|
.withFallback(userConfig)
|
||||||
|
.withFallback(compatConfig)
|
||||||
|
.withFallback(serverConfig)
|
||||||
|
.resolve()
|
||||||
|
|
||||||
logger.debug {
|
logger.debug {
|
||||||
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
"Loaded config:\n" + config.root().render(ConfigRenderOptions.concise().setFormatted(true))
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package xyz.nulldev.ts.config
|
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class ServerConfig(config: Config) : ConfigModule(config) {
|
|
||||||
val ip = config.getString("ip")
|
|
||||||
val port = config.getInt("port")
|
|
||||||
|
|
||||||
val allowConfigChanges = config.getBoolean("allowConfigChanges")
|
|
||||||
val enableWebUi = config.getBoolean("enableWebUi")
|
|
||||||
val useOldWebUi = config.getBoolean("useOldWebUi")
|
|
||||||
val prettyPrintApi = config.getBoolean("prettyPrintApi")
|
|
||||||
// TODO Apply to operation IDs
|
|
||||||
val disabledApiEndpoints = config.getStringList("disabledApiEndpoints").map(String::toLowerCase)
|
|
||||||
val enabledApiEndpoints = config.getStringList("enabledApiEndpoints").map(String::toLowerCase)
|
|
||||||
val httpInitializedPrintMessage = config.getString("httpInitializedPrintMessage")
|
|
||||||
|
|
||||||
val useExternalStaticFiles = config.getBoolean("useExternalStaticFiles")
|
|
||||||
val externalStaticFilesFolder = config.getString("externalStaticFilesFolder")
|
|
||||||
|
|
||||||
val rootDir = registerFile(config.getString("rootDir"))
|
|
||||||
val patchesDir = registerFile(config.getString("patchesDir"))
|
|
||||||
|
|
||||||
fun registerFile(file: String): File {
|
|
||||||
return File(file).apply {
|
|
||||||
mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun register(config: Config)
|
|
||||||
= ServerConfig(config.getConfig("ts.server"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
# Server ip and port bindings
|
|
||||||
ts.server.ip = 0.0.0.0
|
|
||||||
ts.server.port = 4567
|
|
||||||
|
|
||||||
# Allow/disallow preference changes (useful for demos)
|
# Allow/disallow preference changes (useful for demos)
|
||||||
ts.server.allowConfigChanges = true
|
ts.server.allowConfigChanges = true
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|

|
||||||
# Tachidesk
|
# Tachidesk
|
||||||
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
A free and open source manga reader that runs extensions built for [Tachiyomi](https://tachiyomi.org/).
|
||||||
|
|
||||||
@@ -5,7 +7,20 @@ 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 app?
|
## 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 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.
|
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.
|
||||||
|
|
||||||
@@ -21,6 +36,25 @@ Windows specific builds have java bundled inside them, so you don't have to inst
|
|||||||
#### 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
|
||||||
@@ -41,21 +75,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
|
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.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
|
|
||||||
|
|
||||||
## 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`.
|
||||||
|
|
||||||
|
|||||||
@@ -76,5 +76,8 @@ configure(listOf(
|
|||||||
|
|
||||||
// dependency of :AndroidCompat:Config
|
// dependency of :AndroidCompat:Config
|
||||||
implementation("com.typesafe:config:1.4.0")
|
implementation("com.typesafe:config:1.4.0")
|
||||||
|
|
||||||
|
// to get application content root
|
||||||
|
implementation("net.harawata:appdirs:1.2.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ plugins {
|
|||||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
val TachideskVersion = "v0.2.1"
|
val TachideskVersion = "v0.2.4"
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@@ -72,9 +72,6 @@ dependencies {
|
|||||||
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
|
||||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
|
||||||
|
|
||||||
// to get application content root
|
|
||||||
implementation("net.harawata:appdirs:1.2.0")
|
|
||||||
|
|
||||||
// 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")
|
||||||
@@ -149,7 +146,7 @@ launch4j { //used for windows
|
|||||||
bundledJre64Bit = true
|
bundledJre64Bit = true
|
||||||
jreMinVersion = "8"
|
jreMinVersion = "8"
|
||||||
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
|
||||||
icon = "${projectDir}/src/main/resources/icon/icon_round.ico"
|
icon = "${projectDir}/src/main/resources/icon/faviconlogo.ico"
|
||||||
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package ir.armor.tachidesk
|
|
||||||
|
|
||||||
/* 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 net.harawata.appdirs.AppDirsFactory
|
|
||||||
|
|
||||||
object Config {
|
|
||||||
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)
|
|
||||||
val extensionsRoot = "$dataRoot/extensions"
|
|
||||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
|
||||||
val mangaRoot = "$dataRoot/manga"
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,9 @@ package ir.armor.tachidesk
|
|||||||
* 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.App
|
|
||||||
import io.javalin.Javalin
|
import io.javalin.Javalin
|
||||||
import ir.armor.tachidesk.util.addMangaToCategory
|
import ir.armor.tachidesk.util.addMangaToCategory
|
||||||
import ir.armor.tachidesk.util.addMangaToLibrary
|
import ir.armor.tachidesk.util.addMangaToLibrary
|
||||||
import ir.armor.tachidesk.util.applicationSetup
|
|
||||||
import ir.armor.tachidesk.util.createCategory
|
import ir.armor.tachidesk.util.createCategory
|
||||||
import ir.armor.tachidesk.util.getCategoryList
|
import ir.armor.tachidesk.util.getCategoryList
|
||||||
import ir.armor.tachidesk.util.getCategoryMangaList
|
import ir.armor.tachidesk.util.getCategoryMangaList
|
||||||
@@ -18,6 +16,7 @@ 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.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.getPageImage
|
import ir.armor.tachidesk.util.getPageImage
|
||||||
import ir.armor.tachidesk.util.getSource
|
import ir.armor.tachidesk.util.getSource
|
||||||
@@ -29,47 +28,17 @@ import ir.armor.tachidesk.util.removeCategory
|
|||||||
import ir.armor.tachidesk.util.removeExtension
|
import ir.armor.tachidesk.util.removeExtension
|
||||||
import ir.armor.tachidesk.util.removeMangaFromCategory
|
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||||
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
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 ir.armor.tachidesk.util.updateCategory
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.conf.global
|
|
||||||
import xyz.nulldev.androidcompat.AndroidCompat
|
|
||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
|
||||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
|
||||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
companion object {
|
companion object {
|
||||||
val androidCompat by lazy { AndroidCompat() }
|
|
||||||
|
|
||||||
fun registerConfigModules() {
|
|
||||||
GlobalConfigManager.registerModules(
|
|
||||||
// ServerConfig.register(GlobalConfigManager.config),
|
|
||||||
// SyncConfigModule.register(GlobalConfigManager.config)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
// System.getProperties()["proxySet"] = "true"
|
serverSetup()
|
||||||
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
|
|
||||||
// System.getProperties()["socksProxyPort"] = "2020"
|
|
||||||
|
|
||||||
// make sure everything we need exists
|
|
||||||
applicationSetup()
|
|
||||||
val tray = systemTray()
|
|
||||||
|
|
||||||
registerConfigModules()
|
|
||||||
|
|
||||||
// Load config API
|
|
||||||
DI.global.addImport(ConfigKodeinModule().create())
|
|
||||||
// Load Android compatibility dependencies
|
|
||||||
AndroidCompatInitializer().init()
|
|
||||||
// start app
|
|
||||||
androidCompat.startApp(App())
|
|
||||||
|
|
||||||
var hasWebUiBundled: Boolean = false
|
var hasWebUiBundled: Boolean = false
|
||||||
|
|
||||||
@@ -84,16 +53,11 @@ class Main {
|
|||||||
hasWebUiBundled = false
|
hasWebUiBundled = false
|
||||||
}
|
}
|
||||||
config.enableCorsForAllOrigins()
|
config.enableCorsForAllOrigins()
|
||||||
}.start(4567)
|
}.start(serverConfig.ip, serverConfig.port)
|
||||||
if (hasWebUiBundled) {
|
if (hasWebUiBundled) {
|
||||||
openInBrowser()
|
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 ->
|
app.get("/api/v1/extension/list") { ctx ->
|
||||||
ctx.json(getExtensionList())
|
ctx.json(getExtensionList())
|
||||||
}
|
}
|
||||||
@@ -165,11 +129,16 @@ class Main {
|
|||||||
// removes the manga from the library
|
// removes the manga from the library
|
||||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
println("fuck")
|
|
||||||
removeMangaFromLibrary(mangaId)
|
removeMangaFromLibrary(mangaId)
|
||||||
ctx.status(200)
|
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
|
// adds the manga to category
|
||||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||||
@@ -227,7 +196,7 @@ class Main {
|
|||||||
ctx.json(sourceFilters(sourceId))
|
ctx.json(sourceFilters(sourceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// lists all manga in the library, suitable if no categories are defined
|
// lists mangas that have no category assigned
|
||||||
app.get("/api/v1/library/") { ctx ->
|
app.get("/api/v1/library/") { ctx ->
|
||||||
ctx.json(getLibraryMangas())
|
ctx.json(getLibraryMangas())
|
||||||
}
|
}
|
||||||
@@ -240,20 +209,28 @@ class Main {
|
|||||||
// category create
|
// category create
|
||||||
app.post("/api/v1/category/") { ctx ->
|
app.post("/api/v1/category/") { ctx ->
|
||||||
val name = ctx.formParam("name")!!
|
val name = ctx.formParam("name")!!
|
||||||
val isLanding = ctx.formParam("isLanding", "false").toBoolean()
|
createCategory(name)
|
||||||
createCategory(name, isLanding)
|
|
||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// category modification
|
// category modification
|
||||||
app.put("/api/v1/category/:categoryId") { ctx ->
|
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
||||||
val name = ctx.formParam("name")!!
|
val name = ctx.formParam("name")
|
||||||
val isLanding = ctx.formParam("isLanding").toBoolean()
|
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||||
updateCategory(categoryId, name, isLanding)
|
updateCategory(categoryId, name, isLanding)
|
||||||
ctx.status(200)
|
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
|
// category delete
|
||||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import xyz.nulldev.ts.config.ConfigModule
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ServerConfig(config: Config) : ConfigModule(config) {
|
||||||
|
val ip = config.getString("ip")
|
||||||
|
val port = config.getInt("port")
|
||||||
|
|
||||||
|
// proxy
|
||||||
|
val socksProxy = config.getBoolean("socksProxy")
|
||||||
|
val socksProxyHost = config.getString("socksProxyHost")
|
||||||
|
val socksProxyPort = config.getString("socksProxyPort")
|
||||||
|
|
||||||
|
fun registerFile(file: String): File {
|
||||||
|
return File(file).apply {
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun register(config: Config) = ServerConfig(config.getConfig("server"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package ir.armor.tachidesk
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.App
|
||||||
|
import ir.armor.tachidesk.database.makeDataBaseTables
|
||||||
|
import ir.armor.tachidesk.util.systemTray
|
||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
|
import org.kodein.di.DI
|
||||||
|
import org.kodein.di.conf.global
|
||||||
|
import xyz.nulldev.androidcompat.AndroidCompat
|
||||||
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
|
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||||
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object applicationDirs {
|
||||||
|
val dataRoot = AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!!
|
||||||
|
val extensionsRoot = "$dataRoot/extensions"
|
||||||
|
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||||
|
val mangaRoot = "$dataRoot/manga"
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||||
|
|
||||||
|
val systemTray by lazy { systemTray() }
|
||||||
|
|
||||||
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
|
fun serverSetup() {
|
||||||
|
// register server config
|
||||||
|
GlobalConfigManager.registerModule(
|
||||||
|
ServerConfig.register(GlobalConfigManager.config)
|
||||||
|
)
|
||||||
|
|
||||||
|
// make dirs we need
|
||||||
|
listOf(
|
||||||
|
applicationDirs.dataRoot,
|
||||||
|
applicationDirs.extensionsRoot,
|
||||||
|
"${applicationDirs.extensionsRoot}/icon",
|
||||||
|
applicationDirs.thumbnailsRoot
|
||||||
|
).forEach {
|
||||||
|
File(it).mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDataBaseTables()
|
||||||
|
|
||||||
|
// create system tray
|
||||||
|
systemTray
|
||||||
|
|
||||||
|
// Load config API
|
||||||
|
DI.global.addImport(ConfigKodeinModule().create())
|
||||||
|
// Load Android compatibility dependencies
|
||||||
|
AndroidCompatInitializer().init()
|
||||||
|
// start app
|
||||||
|
androidCompat.startApp(App())
|
||||||
|
|
||||||
|
// socks proxy settings
|
||||||
|
System.getProperties()["proxySet"] = serverConfig.socksProxy.toString()
|
||||||
|
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost
|
||||||
|
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ package ir.armor.tachidesk.database
|
|||||||
* 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.Config
|
import ir.armor.tachidesk.applicationDirs
|
||||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.database.table.CategoryTable
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.database.table.ChapterTable
|
import ir.armor.tachidesk.database.table.ChapterTable
|
||||||
@@ -18,23 +18,24 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
|
|
||||||
object DBMangaer {
|
object DBMangaer {
|
||||||
val db by lazy {
|
val db by lazy {
|
||||||
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
|
Database.connect("jdbc:h2:${applicationDirs.dataRoot}/database", "org.h2.Driver")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun makeDataBaseTables() {
|
fun makeDataBaseTables() {
|
||||||
// mention db object to connect
|
// must mention db object so the lazy block executes
|
||||||
DBMangaer.db
|
val db = DBMangaer.db
|
||||||
// val db = DBMangaer.db
|
db.useNestedTransactions = true
|
||||||
// db.useNestedTransactions = true
|
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(ExtensionTable)
|
SchemaUtils.createMissingTablesAndColumns(
|
||||||
SchemaUtils.create(SourceTable)
|
ExtensionTable,
|
||||||
SchemaUtils.create(MangaTable)
|
SourceTable,
|
||||||
SchemaUtils.create(ChapterTable)
|
MangaTable,
|
||||||
SchemaUtils.create(PageTable)
|
ChapterTable,
|
||||||
SchemaUtils.create(CategoryTable)
|
PageTable,
|
||||||
SchemaUtils.create(CategoryMangaTable)
|
CategoryTable,
|
||||||
|
CategoryMangaTable,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
data class CategoryDataClass(
|
data class CategoryDataClass(
|
||||||
|
val id: Int,
|
||||||
|
val order: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val isLanding: Boolean
|
val isLanding: Boolean
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import org.jetbrains.exposed.sql.ResultRow
|
|||||||
object CategoryTable : IntIdTable() {
|
object CategoryTable : IntIdTable() {
|
||||||
val name = varchar("name", 64)
|
val name = varchar("name", 64)
|
||||||
val isLanding = bool("is_landing").default(false)
|
val isLanding = bool("is_landing").default(false)
|
||||||
|
val order = integer("order").default(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||||
|
categoryEntry[CategoryTable.id].value,
|
||||||
|
categoryEntry[CategoryTable.order],
|
||||||
categoryEntry[CategoryTable.name],
|
categoryEntry[CategoryTable.name],
|
||||||
categoryEntry[CategoryTable.isLanding],
|
categoryEntry[CategoryTable.isLanding],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ object MangaTable : IntIdTable() {
|
|||||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
val inLibrary = bool("in_library").default(false)
|
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)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package ir.armor.tachidesk.util
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
|
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||||
import ir.armor.tachidesk.database.table.CategoryTable
|
import ir.armor.tachidesk.database.table.CategoryTable
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
import ir.armor.tachidesk.database.table.toDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
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
|
||||||
@@ -14,34 +16,50 @@ import org.jetbrains.exposed.sql.update
|
|||||||
* 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/. */
|
||||||
|
|
||||||
fun createCategory(name: String, isLanding: Boolean) {
|
fun createCategory(name: String) {
|
||||||
transaction {
|
transaction {
|
||||||
|
val count = CategoryTable.selectAll().count()
|
||||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||||
CategoryTable.insert {
|
CategoryTable.insert {
|
||||||
it[CategoryTable.name] = name
|
it[CategoryTable.name] = name
|
||||||
it[CategoryTable.isLanding] = isLanding
|
it[CategoryTable.order] = count.toInt() + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCategory(categoryId: Int, name: String, isLanding: Boolean) {
|
fun updateCategory(categoryId: Int, name: String?, isLanding: Boolean?) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||||
it[CategoryTable.name] = name
|
if (name != null) it[CategoryTable.name] = name
|
||||||
it[CategoryTable.isLanding] = isLanding
|
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) {
|
fun removeCategory(categoryId: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
|
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
|
||||||
|
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
|
||||||
|
}
|
||||||
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCategoryList(): List<CategoryDataClass> {
|
fun getCategoryList(): List<CategoryDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
CategoryTable.selectAll().map {
|
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||||
CategoryTable.toDataClass(it)
|
CategoryTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package ir.armor.tachidesk.util
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
|
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
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.MangaTable
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
import ir.armor.tachidesk.database.table.toDataClass
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
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
|
||||||
|
|
||||||
/* 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
|
||||||
@@ -21,6 +25,10 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
|||||||
it[CategoryMangaTable.category] = categoryId
|
it[CategoryMangaTable.category] = categoryId
|
||||||
it[CategoryMangaTable.manga] = mangaId
|
it[CategoryMangaTable.manga] = mangaId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
it[MangaTable.defaultCategory] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +36,11 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
|||||||
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
|
||||||
transaction {
|
transaction {
|
||||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,3 +51,11 @@ fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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.applicationDirs
|
||||||
import ir.armor.tachidesk.database.table.ExtensionTable
|
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
|
||||||
@@ -32,10 +32,10 @@ import java.net.URLClassLoader
|
|||||||
fun installAPK(apkName: String): Int {
|
fun installAPK(apkName: String): Int {
|
||||||
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
|
||||||
val fileNameWithoutType = apkName.substringBefore(".apk")
|
val fileNameWithoutType = apkName.substringBefore(".apk")
|
||||||
val dirPathWithoutType = "${Config.extensionsRoot}/$fileNameWithoutType"
|
val dirPathWithoutType = "${applicationDirs.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 jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
if (!File(jarPath).exists()) {
|
if (!File(jarPath).exists()) {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val api = ExtensionGithubApi()
|
val api = ExtensionGithubApi()
|
||||||
@@ -137,7 +137,7 @@ private fun downloadAPKFile(url: String, apkPath: String) {
|
|||||||
fun removeExtension(pkgName: String) {
|
fun removeExtension(pkgName: String) {
|
||||||
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
|
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
|
||||||
val fileNameWithoutType = pkgName.substringBefore(".apk")
|
val fileNameWithoutType = pkgName.substringBefore(".apk")
|
||||||
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
|
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||||
transaction {
|
transaction {
|
||||||
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
val extensionId = ExtensionTable.select { ExtensionTable.name eq extensionRecord.name }.first()[ExtensionTable.id]
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ val network: NetworkHelper by injectLazy()
|
|||||||
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
val iconUrl = transaction { ExtensionTable.select { ExtensionTable.apkName eq apkName }.firstOrNull()!! }[ExtensionTable.iconUrl]
|
||||||
|
|
||||||
val saveDir = "${Config.extensionsRoot}/icon"
|
val saveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||||
val fileName = apkName
|
val fileName = apkName
|
||||||
|
|
||||||
return getCachedResponse(saveDir, fileName) {
|
return getCachedResponse(saveDir, fileName) {
|
||||||
@@ -168,5 +168,5 @@ fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionIconUrl(apkName: String): String {
|
fun getExtensionIconUrl(apkName: String): String {
|
||||||
return "http://127.0.0.1:4567/api/v1/extension/icon/$apkName"
|
return "/api/v1/extension/icon/$apkName"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package ir.armor.tachidesk.util
|
package ir.armor.tachidesk.util
|
||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
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.MangaTable
|
||||||
import ir.armor.tachidesk.database.table.toDataClass
|
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.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
|
||||||
@@ -24,18 +28,20 @@ fun addMangaToLibrary(mangaId: Int) {
|
|||||||
|
|
||||||
fun removeMangaFromLibrary(mangaId: Int) {
|
fun removeMangaFromLibrary(mangaId: Int) {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
if (!manga.inLibrary) {
|
if (manga.inLibrary) {
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||||
it[inLibrary] = false
|
it[inLibrary] = false
|
||||||
|
it[defaultCategory] = true
|
||||||
}
|
}
|
||||||
|
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryMangas(): List<MangaDataClass> {
|
fun getLibraryMangas(): List<MangaDataClass> {
|
||||||
return transaction {
|
return transaction {
|
||||||
MangaTable.select { MangaTable.inLibrary eq true }.map {
|
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||||
MangaTable.toDataClass(it)
|
MangaTable.toDataClass(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.applicationDirs
|
||||||
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
|
||||||
@@ -85,7 +85,7 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
|
|
||||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
val saveDir = Config.thumbnailsRoot
|
val saveDir = applicationDirs.thumbnailsRoot
|
||||||
val fileName = mangaId.toString()
|
val fileName = mangaId.toString()
|
||||||
|
|
||||||
return getCachedResponse(saveDir, fileName) {
|
return getCachedResponse(saveDir, fileName) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ 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 {
|
fun proxyThumbnailUrl(mangaId: Int): String {
|
||||||
return "http://127.0.0.1:4567/api/v1/manga/$mangaId/thumbnail"
|
return "/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 {
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import ir.armor.tachidesk.Config
|
import ir.armor.tachidesk.applicationDirs
|
||||||
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 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.SqlExpressionBuilder.eq
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
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
|
||||||
@@ -70,7 +69,7 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
|||||||
val mangaTitle = mangaEntry[MangaTable.title]
|
val mangaTitle = mangaEntry[MangaTable.title]
|
||||||
val sourceName = source.toString()
|
val sourceName = source.toString()
|
||||||
|
|
||||||
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
|
||||||
// make sure dirs exist
|
// make sure dirs exist
|
||||||
File(mangaDir).mkdirs()
|
File(mangaDir).mkdirs()
|
||||||
return mangaDir
|
return mangaDir
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
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.Config
|
import ir.armor.tachidesk.applicationDirs
|
||||||
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
|
||||||
@@ -36,7 +36,7 @@ fun getHttpSource(sourceId: Long): HttpSource {
|
|||||||
val apkName = extensionRecord.apkName
|
val apkName = extensionRecord.apkName
|
||||||
val className = extensionRecord.classFQName
|
val className = extensionRecord.classFQName
|
||||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||||
val jarPath = "${Config.extensionsRoot}/$jarName"
|
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||||
|
|
||||||
println(jarName)
|
println(jarName)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ fun getSourceList(): List<SourceDataClass> {
|
|||||||
SourceDataClass(
|
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])),
|
it[SourceTable.lang],
|
||||||
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
getExtensionIconUrl(ExtensionTable.select { ExtensionTable.id eq it[SourceTable.extension] }.first()[ExtensionTable.apkName]),
|
||||||
getHttpSource(it[SourceTable.id].value).supportsLatest
|
getHttpSource(it[SourceTable.id].value).supportsLatest
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,30 +9,20 @@ import dorkbox.systemTray.SystemTray
|
|||||||
import dorkbox.systemTray.SystemTray.TrayType
|
import dorkbox.systemTray.SystemTray.TrayType
|
||||||
import dorkbox.util.CacheUtil
|
import dorkbox.util.CacheUtil
|
||||||
import dorkbox.util.Desktop
|
import dorkbox.util.Desktop
|
||||||
import ir.armor.tachidesk.Config
|
|
||||||
import ir.armor.tachidesk.Main
|
import ir.armor.tachidesk.Main
|
||||||
import ir.armor.tachidesk.database.makeDataBaseTables
|
|
||||||
import java.awt.event.ActionListener
|
import java.awt.event.ActionListener
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
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() {
|
fun openInBrowser() {
|
||||||
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
|
} catch (e1: IOException) {
|
||||||
|
e1.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val icon = Main::class.java.getResource("/icon/icon_round.png")
|
|
||||||
|
|
||||||
fun systemTray(): SystemTray? {
|
fun systemTray(): SystemTray? {
|
||||||
|
try {
|
||||||
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
|
// 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
|
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
|
||||||
if (System.getProperty("os.name").startsWith("Windows"))
|
if (System.getProperty("os.name").startsWith("Windows"))
|
||||||
@@ -49,13 +39,15 @@ fun systemTray(): SystemTray? {
|
|||||||
ActionListener {
|
ActionListener {
|
||||||
try {
|
try {
|
||||||
Desktop.browseURL("http://127.0.0.1:4567")
|
Desktop.browseURL("http://127.0.0.1:4567")
|
||||||
} catch (e1: IOException) {
|
} catch (e: IOException) {
|
||||||
e1.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
|
||||||
|
|
||||||
// systemTray.setTooltip("Tachidesk")
|
// systemTray.setTooltip("Tachidesk")
|
||||||
systemTray.setImage(icon)
|
systemTray.setImage(icon)
|
||||||
// systemTray.status = "No Mail"
|
// systemTray.status = "No Mail"
|
||||||
@@ -68,4 +60,8 @@ fun systemTray(): SystemTray? {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return systemTray
|
return systemTray
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
# Server ip and port bindings
|
||||||
|
server.ip = 0.0.0.0
|
||||||
|
server.port = 4567
|
||||||
|
|
||||||
|
# Socks5 proxy
|
||||||
|
server.socksProxy = false
|
||||||
|
server.socksProxyHost = ""
|
||||||
|
server.socksProxyPort = ""
|
||||||
@@ -13,5 +13,7 @@ module.exports = {
|
|||||||
|
|
||||||
// Indent props with 4 spaces
|
// Indent props with 4 spaces
|
||||||
'react/jsx-indent-props': ['error', 4],
|
'react/jsx-indent-props': ['error', 4],
|
||||||
|
|
||||||
|
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.vscode
|
.vscode
|
||||||
|
.env
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 579 KiB |
@@ -7,9 +7,9 @@
|
|||||||
<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%/favicon.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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Tachidesk",
|
||||||
"name": "Create React App Sample",
|
"name": "Tachidesk",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
@@ -8,18 +8,24 @@
|
|||||||
"type": "image/x-icon"
|
"type": "image/x-icon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo192.png",
|
"src": "favicon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "logo512.png",
|
"src": "favicon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "favicon.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#ff2323",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#ff2323"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,28 +4,33 @@
|
|||||||
|
|
||||||
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 NavbarContext from './context/NavbarContext';
|
||||||
import DarkTheme from './context/DarkTheme';
|
import DarkTheme from './context/DarkTheme';
|
||||||
import Library from './screens/Library';
|
import Library from './screens/Library';
|
||||||
|
import Settings from './screens/Settings';
|
||||||
|
import Categories from './screens/settings/Categories';
|
||||||
|
import useLocalStorage from './util/useLocalStorage';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
const [darkTheme, setDarkTheme] = useState<boolean>(true);
|
const [action, setAction] = useState<any>(<div />);
|
||||||
const navTitleContext = { title, setTitle };
|
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
|
||||||
|
const navBarContext = {
|
||||||
|
title, setTitle, action, setAction,
|
||||||
|
};
|
||||||
const darkThemeContext = { darkTheme, setDarkTheme };
|
const darkThemeContext = { darkTheme, setDarkTheme };
|
||||||
|
|
||||||
const theme = React.useMemo(
|
const theme = React.useMemo(
|
||||||
@@ -55,11 +60,9 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<NavBarTitle.Provider value={navTitleContext}>
|
<NavbarContext.Provider value={navBarContext}>
|
||||||
<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 +72,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 />
|
||||||
@@ -86,12 +89,24 @@ export default function App() {
|
|||||||
<Route path="/library">
|
<Route path="/library">
|
||||||
<Library />
|
<Library />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/settings/categories">
|
||||||
<Home />
|
<Categories />
|
||||||
</Route>
|
</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>
|
</NavbarContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* 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';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
|
client.get('/api/v1/category/')
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data: ICategory[]) => {
|
||||||
|
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
client.get(`/api/v1/manga/${mangaId}/category/`)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.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;
|
||||||
|
|
||||||
|
const method = checked ? client.get : client.delete;
|
||||||
|
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
||||||
|
.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
|
|||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import client from '../util/client';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -38,6 +40,7 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
extension: IExtension
|
extension: IExtension
|
||||||
|
notifyInstall: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExtensionCard(props: IProps) {
|
export default function ExtensionCard(props: IProps) {
|
||||||
@@ -45,23 +48,30 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
extension: {
|
extension: {
|
||||||
name, lang, versionName, installed, apkName, iconUrl,
|
name, lang, versionName, installed, apkName, iconUrl,
|
||||||
},
|
},
|
||||||
|
notifyInstall,
|
||||||
} = props;
|
} = props;
|
||||||
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||||
|
|
||||||
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
|
||||||
|
|
||||||
function install() {
|
function install() {
|
||||||
setInstalledState('installing');
|
setInstalledState('installing');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
|
client.get(`/api/v1/extension/install/${apkName}`)
|
||||||
|
.then(() => {
|
||||||
setInstalledState('uninstall');
|
setInstalledState('uninstall');
|
||||||
|
notifyInstall();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall() {
|
function uninstall() {
|
||||||
setInstalledState('uninstalling');
|
setInstalledState('uninstalling');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => {
|
client.get(`/api/v1/extension/uninstall/${apkName}`)
|
||||||
setInstalledState('install');
|
.then(() => {
|
||||||
|
// setInstalledState('install');
|
||||||
|
notifyInstall();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +91,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
variant="rounded"
|
variant="rounded"
|
||||||
className={classes.icon}
|
className={classes.icon}
|
||||||
alt={name}
|
alt={name}
|
||||||
src={iconUrl}
|
src={serverAddress + iconUrl}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/* 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, { 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 Switch from '@material-ui/core/Switch';
|
||||||
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
|
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||||
|
import { List, ListItemSecondaryAction, ListItemText } from '@material-ui/core';
|
||||||
|
import ListItem from '@material-ui/core/ListItem';
|
||||||
|
import { langCodeToName } from '../util/language';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => createStyles({
|
||||||
|
paper: {
|
||||||
|
maxHeight: 435,
|
||||||
|
width: '80%',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
shownLangs: string[]
|
||||||
|
setShownLangs: (arg0: string[]) => void
|
||||||
|
allLangs: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtensionLangSelect(props: IProps) {
|
||||||
|
const { shownLangs, setShownLangs, allLangs } = props;
|
||||||
|
// hold a copy and only sate state on parent when OK pressed, improves performance
|
||||||
|
const [mShownLangs, setMShownLangs] = useState(shownLangs);
|
||||||
|
const classes = useStyles();
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
setOpen(false);
|
||||||
|
setShownLangs(mShownLangs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, lang: string) => {
|
||||||
|
const { checked } = event.target as HTMLInputElement;
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
setMShownLangs([...mShownLangs, lang]);
|
||||||
|
} else {
|
||||||
|
const clone = JSON.parse(JSON.stringify(mShownLangs));
|
||||||
|
clone.splice(clone.indexOf(lang), 1);
|
||||||
|
setMShownLangs(clone);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label="display more actions"
|
||||||
|
edge="end"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<FilterListIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Dialog
|
||||||
|
classes={classes}
|
||||||
|
maxWidth="xs"
|
||||||
|
open={open}
|
||||||
|
>
|
||||||
|
<DialogTitle>Enabled Languages</DialogTitle>
|
||||||
|
<DialogContent dividers style={{ padding: 0 }}>
|
||||||
|
<List>
|
||||||
|
{allLangs.map((lang) => (
|
||||||
|
<ListItem key={lang}>
|
||||||
|
<ListItemText primary={langCodeToName(lang)} />
|
||||||
|
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Switch
|
||||||
|
checked={mShownLangs.indexOf(lang) !== -1}
|
||||||
|
onChange={(e) => handleChange(e, lang)}
|
||||||
|
/>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button autoFocus onClick={handleCancel} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleOk} color="primary">
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import CardMedia from '@material-ui/core/CardMedia';
|
|||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Grid } from '@material-ui/core';
|
import { Grid } from '@material-ui/core';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -51,6 +52,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
|
|||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={6} sm={4} md={3} lg={2}>
|
<Grid item xs={6} sm={4} md={3} lg={2}>
|
||||||
@@ -62,7 +64,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
|
|||||||
className={classes.image}
|
className={classes.image}
|
||||||
component="img"
|
component="img"
|
||||||
alt={title}
|
alt={title}
|
||||||
image={thumbnailUrl}
|
image={serverAddress + thumbnailUrl}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
<div className={classes.gradient} />
|
<div className={classes.gradient} />
|
||||||
|
|||||||
@@ -2,29 +2,43 @@
|
|||||||
* 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 { Button } from '@material-ui/core';
|
import { Button, createStyles, makeStyles } from '@material-ui/core';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import client from '../util/client';
|
||||||
|
import CategorySelect from './CategorySelect';
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => createStyles({
|
||||||
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
'& button': {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
manga: IManga
|
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>(
|
const [inLibrary, setInLibrary] = useState<string>(
|
||||||
manga.inLibrary ? 'In Library' : 'Not In Library',
|
manga.inLibrary ? 'In Library' : 'Not In Library',
|
||||||
);
|
);
|
||||||
|
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
function addToLibrary() {
|
function addToLibrary() {
|
||||||
setInLibrary('adding');
|
setInLibrary('adding');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`).then(() => {
|
client.get(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||||
setInLibrary('In Library');
|
setInLibrary('In Library');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFromLibrary() {
|
function removeFromLibrary() {
|
||||||
setInLibrary('removing');
|
setInLibrary('removing');
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${manga.id}/library/`, { method: 'DELETE', mode: 'cors' }).then(() => {
|
client.delete(`/api/v1/manga/${manga.id}/library/`).then(() => {
|
||||||
setInLibrary('Not In Library');
|
setInLibrary('Not In Library');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -38,13 +52,21 @@ export default function MangaDetails(props: IProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
{manga && manga.title}
|
{manga && manga.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
|
<div className={classes.root}>
|
||||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
|
<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>
|
</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/. */
|
||||||
@@ -14,7 +16,7 @@ import MenuItem from '@material-ui/core/MenuItem';
|
|||||||
import Menu from '@material-ui/core/Menu';
|
import Menu from '@material-ui/core/Menu';
|
||||||
|
|
||||||
import TemporaryDrawer from './TemporaryDrawer';
|
import TemporaryDrawer from './TemporaryDrawer';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavBarContext from '../context/NavbarContext';
|
||||||
import DarkTheme from '../context/DarkTheme';
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
@@ -42,10 +44,10 @@ export default function NavBar() {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const { title } = useContext(NavBarTitle);
|
const { title, action } = useContext(NavBarContext);
|
||||||
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,16 @@ export default function NavBar() {
|
|||||||
<Typography variant="h6" className={classes.title}>
|
<Typography variant="h6" className={classes.title}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton
|
{action}
|
||||||
|
{/* <IconButton
|
||||||
onClick={handleMenu}
|
onClick={handleMenu}
|
||||||
aria-label="display more actions"
|
aria-label="display more actions"
|
||||||
edge="end"
|
edge="end"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
<MoreIcon />
|
<FilterListIcon />
|
||||||
</IconButton>
|
</IconButton> */}
|
||||||
<Menu
|
{/* <Menu
|
||||||
id="menu-appbar"
|
id="menu-appbar"
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
@@ -107,7 +110,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} />
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
|
|||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
import { langCodeToName } from '../util/language';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -47,6 +49,8 @@ export default function SourceCard(props: IProps) {
|
|||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,14 +61,14 @@ export default function SourceCard(props: IProps) {
|
|||||||
variant="rounded"
|
variant="rounded"
|
||||||
className={classes.icon}
|
className={classes.icon}
|
||||||
alt={name}
|
alt={name}
|
||||||
src={iconUrl}
|
src={serverAddress + iconUrl}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
{name}
|
{name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" display="block" gutterBottom>
|
<Typography variant="caption" display="block" gutterBottom>
|
||||||
{lang}
|
{langCodeToName(lang)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,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>
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import React from 'react';
|
|||||||
type ContextType = {
|
type ContextType = {
|
||||||
title: string
|
title: string
|
||||||
setTitle: React.Dispatch<React.SetStateAction<string>>
|
setTitle: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
action: any
|
||||||
|
setAction: React.Dispatch<React.SetStateAction<any>>
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavBarTitle = React.createContext<ContextType>({
|
const NavBarContext = React.createContext<ContextType>({
|
||||||
title: 'Tachidesk',
|
title: 'Tachidesk',
|
||||||
setTitle: ():void => {},
|
setTitle: ():void => {},
|
||||||
|
action: <div />,
|
||||||
|
setAction: ():void => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default NavBarTitle;
|
export default NavBarContext;
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
// roboto font
|
// roboto font
|
||||||
import 'fontsource-roboto';
|
import 'fontsource-roboto';
|
||||||
@@ -16,8 +15,3 @@ ReactDOM.render(
|
|||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
/* 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 { ReportHandler } from 'web-vitals';
|
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
||||||
if (onPerfEntry) {
|
|
||||||
import('web-vitals').then(({
|
|
||||||
getCLS, getFID, getFCP, getLCP, getTTFB,
|
|
||||||
}) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
@@ -4,21 +4,97 @@
|
|||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import ExtensionCard from '../components/ExtensionCard';
|
import ExtensionCard from '../components/ExtensionCard';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
import ExtensionLangSelect from '../components/ExtensionLangSelect';
|
||||||
|
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language';
|
||||||
|
|
||||||
|
const allLangs: string[] = [];
|
||||||
|
|
||||||
|
function groupExtensions(extensions: IExtension[]) {
|
||||||
|
allLangs.length = 0; // empty the array
|
||||||
|
const result = { installed: [] } as any;
|
||||||
|
extensions.sort((a, b) => ((a.apkName > b.apkName) ? 1 : -1));
|
||||||
|
|
||||||
|
extensions.forEach((extension) => {
|
||||||
|
if (result[extension.lang] === undefined) {
|
||||||
|
result[extension.lang] = [];
|
||||||
|
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
|
||||||
|
}
|
||||||
|
if (extension.installed) {
|
||||||
|
result.installed.push(extension);
|
||||||
|
} else {
|
||||||
|
result[extension.lang].push(extension);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// put english first for convience
|
||||||
|
allLangs.sort(langSortCmp);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Extensions() {
|
export default function Extensions() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
setTitle('Extensions');
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
|
||||||
const [extensions, setExtensions] = useState<IExtension[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('http://127.0.0.1:4567/api/v1/extension/list')
|
setTitle('Extensions');
|
||||||
.then((response) => response.json())
|
setAction(
|
||||||
.then((data) => setExtensions(data));
|
<ExtensionLangSelect
|
||||||
}, []);
|
shownLangs={shownLangs}
|
||||||
|
setShownLangs={setShownLangs}
|
||||||
|
allLangs={allLangs}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}, [shownLangs]);
|
||||||
|
|
||||||
if (extensions.length === 0) {
|
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
|
||||||
return <h3>wait</h3>;
|
const [extensions, setExtensions] = useState<any>({});
|
||||||
|
|
||||||
|
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||||
|
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.get('/api/v1/extension/list')
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data) => setExtensionsRaw(data));
|
||||||
|
}, [updateTriggerHolder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (extensionsRaw.length > 0) {
|
||||||
|
const groupedExtension = groupExtensions(extensionsRaw);
|
||||||
|
setExtensions(groupedExtension);
|
||||||
}
|
}
|
||||||
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
|
}, [extensionsRaw]);
|
||||||
|
|
||||||
|
if (Object.entries(extensions).length === 0) {
|
||||||
|
return <h3>loading...</h3>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
Object.entries(extensions).map(([lang, list]) => (
|
||||||
|
(['installed', ...shownLangs].indexOf(lang) !== -1
|
||||||
|
&& (
|
||||||
|
<React.Fragment key={lang}>
|
||||||
|
<h1 key={lang} style={{ marginLeft: 25 }}>
|
||||||
|
{langCodeToName(lang)}
|
||||||
|
</h1>
|
||||||
|
{(list as IExtension[]).map((it) => (
|
||||||
|
<ExtensionCard
|
||||||
|
key={it.apkName}
|
||||||
|
extension={it}
|
||||||
|
notifyInstall={() => {
|
||||||
|
triggerUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/* 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>
|
|
||||||
Hint: Click Tn The Top Left Menu Button
|
|
||||||
</h1>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,33 +2,156 @@
|
|||||||
* 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 { Tab, Tabs } from '@material-ui/core';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
export default function MangaList() {
|
interface IMangaCategory {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
category: ICategory
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
mangas: IManga[]
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
isFetched: boolean
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
interface TabPanelProps {
|
||||||
setTitle('Library');
|
children: React.ReactNode;
|
||||||
}, []);
|
index: any;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
function TabPanel(props: TabPanelProps) {
|
||||||
fetch('http://127.0.0.1:4567/api/v1/library')
|
const {
|
||||||
.then((response) => response.json())
|
children, value, index,
|
||||||
.then((data: IManga[]) => {
|
} = props;
|
||||||
setMangas(data);
|
|
||||||
});
|
|
||||||
}, [lastPageNum]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
hidden={value !== index}
|
||||||
|
id={`simple-tabpanel-${index}`}
|
||||||
|
>
|
||||||
|
{value === index && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Library() {
|
||||||
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Library'); setAction(<></>); }, []);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const handleTabChange = (newTab: number) => {
|
||||||
|
setTabNum(newTab);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Promise.all<IManga[], ICategory[]>([
|
||||||
|
client.get('/api/v1/library').then((response) => response.data),
|
||||||
|
client.get('/api/v1/category').then((response) => response.data),
|
||||||
|
])
|
||||||
|
.then(
|
||||||
|
([libraryMangas, categories]) => {
|
||||||
|
const categoryTabs = categories.map((category) => ({
|
||||||
|
category,
|
||||||
|
mangas: [] as IManga[],
|
||||||
|
isFetched: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (libraryMangas.length > 0 || categoryTabs.length === 0) {
|
||||||
|
const defaultCategoryTab = {
|
||||||
|
category: {
|
||||||
|
name: 'Default',
|
||||||
|
isLanding: true,
|
||||||
|
order: 0,
|
||||||
|
id: -1,
|
||||||
|
},
|
||||||
|
mangas: libraryMangas,
|
||||||
|
isFetched: true,
|
||||||
|
};
|
||||||
|
setTabs(
|
||||||
|
[defaultCategoryTab, ...categoryTabs],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setTabs(categoryTabs);
|
||||||
|
setTabNum(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// console.log(client.defaults.baseURL);
|
||||||
|
// fetch the current tab
|
||||||
|
useEffect(() => {
|
||||||
|
tabs.forEach((tab, index) => {
|
||||||
|
if (tab.category.order === tabNum && !tab.isFetched) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
client.get(`/api/v1/category/${tab.category.id}`)
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data: IManga[]) => {
|
||||||
|
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
||||||
|
tabsClone[index].mangas = data;
|
||||||
|
tabsClone[index].isFetched = true;
|
||||||
|
|
||||||
|
setTabs(tabsClone); // clone the object
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [tabNum]);
|
||||||
|
|
||||||
|
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}
|
||||||
|
message={tab.isFetched ? 'Category is Empty' : 'Loading...'}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
));
|
||||||
|
|
||||||
|
// Visual Hack: 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
|
<MangaGrid
|
||||||
mangas={mangas}
|
mangas={mangas}
|
||||||
hasNextPage={false}
|
hasNextPage={false}
|
||||||
lastPageNum={lastPageNum}
|
lastPageNum={lastPageNum}
|
||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
|
message={tabs.length > 0 ? 'Library is Empty' : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return toRender;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ import React, { useEffect, useState, useContext } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import ChapterCard from '../components/ChapterCard';
|
import ChapterCard from '../components/ChapterCard';
|
||||||
import MangaDetails from '../components/MangaDetails';
|
import MangaDetails from '../components/MangaDetails';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
export default function Manga() {
|
export default function Manga() {
|
||||||
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { id } = useParams<{id: string}>();
|
const { id } = useParams<{id: string}>();
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
|
||||||
|
|
||||||
const [manga, setManga] = useState<IManga>();
|
const [manga, setManga] = useState<IManga>();
|
||||||
const [chapters, setChapters] = useState<IChapter[]>([]);
|
const [chapters, setChapters] = useState<IChapter[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
|
client.get(`/api/v1/manga/${id}/`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data: IManga) => {
|
.then((data: IManga) => {
|
||||||
setManga(data);
|
setManga(data);
|
||||||
setTitle(data.title);
|
setTitle(data.title);
|
||||||
@@ -25,8 +28,8 @@ export default function Manga() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/chapters`)
|
client.get(`/api/v1/manga/${id}/chapters`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data) => setChapters(data));
|
.then((data) => setChapters(data));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -17,14 +19,17 @@ const style = {
|
|||||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||||
|
|
||||||
export default function Reader() {
|
export default function Reader() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Reader'); setAction(<></>); }, []);
|
||||||
|
|
||||||
|
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
|
||||||
const [pageCount, setPageCount] = useState<number>(-1);
|
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}`)
|
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data:IChapter) => {
|
.then((data:IChapter) => {
|
||||||
setTitle(data.name);
|
setTitle(data.name);
|
||||||
setPageCount(data.pageCount);
|
setPageCount(data.pageCount);
|
||||||
@@ -41,7 +46,7 @@ export default function Reader() {
|
|||||||
|
|
||||||
const mapped = range(pageCount).map((index) => (
|
const mapped = range(pageCount).map((index) => (
|
||||||
<div style={{ margin: '0 auto' }}>
|
<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%' }} />
|
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import TextField from '@material-ui/core/TextField';
|
|||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -20,7 +21,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function SearchSingle() {
|
export default function SearchSingle() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
@@ -33,8 +36,8 @@ export default function SearchSingle() {
|
|||||||
const textInput = React.createRef<HTMLInputElement>();
|
const textInput = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
|
client.get(`/api/v1/source/${sourceId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
|
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -54,8 +57,8 @@ export default function SearchSingle() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchTerm.length > 0) {
|
if (searchTerm.length > 0) {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
|
client.get(`/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||||
if (data.mangaList.length > 0) {
|
if (data.mangaList.length > 0) {
|
||||||
setMangas([
|
setMangas([
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/* 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, useEffect, useState } from 'react';
|
||||||
|
import List from '@material-ui/core/List';
|
||||||
|
import InboxIcon from '@material-ui/icons/Inbox';
|
||||||
|
import Brightness6Icon from '@material-ui/icons/Brightness6';
|
||||||
|
import DnsIcon from '@material-ui/icons/Dns';
|
||||||
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
|
import {
|
||||||
|
Button, Dialog, DialogActions, DialogContent,
|
||||||
|
DialogContentText, IconButton, ListItemSecondaryAction, Switch, TextField,
|
||||||
|
ListItemIcon, ListItemText,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
|
||||||
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import DarkTheme from '../context/DarkTheme';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
|
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, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Settings'); setAction(<></>); }, []);
|
||||||
|
|
||||||
|
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||||
|
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [dialogValue, setDialogValue] = useState(serverAddress);
|
||||||
|
|
||||||
|
const handleDialogOpen = () => {
|
||||||
|
setDialogValue(serverAddress);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogCancel = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setServerAddress(dialogValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List 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>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<DnsIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="Server Address" secondary={serverAddress} />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handleDialogOpen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||||
|
<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">
|
||||||
|
Set
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,25 +5,28 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import MangaGrid from '../components/MangaGrid';
|
import MangaGrid from '../components/MangaGrid';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
|
||||||
|
export default function SourceMangas(props: { popular: boolean }) {
|
||||||
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Source'); setAction(<></>); }, []);
|
||||||
|
|
||||||
export default function MangaList(props: { popular: boolean }) {
|
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
|
||||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||||
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
|
||||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
|
client.get(`/api/v1/source/${sourceId}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data: { name: string }) => setTitle(data.name));
|
.then((data: { name: string }) => setTitle(data.name));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sourceType = props.popular ? 'popular' : 'latest';
|
const sourceType = props.popular ? 'popular' : 'latest';
|
||||||
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
client.get(`/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.data)
|
||||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||||
setMangas([
|
setMangas([
|
||||||
...mangas,
|
...mangas,
|
||||||
@@ -3,22 +3,79 @@
|
|||||||
* 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, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import ExtensionLangSelect from '../components/ExtensionLangSelect';
|
||||||
import SourceCard from '../components/SourceCard';
|
import SourceCard from '../components/SourceCard';
|
||||||
import NavBarTitle from '../context/NavbarTitle';
|
import NavbarContext from '../context/NavbarContext';
|
||||||
|
import client from '../util/client';
|
||||||
|
import { defualtLangs, langCodeToName, langSortCmp } from '../util/language';
|
||||||
|
import useLocalStorage from '../util/useLocalStorage';
|
||||||
|
|
||||||
|
function sourceToLangList(sources: ISource[]) {
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
sources.forEach((source) => {
|
||||||
|
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
||||||
|
});
|
||||||
|
|
||||||
|
result.sort(langSortCmp);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByLang(sources: ISource[]) {
|
||||||
|
const result = {} as any;
|
||||||
|
sources.forEach((source) => {
|
||||||
|
if (result[source.lang] === undefined) { result[source.lang] = [] as ISource[]; }
|
||||||
|
result[source.lang].push(source);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Sources() {
|
export default function Sources() {
|
||||||
const { setTitle } = useContext(NavBarTitle);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
setTitle('Sources');
|
|
||||||
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', defualtLangs());
|
||||||
|
|
||||||
const [sources, setSources] = useState<ISource[]>([]);
|
const [sources, setSources] = useState<ISource[]>([]);
|
||||||
|
const [fetched, setFetched] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('http://127.0.0.1:4567/api/v1/source/list')
|
setTitle('Sources');
|
||||||
.then((response) => response.json())
|
setAction(
|
||||||
.then((data) => setSources(data));
|
<ExtensionLangSelect
|
||||||
|
shownLangs={shownLangs}
|
||||||
|
setShownLangs={setShownLangs}
|
||||||
|
allLangs={sourceToLangList(sources)}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}, [shownLangs, sources]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.get('/api/v1/source/list')
|
||||||
|
.then((response) => response.data)
|
||||||
|
.then((data) => { setSources(data); setFetched(true); });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (sources.length === 0) {
|
if (sources.length === 0) {
|
||||||
return (<h3>wait</h3>);
|
if (fetched) return (<h3>No sources found. Install Some Extensions first.</h3>);
|
||||||
|
return (<h3>loading...</h3>);
|
||||||
}
|
}
|
||||||
return <>{sources.map((it) => <SourceCard source={it} />)}</>;
|
return (
|
||||||
|
<>
|
||||||
|
{/* eslint-disable-next-line max-len */}
|
||||||
|
{Object.entries(groupByLang(sources)).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => (
|
||||||
|
shownLangs.indexOf(lang) !== -1 && (
|
||||||
|
<React.Fragment key={lang}>
|
||||||
|
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
||||||
|
{(list as ISource[]).map((source) => (
|
||||||
|
<SourceCard
|
||||||
|
key={source.id}
|
||||||
|
source={source}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/* 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 NavbarContext from '../../context/NavbarContext';
|
||||||
|
import client from '../../util/client';
|
||||||
|
|
||||||
|
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, setAction } = useContext(NavbarContext);
|
||||||
|
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
||||||
|
|
||||||
|
const [categories, setCategories] = useState([]);
|
||||||
|
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
||||||
|
const [dialogOpen, setDialogOpen] = 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) {
|
||||||
|
client.get('/api/v1/category/')
|
||||||
|
.then((response) => response.data)
|
||||||
|
.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);
|
||||||
|
client.post(`/api/v1/category/${category.id}/reorder`, 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 resetDialog = () => {
|
||||||
|
setDialogValue('');
|
||||||
|
setCategoryToEdit(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogOpen = () => {
|
||||||
|
resetDialog();
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogCancel = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDialogSubmit = () => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', dialogValue);
|
||||||
|
|
||||||
|
if (categoryToEdit === -1) {
|
||||||
|
client.post('/api/v1/category/', formData)
|
||||||
|
.finally(() => triggerUpdate());
|
||||||
|
} else {
|
||||||
|
const category = categories[categoryToEdit];
|
||||||
|
client.patch(`/api/v1/category/${category.id}`, formData)
|
||||||
|
.finally(() => triggerUpdate());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCategory = (index) => {
|
||||||
|
const category = categories[index];
|
||||||
|
client.delete(`/api/v1/category/${category.id}`)
|
||||||
|
.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={() => {
|
||||||
|
handleDialogOpen();
|
||||||
|
setCategoryToEdit(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,3 +38,10 @@ interface IChapter {
|
|||||||
mangaId: number
|
mangaId: number
|
||||||
pageCount: number
|
pageCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICategory {
|
||||||
|
id: number
|
||||||
|
order: number
|
||||||
|
name: String
|
||||||
|
isLanding: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* 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 axios from 'axios';
|
||||||
|
import storage from './localStorage';
|
||||||
|
|
||||||
|
const { hostname, port, protocol } = window.location;
|
||||||
|
|
||||||
|
// if port is 3000 it's probably running from webpack devlopment server
|
||||||
|
let inferredPort;
|
||||||
|
if (port === '3000') { inferredPort = '4567'; } else { inferredPort = port; }
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
// baseURL must not have traling slash
|
||||||
|
baseURL: storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.interceptors.request.use((config) => {
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
Object.assign(config.headers, { 'Content-Type': 'multipart/form-data' });
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default client;
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
export const ISOLanguages = [
|
||||||
|
{ code: 'all', name: 'All', nativeName: 'All' },
|
||||||
|
{ code: 'installed', name: 'Installed', nativeName: 'Installed' },
|
||||||
|
|
||||||
|
// full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js
|
||||||
|
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||||
|
{ code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' },
|
||||||
|
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||||
|
{ code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' },
|
||||||
|
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||||
|
{ code: 'id', name: 'Indonesian', nativeName: 'Indonesia' },
|
||||||
|
{ code: 'it', name: 'Italian', nativeName: 'Italiano' },
|
||||||
|
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
|
||||||
|
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
|
||||||
|
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
|
||||||
|
{ code: 'ru', name: 'Russian', nativeName: 'русский' },
|
||||||
|
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
|
||||||
|
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
|
||||||
|
{ code: 'th', name: 'Thai', nativeName: 'ไทย' },
|
||||||
|
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
|
||||||
|
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
|
||||||
|
{ code: 'ko', name: 'Korean', nativeName: '한국어' },
|
||||||
|
{ code: 'zu', name: 'Zulu', nativeName: 'isiZulu' },
|
||||||
|
{ code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' },
|
||||||
|
{ code: 'uk', name: 'Ukrainian', nativeName: 'Українська' },
|
||||||
|
{ code: 'ro', name: 'Romanian', nativeName: 'Română' },
|
||||||
|
{ code: 'bg', name: 'Bulgarian', nativeName: 'български' },
|
||||||
|
{ code: 'cs', name: 'Czech', nativeName: 'čeština' },
|
||||||
|
{ code: 'pl', name: 'Polish', nativeName: 'polski' },
|
||||||
|
{ code: 'no', name: 'Norwegian', nativeName: 'Norsk' },
|
||||||
|
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
|
||||||
|
{ code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' },
|
||||||
|
{ code: 'ms', name: 'Malay', nativeName: 'Malaysia' },
|
||||||
|
{ code: 'mn', name: 'Mongolian', nativeName: 'Монгол' },
|
||||||
|
{ code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' },
|
||||||
|
{ code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' },
|
||||||
|
{ code: 'hu', name: 'Hungarian', nativeName: 'Magyar' },
|
||||||
|
{ code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' },
|
||||||
|
{ code: 'he', name: 'Hebrew', nativeName: 'עברית' },
|
||||||
|
{ code: 'fil', name: 'Filipino', nativeName: 'Filipino' },
|
||||||
|
{ code: 'fi', name: 'Finnish', nativeName: 'suomi' },
|
||||||
|
{ code: 'fa', name: 'Persian', nativeName: 'فارسی' },
|
||||||
|
{ code: 'eu', name: 'Basque', nativeName: 'euskara' },
|
||||||
|
{ code: 'el', name: 'Greek', nativeName: 'Ελληνικά' },
|
||||||
|
{ code: 'da', name: 'Danish', nativeName: 'dansk' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function langCodeToName(code: string): string {
|
||||||
|
const whereToCut = code.indexOf('-') !== -1 ? code.indexOf('-') : code.length;
|
||||||
|
|
||||||
|
const proccessedCode = code.toLocaleLowerCase().substring(0, whereToCut);
|
||||||
|
let result = 'Error';
|
||||||
|
|
||||||
|
for (let i = 0; i < ISOLanguages.length; i++) {
|
||||||
|
if (ISOLanguages[i].code === proccessedCode) result = ISOLanguages[i].nativeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code.indexOf('-') !== -1) {
|
||||||
|
result = `${result} (${code.substring(whereToCut + 1)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defualtLangs() {
|
||||||
|
return [
|
||||||
|
// todo: infer this from the browser
|
||||||
|
'en',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const langSortCmp = (a: string, b: string) => {
|
||||||
|
// puts english first for convience
|
||||||
|
const aLang = langCodeToName(a);
|
||||||
|
const bLang = langCodeToName(b);
|
||||||
|
|
||||||
|
if (a === 'en') return -1;
|
||||||
|
if (b === 'en') return 1;
|
||||||
|
return aLang > bLang ? 1 : -1;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
function getItem<T>(key: string, defaultValue: T) : T {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (item !== null) {
|
||||||
|
return JSON.parse(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(defaultValue));
|
||||||
|
|
||||||
|
/* eslint-disable no-empty */
|
||||||
|
} finally { }
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItem<T>(key: string, value: T): void {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
} finally { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { getItem, setItem };
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/* 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 { useState, Dispatch, SetStateAction } from 'react';
|
||||||
|
import storage from './localStorage';
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
export default function useLocalStorage<T>(key: string, defaultValue: T) : [T, Dispatch<SetStateAction<T>>] {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(storage.getItem(key, defaultValue));
|
||||||
|
|
||||||
|
const setValue = (value: T | ((prevState: T) => T)) => {
|
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
storage.setItem(key, valueToStore);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [storedValue, setValue];
|
||||||
|
}
|
||||||
@@ -2625,6 +2625,13 @@ axe-core@^4.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf"
|
||||||
integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==
|
integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==
|
||||||
|
|
||||||
|
axios@^0.21.1:
|
||||||
|
version "0.21.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||||
|
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.10.0"
|
||||||
|
|
||||||
axobject-query@^2.2.0:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||||
@@ -3744,6 +3751,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"
|
||||||
@@ -5226,6 +5240,11 @@ follow-redirects@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||||
|
|
||||||
|
follow-redirects@^1.10.0:
|
||||||
|
version "1.13.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
|
||||||
|
integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
|
||||||
|
|
||||||
fontsource-roboto@^4.0.0:
|
fontsource-roboto@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fontsource-roboto/-/fontsource-roboto-4.0.0.tgz#35eacd4fb8d90199053c0eec9b34a57fb79cd820"
|
resolved "https://registry.yarnpkg.com/fontsource-roboto/-/fontsource-roboto-4.0.0.tgz#35eacd4fb8d90199053c0eec9b34a57fb79cd820"
|
||||||
@@ -7344,6 +7363,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 +9232,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 +9286,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 +9343,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 +9353,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 +9571,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 +10733,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 +10884,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 +11237,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"
|
||||||
|
|||||||