Compare commits

...

20 Commits

Author SHA1 Message Date
Aria Moradi af1c34fba5 v0.2.3
Publish / Validate Gradle Wrapper (push) Successful in 12s
Publish / Build FatJar (push) Failing after 16s
2021-02-24 12:27:28 +03:30
Aria Moradi 7b7d93786f Merge branch 'master' of github.com:AriaMoradi/Tachidesk 2021-02-24 12:09:39 +03:30
Aria Moradi 7c1c504482 new icon, fix headless systems crashing 2021-02-24 11:55:43 +03:30
Aria Moradi 33b22fcab6 Update README.md 2021-02-22 14:54:04 +03:30
Aria Moradi ab0566dcba Update README.md 2021-02-22 14:51:39 +03:30
Aria Moradi c4f2cc7189 Update README.md 2021-02-22 14:49:17 +03:30
Aria Moradi 4626d99590 Update README.md 2021-02-22 14:48:17 +03:30
Aria Moradi 6465ca8a19 Update README.md 2021-02-22 01:29:55 +03:30
Aria Moradi 15b9d151df Update README.md 2021-02-22 01:28:13 +03:30
Aria Moradi dd1b6c86cd Update README.md 2021-02-22 01:23:44 +03:30
Aria Moradi 9613cda79a new icons by @as280093 2021-02-21 23:37:11 +03:30
Aria Moradi 648b8e5960 bump version: v0.2.2
Publish / Validate Gradle Wrapper (push) Successful in 15s
Publish / Build FatJar (push) Failing after 16s
2021-02-21 04:42:33 +03:30
Aria Moradi ce545b1fd5 fix some bugs 2021-02-21 04:41:56 +03:30
Aria Moradi 9151034fbc category done! 2021-02-21 04:27:41 +03:30
Aria Moradi 312a8baa13 hide menu button for now 2021-02-20 02:59:32 +03:30
Aria Moradi 18b6168cd1 theme select in settings 2021-02-20 02:57:52 +03:30
Aria Moradi 9a282c3bf4 redirect / to library 2021-02-20 02:41:30 +03:30
Aria Moradi 2bbebe4c30 fix removing manga from library not working 2021-02-20 02:34:26 +03:30
Aria Moradi 162961b560 fix tabs 2021-02-20 02:28:55 +03:30
Aria Moradi f1cc37d0db finished the category screen 2021-02-20 01:23:52 +03:30
32 changed files with 827 additions and 124 deletions
+33 -16
View File
@@ -5,7 +5,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 +34,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 +73,6 @@ How to do it is described in `webUI/react/README.md` but for short,
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project 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`.
+2 -2
View File
@@ -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.3"
repositories { repositories {
@@ -149,7 +149,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"
} }
@@ -18,6 +18,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,6 +30,7 @@ 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
@@ -60,7 +62,7 @@ class Main {
// make sure everything we need exists // make sure everything we need exists
applicationSetup() applicationSetup()
val tray = systemTray() val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
registerConfigModules() registerConfigModules()
@@ -165,11 +167,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 +234,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 +247,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()
@@ -29,12 +29,14 @@ fun makeDataBaseTables() {
// 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)
@@ -3,6 +3,7 @@ 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.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,21 +15,34 @@ 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
}
} }
} }
} }
@@ -41,7 +55,7 @@ fun removeCategory(categoryId: Int) {
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)
}
}
}
@@ -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)
} }
} }
@@ -27,45 +27,54 @@ fun applicationSetup() {
} }
fun openInBrowser() { 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") val icon = Main::class.java.getResource("/icon/faviconlogo.png")
fun systemTray(): SystemTray? { fun systemTray(): SystemTray? {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java try {
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode // ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
if (System.getProperty("os.name").startsWith("Windows")) SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear() CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu val mainMenu = systemTray.menu
mainMenu.add( mainMenu.add(
MenuItem( MenuItem(
"Open Tachidesk", "Open Tachidesk",
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()
}
} }
} )
) )
)
// systemTray.setTooltip("Tachidesk") // systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon) systemTray.setImage(icon)
// systemTray.status = "No Mail" // systemTray.status = "No Mail"
systemTray.getMenu().add( systemTray.getMenu().add(
MenuItem("Quit") { MenuItem("Quit") {
systemTray.shutdown() systemTray.shutdown()
System.exit(0) 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: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

+1
View File
@@ -1,3 +1,4 @@
node_modules/ node_modules/
.eslintcache .eslintcache
.vscode .vscode
.env
+1
View File
@@ -10,6 +10,7 @@
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"fontsource-roboto": "^4.0.0", "fontsource-roboto": "^4.0.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-beautiful-dnd": "^13.0.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

+4 -4
View File
@@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/> <link rel="icon" href="%PUBLIC_URL%/faviconlogo.ico"/>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
<meta name="theme-color" content="#000000"/> <meta name="theme-color" content="#000000"/>
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="A manga reader that runs tachiyomi's extensions"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/> <link rel="apple-touch-icon" href="%PUBLIC_URL%/faviconlogo.png"/>
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Tachidesk</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
+21 -10
View File
@@ -4,23 +4,24 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
BrowserRouter as Router, Route, Switch, BrowserRouter as Router, Redirect, Route, Switch,
} from 'react-router-dom'; } from 'react-router-dom';
import { Container } from '@material-ui/core'; import { Container } from '@material-ui/core';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import Home from './screens/Home';
import Sources from './screens/Sources'; import Sources from './screens/Sources';
import Extensions from './screens/Extensions'; import Extensions from './screens/Extensions';
import MangaList from './screens/MangaList'; import SourceMangas from './screens/SourceMangas';
import Manga from './screens/Manga'; import Manga from './screens/Manga';
import Reader from './screens/Reader'; import Reader from './screens/Reader';
import Search from './screens/SearchSingle'; import Search from './screens/SearchSingle';
import NavBarTitle from './context/NavbarTitle'; import NavBarTitle from './context/NavbarTitle';
import DarkTheme from './context/DarkTheme'; import DarkTheme from './context/DarkTheme';
import Library from './screens/Library'; import Library from './screens/Library';
import Settings from './screens/Settings';
import Categories from './screens/settings/Categories';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
@@ -57,9 +58,7 @@ export default function App() {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<NavBarTitle.Provider value={navTitleContext}> <NavBarTitle.Provider value={navTitleContext}>
<CssBaseline /> <CssBaseline />
<DarkTheme.Provider value={darkThemeContext}> <NavBar />
<NavBar />
</DarkTheme.Provider>
<Container maxWidth={false} disableGutters> <Container maxWidth={false} disableGutters>
<Switch> <Switch>
<Route path="/sources/:sourceId/search/"> <Route path="/sources/:sourceId/search/">
@@ -69,10 +68,10 @@ export default function App() {
<Extensions /> <Extensions />
</Route> </Route>
<Route path="/sources/:sourceId/popular/"> <Route path="/sources/:sourceId/popular/">
<MangaList popular /> <SourceMangas popular />
</Route> </Route>
<Route path="/sources/:sourceId/latest/"> <Route path="/sources/:sourceId/latest/">
<MangaList popular={false} /> <SourceMangas popular={false} />
</Route> </Route>
<Route path="/sources"> <Route path="/sources">
<Sources /> <Sources />
@@ -86,9 +85,21 @@ 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> </NavBarTitle.Provider>
@@ -0,0 +1,112 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useEffect, useState } from 'react';
import { makeStyles, createStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormGroup from '@material-ui/core/FormGroup';
const useStyles = makeStyles(() => createStyles({
paper: {
maxHeight: 435,
width: '80%',
},
}));
interface IProps {
open: boolean
setOpen: (value: boolean) => void
mangaId: number
}
interface ICategoryInfo {
category: ICategory
selected: boolean
}
export default function CategorySelect(props: IProps) {
const classes = useStyles();
const { open, setOpen, mangaId } = props;
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
let tmpCategoryInfos: ICategoryInfo[] = [];
fetch('http://127.0.0.1:4567/api/v1/category/')
.then((response) => response.json())
.then((data: ICategory[]) => {
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
})
.then(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`)
.then((response) => response.json())
.then((data: ICategory[]) => {
data.forEach((category) => {
tmpCategoryInfos[category.order - 1].selected = true;
});
setCategoryInfos(tmpCategoryInfos);
});
});
}, [updateTriggerHolder, open]);
const handleCancel = () => {
setOpen(false);
};
const handleOk = () => {
setOpen(false);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
const { checked } = event.target as HTMLInputElement;
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, {
method: checked ? 'GET' : 'DELETE', mode: 'cors',
})
.then(() => triggerUpdate());
};
return (
<Dialog
classes={classes}
maxWidth="xs"
open={open}
>
<DialogTitle>Set categories</DialogTitle>
<DialogContent dividers>
<FormGroup>
{categoryInfos.map((categoryInfo) => (
<FormControlLabel
control={(
<Checkbox
checked={categoryInfo.selected}
onChange={(e) => handleChange(e, categoryInfo.category.id)}
color="default"
/>
)}
label={categoryInfo.category.name}
/>
))}
</FormGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel} color="primary">
Cancel
</Button>
<Button onClick={handleOk} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
);
}
+25 -4
View File
@@ -2,18 +2,31 @@
* 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 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');
@@ -38,13 +51,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> </div>
</> <CategorySelect
open={categoryDialogOpen}
setOpen={setCategoryDialogOpen}
mangaId={manga.id}
/>
</div>
); );
} }
+7 -5
View File
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: remove above!
/* This Source Code Form is subject to the terms of the Mozilla Public /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
@@ -45,7 +47,7 @@ export default function NavBar() {
const { title } = useContext(NavBarTitle); const { title } = useContext(NavBarTitle);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const { darkTheme, setDarkTheme } = useContext(DarkTheme); const { darkTheme } = useContext(DarkTheme);
const handleMenu = (event: React.MouseEvent<HTMLElement>) => { const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@@ -72,15 +74,15 @@ export default function NavBar() {
<Typography variant="h6" className={classes.title}> <Typography variant="h6" className={classes.title}>
{title} {title}
</Typography> </Typography>
<IconButton {/* <IconButton
onClick={handleMenu} onClick={handleMenu}
aria-label="display more actions" aria-label="display more actions"
edge="end" edge="end"
color="inherit" color="inherit"
> >
<MoreIcon /> <MoreIcon />
</IconButton> </IconButton> */}
<Menu {/* <Menu
id="menu-appbar" id="menu-appbar"
anchorEl={anchorEl} anchorEl={anchorEl}
anchorOrigin={{ anchorOrigin={{
@@ -107,7 +109,7 @@ export default function NavBar() {
Light Theme Light Theme
</MenuItem> </MenuItem>
</Menu> </Menu> */}
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} /> <TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
@@ -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>
-13
View File
@@ -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>
);
}
+138 -15
View File
@@ -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 NavBarTitle from '../context/NavbarTitle';
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); }
interface TabPanelProps {
children: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const {
children, value, index,
} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
>
{value === index && children}
</div>
);
}
export default function Library() {
const { setTitle } = useContext(NavBarTitle);
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
const [tabNum, setTabNum] = useState<number>(0);
// a hack so MangaGrid doesn't stop working. I won't change it in case
// if I do manga pagination for library..
const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => { useEffect(() => {
setTitle('Library'); 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(() => { useEffect(() => {
fetch('http://127.0.0.1:4567/api/v1/library') fetch('http://127.0.0.1:4567/api/v1/library')
.then((response) => response.json()) .then((response) => response.json())
.then((data: IManga[]) => { .then((data: IManga[]) => {
setMangas(data); // if some manga with no category exist, they will be added under a virtual category
}); if (data.length > 0) {
}, [lastPageNum]); return [
{
category: {
name: 'Default', isLanding: true, order: 0, id: -1,
},
mangas: data,
},
]; // will set state on the next fetch
}
return ( // no default category so the first tab is 1
<MangaGrid setTabNum(1);
mangas={mangas} return [];
hasNextPage={false} })
lastPageNum={lastPageNum} .then(
setLastPageNum={setLastPageNum} (newTabs: IMangaCategory[]) => {
/> fetch('http://127.0.0.1:4567/api/v1/category')
); .then((response) => response.json())
.then((data: ICategory[]) => {
const mangaCategories = data.map((category) => ({
category,
mangas: [] as IManga[],
}));
const newNewTabs = [...newTabs, ...mangaCategories];
setTabs(newNewTabs);
// if no default category, we must fetch the first tab now...
// eslint-disable-next-line max-len
if (newTabs.length === 0) { fetchAndSetMangas(newNewTabs, newNewTabs[0], 0); }
});
},
);
}, []);
let toRender;
if (tabs.length > 1) {
// eslint-disable-next-line max-len
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));
const tabBodies = tabs.map((tab) => (
<TabPanel value={tabNum} index={tab.category.order}>
<MangaGrid
mangas={tab.mangas}
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
</TabPanel>
));
// 160px is min-width for viewport width of >600
const scrollableTabs = window.innerWidth < tabs.length * 160;
toRender = (
<>
<Tabs
value={tabNum}
onChange={(e, newTab) => handleTabChange(newTab)}
indicatorColor="primary"
textColor="primary"
centered={!scrollableTabs}
variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
scrollButtons="on"
>
{tabDefines}
</Tabs>
{tabBodies}
</>
);
} else {
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
toRender = (
<MangaGrid
mangas={mangas}
hasNextPage={false}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
}
return toRender;
} }
+51
View File
@@ -0,0 +1,51 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import React, { useContext } from 'react';
import List from '@material-ui/core/List';
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import InboxIcon from '@material-ui/icons/Inbox';
import Brightness6Icon from '@material-ui/icons/Brightness6';
import { ListItemSecondaryAction, Switch } from '@material-ui/core';
import NavBarTitle from '../context/NavbarTitle';
import DarkTheme from '../context/DarkTheme';
function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ListItem button component="a" {...props} />;
}
export default function Settings() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Settings');
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
return (
<div>
<List component="nav" style={{ padding: 0 }}>
<ListItemLink href="/settings/categories">
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Categories" />
</ListItemLink>
<ListItem>
<ListItemIcon>
<Brightness6Icon />
</ListItemIcon>
<ListItemText primary="Dark Theme" />
<ListItemSecondaryAction>
<Switch
edge="end"
checked={darkTheme}
onChange={() => setDarkTheme(!darkTheme)}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</div>
);
}
@@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
import MangaGrid from '../components/MangaGrid'; import MangaGrid from '../components/MangaGrid';
import NavBarTitle from '../context/NavbarTitle'; import NavBarTitle from '../context/NavbarTitle';
export default function MangaList(props: { popular: boolean }) { export default function SourceMangas(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
@@ -0,0 +1,235 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState, useContext, useEffect } from 'react';
import {
List,
ListItem,
ListItemText,
ListItemIcon,
IconButton,
} from '@material-ui/core';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import DragHandleIcon from '@material-ui/icons/DragHandle';
import EditIcon from '@material-ui/icons/Edit';
import { useTheme } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
import DeleteIcon from '@material-ui/icons/Delete';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import NavBarTitle from '../../context/NavbarTitle';
const getItemStyle = (isDragging, draggableStyle, palette) => ({
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
}),
});
export default function Categories() {
const { setTitle } = useContext(NavBarTitle);
setTitle('Categories');
const [categories, setCategories] = useState([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = React.useState(false);
const [dialogValue, setDialogValue] = useState('');
const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => {
if (!dialogOpen) {
fetch('http://127.0.0.1:4567/api/v1/category/')
.then((response) => response.json())
.then((data) => setCategories(data));
}
}, [updateTriggerHolder]);
const categoryReorder = (list, from, to) => {
const category = list[from];
const formData = new FormData();
formData.append('from', from + 1);
formData.append('to', to + 1);
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, {
method: 'PATCH',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
// also move it in local state to avoid jarring moving behviour...
const result = Array.from(list);
const [removed] = result.splice(from, 1);
result.splice(to, 0, removed);
return result;
};
const onDragEnd = (result) => {
// dropped outside the list?
if (!result.destination) {
return;
}
setCategories(categoryReorder(
categories,
result.source.index,
result.destination.index,
));
};
const handleDialogOpen = () => {
setDialogOpen(true);
};
const resetDialog = () => {
setDialogOpen(false);
setDialogValue('');
setCategoryToEdit(-1);
};
const handleDialogCancel = () => {
resetDialog();
};
const handleDialogSubmit = () => {
resetDialog();
const formData = new FormData();
formData.append('name', dialogValue);
if (categoryToEdit === -1) {
fetch('http://127.0.0.1:4567/api/v1/category/', {
method: 'POST',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
} else {
const category = categories[categoryToEdit];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'PATCH',
mode: 'cors',
body: formData,
}).finally(() => triggerUpdate());
}
};
const deleteCategory = (index) => {
const category = categories[index];
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
method: 'DELETE',
mode: 'cors',
}).finally(() => triggerUpdate());
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<List ref={provided.innerRef}>
{categories.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id.toString()}
index={index}
>
{(provided, snapshot) => (
<ListItem
ContainerComponent="li"
ContainerProps={{ ref: provided.innerRef }}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
theme.palette,
)}
ref={provided.innerRef}
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText
primary={item.name}
/>
<IconButton
onClick={() => {
setCategoryToEdit(index);
handleDialogOpen();
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
>
<DeleteIcon />
</IconButton>
</ListItem>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
<Fab
color="primary"
aria-label="add"
style={{
position: 'absolute',
bottom: theme.spacing(2),
right: theme.spacing(2),
}}
onClick={handleDialogOpen}
>
<AddIcon />
</Fab>
<Dialog open={dialogOpen} onClose={handleDialogCancel} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
</DialogTitle>
<DialogContent>
<DialogContentText>
Enter new category name.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Category Name"
type="text"
fullWidth
value={dialogValue}
onChange={(e) => setDialogValue(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogCancel} color="primary">
Cancel
</Button>
<Button onClick={handleDialogSubmit} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
</>
);
}
+7
View File
@@ -38,3 +38,10 @@ interface IChapter {
mangaId: number mangaId: number
pageCount: number pageCount: number
} }
interface ICategory {
id: number
order: number
name: String
isLanding: boolean
}
+57 -3
View File
@@ -3744,6 +3744,13 @@ css-blank-pseudo@^0.1.4:
dependencies: dependencies:
postcss "^7.0.5" postcss "^7.0.5"
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
css-color-names@0.0.4, css-color-names@^0.0.4: css-color-names@0.0.4, css-color-names@^0.0.4:
version "0.0.4" version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -7344,6 +7351,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memoize-one@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memory-fs@^0.4.1: memory-fs@^0.4.1:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -9208,6 +9220,11 @@ querystringify@^2.1.1:
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.4.1: raf@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@@ -9257,6 +9274,19 @@ react-app-polyfill@^2.0.0:
regenerator-runtime "^0.13.7" regenerator-runtime "^0.13.7"
whatwg-fetch "^3.4.1" whatwg-fetch "^3.4.1"
react-beautiful-dnd@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
dependencies:
"@babel/runtime" "^7.8.4"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.1.1"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-dev-utils@^11.0.1: react-dev-utils@^11.0.1:
version "11.0.1" version "11.0.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
@@ -9301,7 +9331,7 @@ react-error-overlay@^6.0.8:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw== integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -9311,6 +9341,17 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-redux@^7.1.1:
version "7.2.2"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
dependencies:
"@babel/runtime" "^7.12.1"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.13.1"
react-refresh@^0.8.3: react-refresh@^0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
@@ -9518,6 +9559,14 @@ redent@^3.0.0:
indent-string "^4.0.0" indent-string "^4.0.0"
strip-indent "^3.0.0" strip-indent "^3.0.0"
redux@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.2.0: regenerate-unicode-properties@^8.2.0:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -10672,7 +10721,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1" unquote "~1.1.1"
util.promisify "~1.0.0" util.promisify "~1.0.0"
symbol-observable@1.2.0: symbol-observable@1.2.0, symbol-observable@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -10823,7 +10872,7 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2: tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
@@ -11176,6 +11225,11 @@ url@^0.11.0:
punycode "1.3.2" punycode "1.3.2"
querystring "0.2.0" querystring "0.2.0"
use-memo-one@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
use@^3.1.0: use@^3.1.0:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"