Compare commits

..

32 Commits

Author SHA1 Message Date
Aria Moradi 92ed48f7f6 bump version to v0.2.4
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 15s
2021-03-13 11:08:39 +03:30
Aria Moradi 13e84bc492 Maskable icons 2021-03-13 11:06:22 +03:30
Aria Moradi 0ef86c34b7 server configuration fam 2021-03-11 14:43:29 +03:30
Aria Moradi 7e1a4259d7 fix langs not showing correctly 2021-03-09 18:05:34 +03:30
Aria Moradi c842c51fb6 section sources by lang 2021-03-09 16:44:09 +03:30
Aria Moradi 6f2f228e08 section extension languages 2021-03-08 21:04:42 +03:30
Aria Moradi c78eaa8b96 add issue closer 2021-03-08 13:47:58 +03:30
Aria Moradi f9606526d2 add issue closer 2021-03-08 13:39:25 +03:30
Aria Moradi fe4cc9ea2c add issue closer 2021-03-08 13:32:17 +03:30
Aria Moradi 54d0c05fcc add issue closer 2021-03-08 13:31:03 +03:30
Aria Moradi 2f7df73a37 add issue closer 2021-03-08 13:22:44 +03:30
Aria Moradi cf19f3626b improve text 2021-03-08 13:01:23 +03:30
Aria Moradi ff2da5e59b issue template 2021-03-08 12:57:12 +03:30
Aria Moradi e03922e518 fix PWA icons 2021-03-07 23:08:30 +03:30
Aria Moradi 893fba5b8c fix image urls 2021-03-07 22:35:27 +03:30
Aria Moradi c1786f8e24 migrate to axios, front-end part of configurable ServerAddress 2021-03-07 22:25:29 +03:30
Aria Moradi a59f974537 fix #25 2021-03-07 22:12:38 +03:30
Aria Moradi 7157e07328 better messages, axios client 2021-03-07 16:27:13 +03:30
Aria Moradi 954084bd82 Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-03-07 10:51:24 +03:30
Aria Moradi 0915ba40f6 🤌 Tachidesk's logo! 2021-02-25 21:54:49 +03:30
Aria Moradi de30d55bcf darkTheme in localStorage 2021-02-25 14:38:16 +03:30
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
61 changed files with 1016 additions and 403 deletions
+43
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+29
View File
@@ -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
+32
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
dependencies {
// Config API
// Config API, moved to the global build.gradle
// 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.ConfigRenderOptions
import mu.KotlinLogging
import net.harawata.appdirs.AppDirsFactory
import java.io.File
/**
* Manages app config.
*/
open class ConfigManager {
private val generatedModules
= mutableMapOf<Class<out ConfigModule>, ConfigModule>()
private val dataRoot by lazy { AppDirsFactory.getInstance().getUserDataDir("Tachidesk", null, null)!! }
private val generatedModules = mutableMapOf<Class<out ConfigModule>, ConfigModule>()
val config by lazy { loadConfigs() }
//Public read-only view of modules
val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules
open val configFolder: String
get() = System.getProperty("compat-configdirs") ?: "tachiserver-data/config"
open val appConfigFile: String = "$dataRoot/server.conf"
val logger = KotlinLogging.logger {}
/**
* Get a config module
*/
inline fun <reified T : ConfigModule> module(): T
= loadedModules[T::class.java] as T
inline fun <reified T : ConfigModule> module(): T = loadedModules[T::class.java] as T
/**
* Get a config module (Java API)
*/
fun <T : ConfigModule> module(type: Class<T>): T
= loadedModules[type] as T
fun <T : ConfigModule> module(type: Class<T>): T = loadedModules[type] as T
/**
* Load configs
*/
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
configs += ConfigFactory.parseResources("reference.conf")
//Load user config
val userConfig =
File(appConfigFile).let{
ConfigFactory.parseFile(it)
}
//Load custom configs from dir
File(configFolder).listFiles()?.map {
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 {
"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)
ts.server.allowConfigChanges = true
+35 -16
View File
@@ -1,3 +1,5 @@
![image](https://github.com/AriaMoradi/Tachidesk/raw/master/server/src/main/resources/icon/faviconlogo.png)
# Tachidesk
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.
## 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
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
Check [arbuilder's repo](https://github.com/arbuilder/Tachidesk-docker) out for more details and the dockerfile.
## General troubleshooting
If the app breaks try deleting the directory below and re-running the app (**This will delete all your data!**) and if the problem persists open an issue.
On Mac OS X : `/Users/<Account>/Library/Application Support/Tachidesk`
On Windows XP : `C:\Documents and Settings\<Account>\Application Data\Local Settings\Tachidesk`
On Windows 7 and later : `C:\Users\<Account>\AppData\Tachidesk`
On Unix/Linux : `/home/<account>/.local/share/Tachidesk`
## Support and help
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support and help.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Building from source
### Get Android stubs jar
#### Manual download
@@ -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
and supports HMR and all the other goodies you'll need.
## Is this application usable? Should I test it?
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
## How does it work?
This project has two components:
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
2. **webUI:** A react SPA project that works with the server to do the presentation.
## Support
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
## Credit
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
+3
View File
@@ -76,5 +76,8 @@ configure(listOf(
// dependency of :AndroidCompat:Config
implementation("com.typesafe:config:1.4.0")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
}
}
+2 -5
View File
@@ -9,7 +9,7 @@ plugins {
id("edu.sc.seis.launch4j") version "2.4.9"
}
val TachideskVersion = "v0.2.2"
val TachideskVersion = "v0.2.4"
repositories {
@@ -72,9 +72,6 @@ dependencies {
implementation("org.slf4j:slf4j-api:1.8.0-beta4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.10.3")
// to get application content root
implementation("net.harawata:appdirs:1.2.0")
// Exposed ORM
val exposed_version = "0.28.1"
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
@@ -149,7 +146,7 @@ launch4j { //used for windows
bundledJre64Bit = true
jreMinVersion = "8"
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"
}
@@ -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
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.App
import io.javalin.Javalin
import ir.armor.tachidesk.util.addMangaToCategory
import ir.armor.tachidesk.util.addMangaToLibrary
import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.createCategory
import ir.armor.tachidesk.util.getCategoryList
import ir.armor.tachidesk.util.getCategoryMangaList
@@ -34,44 +32,13 @@ import ir.armor.tachidesk.util.reorderCategory
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.systemTray
import ir.armor.tachidesk.util.updateCategory
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager
class Main {
companion object {
val androidCompat by lazy { AndroidCompat() }
fun registerConfigModules() {
GlobalConfigManager.registerModules(
// ServerConfig.register(GlobalConfigManager.config),
// SyncConfigModule.register(GlobalConfigManager.config)
)
}
@JvmStatic
fun main(args: Array<String>) {
// System.getProperties()["proxySet"] = "true"
// System.getProperties()["socksProxyHost"] = "127.0.0.1"
// System.getProperties()["socksProxyPort"] = "2020"
// make sure everything we need exists
applicationSetup()
val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
registerConfigModules()
// Load config API
DI.global.addImport(ConfigKodeinModule().create())
// Load Android compatibility dependencies
AndroidCompatInitializer().init()
// start app
androidCompat.startApp(App())
serverSetup()
var hasWebUiBundled: Boolean = false
@@ -86,16 +53,11 @@ class Main {
hasWebUiBundled = false
}
config.enableCorsForAllOrigins()
}.start(4567)
}.start(serverConfig.ip, serverConfig.port)
if (hasWebUiBundled) {
openInBrowser()
}
// app.before() { ctx ->
// // allow the client which is running on another port
// ctx.header("Access-Control-Allow-Origin", "*")
// }
app.get("/api/v1/extension/list") { ctx ->
ctx.json(getExtensionList())
}
@@ -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
* 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.CategoryTable
import ir.armor.tachidesk.database.table.ChapterTable
@@ -18,15 +18,14 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
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() {
// mention db object to connect
DBMangaer.db
// val db = DBMangaer.db
// db.useNestedTransactions = true
// must mention db object so the lazy block executes
val db = DBMangaer.db
db.useNestedTransactions = true
transaction {
SchemaUtils.createMissingTablesAndColumns(
@@ -1,6 +1,7 @@
package ir.armor.tachidesk.util
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.toDataClass
import org.jetbrains.exposed.sql.SortOrder
@@ -49,6 +50,9 @@ fun reorderCategory(categoryId: Int, from: Int, to: Int) {
fun removeCategory(categoryId: Int) {
transaction {
CategoryMangaTable.select { CategoryMangaTable.category eq categoryId }.forEach {
removeMangaFromCategory(it[CategoryMangaTable.manga].value, categoryId)
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
}
}
@@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.APKExtractor
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ExtensionTable
import ir.armor.tachidesk.database.table.SourceTable
import kotlinx.coroutines.runBlocking
@@ -32,10 +32,10 @@ import java.net.URLClassLoader
fun installAPK(apkName: String): Int {
val extensionRecord = getExtensionList(true).first { it.apkName == apkName }
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
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
if (!File(jarPath).exists()) {
runBlocking {
val api = ExtensionGithubApi()
@@ -137,7 +137,7 @@ private fun downloadAPKFile(url: String, apkPath: String) {
fun removeExtension(pkgName: String) {
val extensionRecord = getExtensionList(true).first { it.apkName == pkgName }
val fileNameWithoutType = pkgName.substringBefore(".apk")
val jarPath = "${Config.extensionsRoot}/$fileNameWithoutType.jar"
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
transaction {
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> {
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
return getCachedResponse(saveDir, fileName) {
@@ -168,5 +168,5 @@ fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
}
fun getExtensionIconUrl(apkName: String): String {
return "http://127.0.0.1:4567/api/v1/extension/icon/$apkName"
return "/api/v1/extension/icon/$apkName"
}
@@ -6,7 +6,7 @@ package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.network.GET
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.table.MangaStatus
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> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = Config.thumbnailsRoot
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
return getCachedResponse(saveDir, fileName) {
@@ -14,7 +14,7 @@ import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
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 {
@@ -6,12 +6,11 @@ package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.applicationDirs
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -70,7 +69,7 @@ fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
val mangaDir = "${applicationDirs.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
@@ -6,7 +6,7 @@ package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.SourceFactory
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.entity.ExtensionEntity
import ir.armor.tachidesk.database.entity.SourceEntity
@@ -36,7 +36,7 @@ fun getHttpSource(sourceId: Long): HttpSource {
val apkName = extensionRecord.apkName
val className = extensionRecord.classFQName
val jarName = apkName.substringBefore(".apk") + ".jar"
val jarPath = "${Config.extensionsRoot}/$jarName"
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
println(jarName)
@@ -77,7 +77,7 @@ fun getSourceList(): List<SourceDataClass> {
SourceDataClass(
it[SourceTable.id].value.toString(),
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]),
getHttpSource(it[SourceTable.id].value).supportsLatest
)
@@ -9,63 +9,59 @@ import dorkbox.systemTray.SystemTray
import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import java.awt.event.ActionListener
import java.io.File
import java.io.IOException
fun applicationSetup() {
// make dirs we need
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
File("${Config.extensionsRoot}/icon").mkdirs()
File(Config.thumbnailsRoot).mkdirs()
makeDataBaseTables()
}
fun openInBrowser() {
Desktop.browseURL("http://127.0.0.1:4567")
try {
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? {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e: IOException) {
e.printStackTrace()
}
}
}
)
)
)
val icon = Main::class.java.getResource("/icon/faviconlogo.png")
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
return systemTray
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

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 = ""
+2
View File
@@ -13,5 +13,7 @@ module.exports = {
// Indent props with 4 spaces
'react/jsx-indent-props': ['error', 4],
'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }]
},
};
+1
View File
@@ -8,6 +8,7 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.21.1",
"fontsource-roboto": "^4.0.0",
"react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+3 -3
View File
@@ -7,9 +7,9 @@
<meta name="theme-color" content="#000000"/>
<meta
name="description"
content="Web site created using create-react-app"
content="A manga reader that runs tachiyomi's extensions"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png"/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Tachidesk</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

+12 -6
View File
@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Tachidesk",
"name": "Tachidesk",
"icons": [
{
"src": "favicon.ico",
@@ -8,18 +8,24 @@
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "favicon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "favicon.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
"theme_color": "#ff2323",
"background_color": "#ff2323"
}
+9 -5
View File
@@ -17,16 +17,20 @@ import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga';
import Reader from './screens/Reader';
import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle';
import NavbarContext from './context/NavbarContext';
import DarkTheme from './context/DarkTheme';
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() {
const [title, setTitle] = useState<string>('Tachidesk');
const [darkTheme, setDarkTheme] = useState<boolean>(true);
const navTitleContext = { title, setTitle };
const [action, setAction] = useState<any>(<div />);
const [darkTheme, setDarkTheme] = useLocalStorage<boolean>('darkTheme', true);
const navBarContext = {
title, setTitle, action, setAction,
};
const darkThemeContext = { darkTheme, setDarkTheme };
const theme = React.useMemo(
@@ -56,7 +60,7 @@ export default function App() {
return (
<Router>
<ThemeProvider theme={theme}>
<NavBarTitle.Provider value={navTitleContext}>
<NavbarContext.Provider value={navBarContext}>
<CssBaseline />
<NavBar />
<Container maxWidth={false} disableGutters>
@@ -102,7 +106,7 @@ export default function App() {
/>
</Switch>
</Container>
</NavBarTitle.Provider>
</NavbarContext.Provider>
</ThemeProvider>
</Router>
);
@@ -12,6 +12,7 @@ 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: {
@@ -41,14 +42,14 @@ export default function CategorySelect(props: IProps) {
useEffect(() => {
let tmpCategoryInfos: ICategoryInfo[] = [];
fetch('http://127.0.0.1:4567/api/v1/category/')
.then((response) => response.json())
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data: ICategory[]) => {
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
})
.then(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`)
.then((response) => response.json())
client.get(`/api/v1/manga/${mangaId}/category/`)
.then((response) => response.data)
.then((data: ICategory[]) => {
data.forEach((category) => {
tmpCategoryInfos[category.order - 1].selected = true;
@@ -69,9 +70,9 @@ export default function CategorySelect(props: IProps) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
const { checked } = event.target as HTMLInputElement;
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, {
method: checked ? 'GET' : 'DELETE', mode: 'cors',
})
const method = checked ? client.get : client.delete;
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
.then(() => triggerUpdate());
};
+17 -7
View File
@@ -9,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import client from '../util/client';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles((theme) => ({
root: {
@@ -38,6 +40,7 @@ const useStyles = makeStyles((theme) => ({
interface IProps {
extension: IExtension
notifyInstall: () => void
}
export default function ExtensionCard(props: IProps) {
@@ -45,24 +48,31 @@ export default function ExtensionCard(props: IProps) {
extension: {
name, lang, versionName, installed, apkName, iconUrl,
},
notifyInstall,
} = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
const langPress = lang === 'all' ? 'All' : lang.toUpperCase();
function install() {
setInstalledState('installing');
fetch(`http://127.0.0.1:4567/api/v1/extension/install/${apkName}`).then(() => {
setInstalledState('uninstall');
});
client.get(`/api/v1/extension/install/${apkName}`)
.then(() => {
setInstalledState('uninstall');
notifyInstall();
});
}
function uninstall() {
setInstalledState('uninstalling');
fetch(`http://127.0.0.1:4567/api/v1/extension/uninstall/${apkName}`).then(() => {
setInstalledState('install');
});
client.get(`/api/v1/extension/uninstall/${apkName}`)
.then(() => {
// setInstalledState('install');
notifyInstall();
});
}
function handleButtonClick() {
@@ -81,7 +91,7 @@ export default function ExtensionCard(props: IProps) {
variant="rounded"
className={classes.icon}
alt={name}
src={iconUrl}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<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>
</>
);
}
+3 -1
View File
@@ -10,6 +10,7 @@ import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
import useLocalStorage from '../util/useLocalStorage';
const useStyles = makeStyles({
root: {
@@ -51,6 +52,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
},
} = props;
const classes = useStyles();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
return (
<Grid item xs={6} sm={4} md={3} lg={2}>
@@ -62,7 +64,7 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
className={classes.image}
component="img"
alt={title}
image={thumbnailUrl}
image={serverAddress + thumbnailUrl}
title={title}
/>
<div className={classes.gradient} />
+3 -2
View File
@@ -4,6 +4,7 @@
import { Button, createStyles, makeStyles } from '@material-ui/core';
import React, { useState } from 'react';
import client from '../util/client';
import CategorySelect from './CategorySelect';
const useStyles = makeStyles(() => createStyles({
@@ -30,14 +31,14 @@ export default function MangaDetails(props: IProps) {
function addToLibrary() {
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');
});
}
function removeFromLibrary() {
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');
});
}
+4 -3
View File
@@ -16,7 +16,7 @@ import MenuItem from '@material-ui/core/MenuItem';
import Menu from '@material-ui/core/Menu';
import TemporaryDrawer from './TemporaryDrawer';
import NavBarTitle from '../context/NavbarTitle';
import NavBarContext from '../context/NavbarContext';
import DarkTheme from '../context/DarkTheme';
const useStyles = makeStyles((theme) => ({
@@ -44,7 +44,7 @@ export default function NavBar() {
const classes = useStyles();
const [drawerOpen, setDrawerOpen] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const { title } = useContext(NavBarTitle);
const { title, action } = useContext(NavBarContext);
const open = Boolean(anchorEl);
const { darkTheme } = useContext(DarkTheme);
@@ -74,13 +74,14 @@ export default function NavBar() {
<Typography variant="h6" className={classes.title}>
{title}
</Typography>
{action}
{/* <IconButton
onClick={handleMenu}
aria-label="display more actions"
edge="end"
color="inherit"
>
<MoreIcon />
<FilterListIcon />
</IconButton> */}
{/* <Menu
id="menu-appbar"
+6 -2
View File
@@ -9,6 +9,8 @@ import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import useLocalStorage from '../util/useLocalStorage';
import { langCodeToName } from '../util/language';
const useStyles = makeStyles((theme) => ({
root: {
@@ -47,6 +49,8 @@ export default function SourceCard(props: IProps) {
},
} = props;
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const classes = useStyles();
return (
@@ -57,14 +61,14 @@ export default function SourceCard(props: IProps) {
variant="rounded"
className={classes.icon}
alt={name}
src={iconUrl}
src={serverAddress + iconUrl}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{lang}
{langCodeToName(lang)}
</Typography>
</div>
</div>
@@ -7,11 +7,15 @@ import React from 'react';
type ContextType = {
title: 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',
setTitle: ():void => {},
action: <div />,
setAction: ():void => {},
});
export default NavBarTitle;
export default NavBarContext;
-6
View File
@@ -5,7 +5,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
// roboto font
import 'fontsource-roboto';
@@ -16,8 +15,3 @@ ReactDOM.render(
</React.StrictMode>,
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();
-21
View File
@@ -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;
+87 -11
View File
@@ -4,21 +4,97 @@
import React, { useContext, useEffect, useState } from 'react';
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() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Extensions');
const [extensions, setExtensions] = useState<IExtension[]>([]);
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', defualtLangs());
useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/extension/list')
.then((response) => response.json())
.then((data) => setExtensions(data));
}, []);
setTitle('Extensions');
setAction(
<ExtensionLangSelect
shownLangs={shownLangs}
setShownLangs={setShownLangs}
allLangs={allLangs}
/>,
);
}, [shownLangs]);
if (extensions.length === 0) {
return <h3>wait</h3>;
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
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);
}
}, [extensionsRaw]);
if (Object.entries(extensions).length === 0) {
return <h3>loading...</h3>;
}
return <>{extensions.map((it) => <ExtensionCard extension={it} />)}</>;
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>
))
))
}
</>
);
}
+56 -56
View File
@@ -5,11 +5,13 @@
import { Tab, Tabs } from '@material-ui/core';
import React, { useContext, useEffect, useState } from 'react';
import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
interface IMangaCategory {
category: ICategory
mangas: IManga[]
isFetched: boolean
}
interface TabPanelProps {
@@ -35,78 +37,74 @@ function TabPanel(props: TabPanelProps) {
}
export default function Library() {
const { setTitle } = useContext(NavBarTitle);
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);
useEffect(() => {
setTitle('Library');
}, []);
// eslint-disable-next-line @typescript-eslint/no-shadow
const fetchAndSetMangas = (tabs: IMangaCategory[], tab: IMangaCategory, index: number) => {
fetch(`http://127.0.0.1:4567/api/v1/category/${tab.category.id}`)
.then((response) => response.json())
.then((data: IManga[]) => {
const tabsClone = JSON.parse(JSON.stringify(tabs));
tabsClone[index].mangas = data;
setTabs(tabsClone); // clone the object
});
};
const handleTabChange = (newTab: number) => {
setTabNum(newTab);
tabs.forEach((tab, index) => {
if (tab.category.order === newTab && tab.mangas.length === 0) {
// mangas are empty, fetch the mangas
fetchAndSetMangas(tabs, tab, index);
}
});
};
useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/library')
.then((response) => response.json())
.then((data: IManga[]) => {
// if some manga with no category exist, they will be added under a virtual category
if (data.length > 0) {
return [
{
category: {
name: 'Default', isLanding: true, order: 0, id: -1,
},
mangas: data,
},
]; // will set state on the next fetch
}
// no default category so the first tab is 1
setTabNum(1);
return [];
})
Promise.all<IManga[], ICategory[]>([
client.get('/api/v1/library').then((response) => response.data),
client.get('/api/v1/category').then((response) => response.data),
])
.then(
(newTabs: IMangaCategory[]) => {
fetch('http://127.0.0.1:4567/api/v1/category')
.then((response) => response.json())
.then((data: ICategory[]) => {
const mangaCategories = data.map((category) => ({
category,
mangas: [] as IManga[],
}));
const newNewTabs = [...newTabs, ...mangaCategories];
setTabs(newNewTabs);
([libraryMangas, categories]) => {
const categoryTabs = categories.map((category) => ({
category,
mangas: [] as IManga[],
isFetched: false,
}));
// if no default category, we must fetch the first tab now...
// eslint-disable-next-line max-len
if (newTabs.length === 0) { fetchAndSetMangas(newNewTabs, newNewTabs[0], 0); }
});
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
@@ -119,11 +117,12 @@ export default function Library() {
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
message={tab.isFetched ? 'Category is Empty' : 'Loading...'}
/>
</TabPanel>
));
// 160px is min-width for viewport width of >600
// Visual Hack: 160px is min-width for viewport width of >600
const scrollableTabs = window.innerWidth < tabs.length * 160;
toRender = (
<>
@@ -149,6 +148,7 @@ export default function Library() {
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
message={tabs.length > 0 ? 'Library is Empty' : undefined}
/>
);
}
+9 -6
View File
@@ -6,18 +6,21 @@ import React, { useEffect, useState, useContext } from 'react';
import { useParams } from 'react-router-dom';
import ChapterCard from '../components/ChapterCard';
import MangaDetails from '../components/MangaDetails';
import NavBarTitle from '../context/NavbarTitle';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
export default function Manga() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Manga'); setAction(<></>); }, []);
const { id } = useParams<{id: string}>();
const { setTitle } = useContext(NavBarTitle);
const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
.then((response) => response.json())
client.get(`/api/v1/manga/${id}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setTitle(data.title);
@@ -25,8 +28,8 @@ export default function Manga() {
}, []);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/chapters`)
.then((response) => response.json())
client.get(`/api/v1/manga/${id}/chapters`)
.then((response) => response.data)
.then((data) => setChapters(data));
}, []);
+10 -5
View File
@@ -4,7 +4,9 @@
import React, { useContext, useEffect, useState } from 'react';
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 = {
display: 'flex',
@@ -17,14 +19,17 @@ const style = {
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
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 { chapterId, mangaId } = useParams<{chapterId: string, mangaId: string}>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.json())
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterId}`)
.then((response) => response.data)
.then((data:IChapter) => {
setTitle(data.name);
setPageCount(data.pageCount);
@@ -41,7 +46,7 @@ export default function Reader() {
const mapped = range(pageCount).map((index) => (
<div style={{ margin: '0 auto' }}>
<img src={`http://127.0.0.1:4567/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="f" style={{ maxWidth: '100%' }} />
<img src={`${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterId}/page/${index}`} alt="F" style={{ maxWidth: '100%' }} />
</div>
));
return (
+9 -6
View File
@@ -8,7 +8,8 @@ import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle';
import NavbarContext from '../context/NavbarContext';
import client from '../util/client';
const useStyles = makeStyles((theme) => ({
root: {
@@ -20,7 +21,9 @@ const useStyles = makeStyles((theme) => ({
}));
export default function SearchSingle() {
const { setTitle } = useContext(NavBarTitle);
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Search'); setAction(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>();
const classes = useStyles();
const [error, setError] = useState<boolean>(false);
@@ -33,8 +36,8 @@ export default function SearchSingle() {
const textInput = React.createRef<HTMLInputElement>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
client.get(`/api/v1/source/${sourceId}`)
.then((response) => response.data)
.then((data: { name: string }) => setTitle(`Search: ${data.name}`));
}, []);
@@ -54,8 +57,8 @@ export default function SearchSingle() {
useEffect(() => {
if (searchTerm.length > 0) {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.json())
client.get(`/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.data)
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
if (data.mangaList.length > 0) {
setMangas([
+76 -11
View File
@@ -2,16 +2,21 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import List from '@material-ui/core/List';
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/Inbox';
import Brightness6Icon from '@material-ui/icons/Brightness6';
import { ListItemSecondaryAction, Switch } from '@material-ui/core';
import NavBarTitle from '../context/NavbarTitle';
import 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
@@ -19,13 +24,31 @@ function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
}
export default function Settings() {
const { setTitle } = useContext(NavBarTitle);
setTitle('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 (
<div>
<List component="nav" style={{ padding: 0 }}>
<>
<List style={{ padding: 0 }}>
<ListItemLink href="/settings/categories">
<ListItemIcon>
<InboxIcon />
@@ -45,7 +68,49 @@ export default function Settings() {
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemIcon>
<DnsIcon />
</ListItemIcon>
<ListItemText primary="Server Address" secondary={serverAddress} />
<ListItemSecondaryAction>
<IconButton
onClick={() => {
handleDialogOpen();
}}
>
<EditIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</List>
</div>
<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>
</>
);
}
+9 -6
View File
@@ -5,25 +5,28 @@
import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
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(<></>); }, []);
const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`)
.then((response) => response.json())
client.get(`/api/v1/source/${sourceId}`)
.then((response) => response.data)
.then((data: { name: string }) => setTitle(data.name));
}, []);
useEffect(() => {
const sourceType = props.popular ? 'popular' : 'latest';
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.json())
client.get(`/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.data)
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
setMangas([
...mangas,
+65 -8
View File
@@ -3,22 +3,79 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext, useEffect, useState } from 'react';
import ExtensionLangSelect from '../components/ExtensionLangSelect';
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() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Sources');
const { setTitle, setAction } = useContext(NavbarContext);
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', defualtLangs());
const [sources, setSources] = useState<ISource[]>([]);
const [fetched, setFetched] = useState<boolean>(false);
useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/source/list')
.then((response) => response.json())
.then((data) => setSources(data));
setTitle('Sources');
setAction(
<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) {
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>
)
))}
</>
);
}
+25 -34
View File
@@ -27,7 +27,8 @@ import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import NavBarTitle from '../../context/NavbarTitle';
import NavbarContext from '../../context/NavbarContext';
import client from '../../util/client';
const getItemStyle = (isDragging, draggableStyle, palette) => ({
// styles we need to apply on draggables
@@ -39,11 +40,12 @@ const getItemStyle = (isDragging, draggableStyle, palette) => ({
});
export default function Categories() {
const { setTitle } = useContext(NavBarTitle);
setTitle('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] = React.useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogValue, setDialogValue] = useState('');
const theme = useTheme();
@@ -52,8 +54,8 @@ export default function Categories() {
useEffect(() => {
if (!dialogOpen) {
fetch('http://127.0.0.1:4567/api/v1/category/')
.then((response) => response.json())
client.get('/api/v1/category/')
.then((response) => response.data)
.then((data) => setCategories(data));
}
}, [updateTriggerHolder]);
@@ -64,11 +66,8 @@ export default function Categories() {
const formData = new FormData();
formData.append('from', from + 1);
formData.append('to', to + 1);
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, {
method: 'PATCH',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
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);
@@ -90,48 +89,40 @@ export default function Categories() {
));
};
const handleDialogOpen = () => {
setDialogOpen(true);
};
const resetDialog = () => {
setDialogOpen(false);
setDialogValue('');
setCategoryToEdit(-1);
};
const handleDialogCancel = () => {
const handleDialogOpen = () => {
resetDialog();
setDialogOpen(true);
};
const handleDialogCancel = () => {
setDialogOpen(false);
};
const handleDialogSubmit = () => {
resetDialog();
setDialogOpen(false);
const formData = new FormData();
formData.append('name', dialogValue);
if (categoryToEdit === -1) {
fetch('http://127.0.0.1:4567/api/v1/category/', {
method: 'POST',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
client.post('/api/v1/category/', formData)
.finally(() => triggerUpdate());
} else {
const category = categories[categoryToEdit];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'PATCH',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
client.patch(`/api/v1/category/${category.id}`, formData)
.finally(() => triggerUpdate());
}
};
const deleteCategory = (index) => {
const category = categories[index];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'DELETE',
mode: 'cors',
}).finally(() => triggerUpdate());
client.delete(`/api/v1/category/${category.id}`)
.finally(() => triggerUpdate());
};
return (
@@ -167,8 +158,8 @@ export default function Categories() {
/>
<IconButton
onClick={() => {
setCategoryToEdit(index);
handleDialogOpen();
setCategoryToEdit(index);
}}
>
<EditIcon />
@@ -201,7 +192,7 @@ export default function Categories() {
>
<AddIcon />
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel} aria-labelledby="form-dialog-title">
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
</DialogTitle>
+26
View File
@@ -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;
+84
View File
@@ -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;
};
+28
View File
@@ -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 };
+20
View File
@@ -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];
}
+12
View File
@@ -2625,6 +2625,13 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf"
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:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -5233,6 +5240,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
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:
version "4.0.0"
resolved "https://registry.yarnpkg.com/fontsource-roboto/-/fontsource-roboto-4.0.0.tgz#35eacd4fb8d90199053c0eec9b34a57fb79cd820"