Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af1c34fba5 | |||
| 7b7d93786f | |||
| 7c1c504482 | |||
| 33b22fcab6 | |||
| ab0566dcba | |||
| c4f2cc7189 | |||
| 4626d99590 | |||
| 6465ca8a19 | |||
| 15b9d151df | |||
| dd1b6c86cd | |||
| 9613cda79a | |||
| 648b8e5960 | |||
| ce545b1fd5 | |||
| 9151034fbc | |||
| 312a8baa13 | |||
| 18b6168cd1 | |||
| 9a282c3bf4 | |||
| 2bbebe4c30 | |||
| 162961b560 | |||
| f1cc37d0db |
@@ -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.
|
||||
|
||||
## 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 +34,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 +73,6 @@ How to do it is described in `webUI/react/README.md` but for short,
|
||||
then open `http://127.0.0.1:3000` in a modern browser. This is a `create-react-app` project
|
||||
and supports HMR and all the other goodies you'll need.
|
||||
|
||||
## Is this application usable? Should I test it?
|
||||
If you'd ask me, I'd tell you If you want to read your manga **online** from tachiyomi or in one place and bypass all the ads, you can use Tachidesk.
|
||||
|
||||
There are almost no quality of life features, including no library, no downloading for offline enjoyment and sadly no MangaDex search.
|
||||
|
||||
Anyways, for more info checkout [finished milestone #1](https://github.com/AriaMoradi/Tachidesk/issues/2) and [milestone #2](https://github.com/AriaMoradi/Tachidesk/projects/1) to see what's implemented.
|
||||
|
||||
## How does it work?
|
||||
This project has two components:
|
||||
1. **server:** contains the implementation of [tachiyomi's extensions library](https://github.com/tachiyomiorg/extensions-lib) and uses an Android compatibility library to run apk extensions. All this concludes to serving a REST API to `webUI`.
|
||||
2. **webUI:** A react SPA project that works with the server to do the presentation.
|
||||
|
||||
## Support
|
||||
Join Tachidesk's [discord server](https://discord.gg/wgPyb7hE5d) to hang out with the community and receive support.
|
||||
|
||||
## Credit
|
||||
The `AndroidCompat` module and `scripts/getAndroid.sh` was originally developed by [@null-dev](https://github.com/null-dev) for [TachiWeb-Server](https://github.com/Tachiweb/TachiWeb-server) and is licensed under `Apache License Version 2.0`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ plugins {
|
||||
id("edu.sc.seis.launch4j") version "2.4.9"
|
||||
}
|
||||
|
||||
val TachideskVersion = "v0.2.1"
|
||||
val TachideskVersion = "v0.2.3"
|
||||
|
||||
|
||||
repositories {
|
||||
@@ -149,7 +149,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"
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import ir.armor.tachidesk.util.getExtensionIcon
|
||||
import ir.armor.tachidesk.util.getExtensionList
|
||||
import ir.armor.tachidesk.util.getLibraryMangas
|
||||
import ir.armor.tachidesk.util.getManga
|
||||
import ir.armor.tachidesk.util.getMangaCategories
|
||||
import ir.armor.tachidesk.util.getMangaList
|
||||
import ir.armor.tachidesk.util.getPageImage
|
||||
import ir.armor.tachidesk.util.getSource
|
||||
@@ -29,6 +30,7 @@ import ir.armor.tachidesk.util.removeCategory
|
||||
import ir.armor.tachidesk.util.removeExtension
|
||||
import ir.armor.tachidesk.util.removeMangaFromCategory
|
||||
import ir.armor.tachidesk.util.removeMangaFromLibrary
|
||||
import ir.armor.tachidesk.util.reorderCategory
|
||||
import ir.armor.tachidesk.util.sourceFilters
|
||||
import ir.armor.tachidesk.util.sourceGlobalSearch
|
||||
import ir.armor.tachidesk.util.sourceSearch
|
||||
@@ -60,7 +62,7 @@ class Main {
|
||||
|
||||
// make sure everything we need exists
|
||||
applicationSetup()
|
||||
val tray = systemTray()
|
||||
val tray = systemTray() // assign it to a variable so it's kept in the memory and not garbage collected
|
||||
|
||||
registerConfigModules()
|
||||
|
||||
@@ -165,11 +167,16 @@ class Main {
|
||||
// removes the manga from the library
|
||||
app.delete("api/v1/manga/:mangaId/library") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
println("fuck")
|
||||
removeMangaFromLibrary(mangaId)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
ctx.json(getMangaCategories(mangaId))
|
||||
}
|
||||
|
||||
// adds the manga to category
|
||||
app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx ->
|
||||
val mangaId = ctx.pathParam("mangaId").toInt()
|
||||
@@ -227,7 +234,7 @@ class Main {
|
||||
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 ->
|
||||
ctx.json(getLibraryMangas())
|
||||
}
|
||||
@@ -240,20 +247,28 @@ class Main {
|
||||
// category create
|
||||
app.post("/api/v1/category/") { ctx ->
|
||||
val name = ctx.formParam("name")!!
|
||||
val isLanding = ctx.formParam("isLanding", "false").toBoolean()
|
||||
createCategory(name, isLanding)
|
||||
createCategory(name)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category modification
|
||||
app.put("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val name = ctx.formParam("name")!!
|
||||
val isLanding = ctx.formParam("isLanding").toBoolean()
|
||||
app.patch("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId")!!.toInt()
|
||||
val name = ctx.formParam("name")
|
||||
val isLanding = if (ctx.formParam("isLanding") != null) ctx.formParam("isLanding")?.toBoolean() else null
|
||||
updateCategory(categoryId, name, isLanding)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category re-ordering
|
||||
app.patch("/api/v1/category/:categoryId/reorder") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
val from = ctx.formParam("from")!!.toInt()
|
||||
val to = ctx.formParam("to")!!.toInt()
|
||||
reorderCategory(categoryId, from, to)
|
||||
ctx.status(200)
|
||||
}
|
||||
|
||||
// category delete
|
||||
app.delete("/api/v1/category/:categoryId") { ctx ->
|
||||
val categoryId = ctx.pathParam("categoryId").toInt()
|
||||
|
||||
@@ -29,12 +29,14 @@ fun makeDataBaseTables() {
|
||||
// db.useNestedTransactions = true
|
||||
|
||||
transaction {
|
||||
SchemaUtils.create(ExtensionTable)
|
||||
SchemaUtils.create(SourceTable)
|
||||
SchemaUtils.create(MangaTable)
|
||||
SchemaUtils.create(ChapterTable)
|
||||
SchemaUtils.create(PageTable)
|
||||
SchemaUtils.create(CategoryTable)
|
||||
SchemaUtils.create(CategoryMangaTable)
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
ExtensionTable,
|
||||
SourceTable,
|
||||
MangaTable,
|
||||
ChapterTable,
|
||||
PageTable,
|
||||
CategoryTable,
|
||||
CategoryMangaTable,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ package ir.armor.tachidesk.database.dataclass
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class CategoryDataClass(
|
||||
val id: Int,
|
||||
val order: Int,
|
||||
val name: String,
|
||||
val isLanding: Boolean
|
||||
)
|
||||
|
||||
@@ -11,9 +11,12 @@ import org.jetbrains.exposed.sql.ResultRow
|
||||
object CategoryTable : IntIdTable() {
|
||||
val name = varchar("name", 64)
|
||||
val isLanding = bool("is_landing").default(false)
|
||||
val order = integer("order").default(0)
|
||||
}
|
||||
|
||||
fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass(
|
||||
categoryEntry[CategoryTable.id].value,
|
||||
categoryEntry[CategoryTable.order],
|
||||
categoryEntry[CategoryTable.name],
|
||||
categoryEntry[CategoryTable.isLanding],
|
||||
)
|
||||
|
||||
@@ -25,6 +25,7 @@ object MangaTable : IntIdTable() {
|
||||
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||
|
||||
val inLibrary = bool("in_library").default(false)
|
||||
val defaultCategory = bool("default_category").default(true)
|
||||
|
||||
// source is used by some ancestor of IntIdTable
|
||||
val sourceReference = reference("source", SourceTable)
|
||||
|
||||
@@ -3,6 +3,7 @@ package ir.armor.tachidesk.util
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
@@ -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
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
fun createCategory(name: String, isLanding: Boolean) {
|
||||
fun createCategory(name: String) {
|
||||
transaction {
|
||||
val count = CategoryTable.selectAll().count()
|
||||
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null)
|
||||
CategoryTable.insert {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.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 {
|
||||
CategoryTable.update({ CategoryTable.id eq categoryId }) {
|
||||
it[CategoryTable.name] = name
|
||||
it[CategoryTable.isLanding] = isLanding
|
||||
if (name != null) it[CategoryTable.name] = name
|
||||
if (isLanding != null) it[CategoryTable.isLanding] = isLanding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderCategory(categoryId: Int, from: Int, to: Int) {
|
||||
transaction {
|
||||
val categories = CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
|
||||
categories.add(to - 1, categories.removeAt(from - 1))
|
||||
categories.forEachIndexed { index, cat ->
|
||||
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
|
||||
it[CategoryTable.order] = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +55,7 @@ fun removeCategory(categoryId: Int) {
|
||||
|
||||
fun getCategoryList(): List<CategoryDataClass> {
|
||||
return transaction {
|
||||
CategoryTable.selectAll().map {
|
||||
CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map {
|
||||
CategoryTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package ir.armor.tachidesk.util
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.CategoryDataClass
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.CategoryTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -21,6 +25,10 @@ fun addMangaToCategory(mangaId: Int, categoryId: Int) {
|
||||
it[CategoryMangaTable.category] = categoryId
|
||||
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) {
|
||||
transaction {
|
||||
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
|
||||
if (CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.count() == 0L) {
|
||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||
it[MangaTable.defaultCategory] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||
import ir.armor.tachidesk.database.table.CategoryMangaTable
|
||||
import ir.armor.tachidesk.database.table.MangaTable
|
||||
import ir.armor.tachidesk.database.table.toDataClass
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.deleteWhere
|
||||
import org.jetbrains.exposed.sql.select
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
@@ -24,18 +28,20 @@ fun addMangaToLibrary(mangaId: Int) {
|
||||
|
||||
fun removeMangaFromLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
if (manga.inLibrary) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = false
|
||||
it[defaultCategory] = true
|
||||
}
|
||||
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga eq mangaId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryMangas(): List<MangaDataClass> {
|
||||
return transaction {
|
||||
MangaTable.select { MangaTable.inLibrary eq true }.map {
|
||||
MangaTable.select { (MangaTable.inLibrary eq true) and (MangaTable.defaultCategory eq true) }.map {
|
||||
MangaTable.toDataClass(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,45 +27,54 @@ fun applicationSetup() {
|
||||
}
|
||||
|
||||
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? {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// 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: 408 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 149 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
.eslintcache
|
||||
.vscode
|
||||
.env
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"fontsource-roboto": "^4.0.0",
|
||||
"react": "^17.0.1",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
@@ -2,14 +2,14 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
|
||||
<link rel="icon" href="%PUBLIC_URL%/faviconlogo.ico"/>
|
||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="A manga reader that runs tachiyomi's extensions"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/faviconlogo.png"/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Tachidesk</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
+21
-10
@@ -4,23 +4,24 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router, Route, Switch,
|
||||
BrowserRouter as Router, Redirect, Route, Switch,
|
||||
} from 'react-router-dom';
|
||||
import { Container } from '@material-ui/core';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
|
||||
import NavBar from './components/NavBar';
|
||||
import Home from './screens/Home';
|
||||
import Sources from './screens/Sources';
|
||||
import Extensions from './screens/Extensions';
|
||||
import MangaList from './screens/MangaList';
|
||||
import SourceMangas from './screens/SourceMangas';
|
||||
import Manga from './screens/Manga';
|
||||
import Reader from './screens/Reader';
|
||||
import Search from './screens/SearchSingle';
|
||||
import NavBarTitle from './context/NavbarTitle';
|
||||
import DarkTheme from './context/DarkTheme';
|
||||
import Library from './screens/Library';
|
||||
import Settings from './screens/Settings';
|
||||
import Categories from './screens/settings/Categories';
|
||||
|
||||
export default function App() {
|
||||
const [title, setTitle] = useState<string>('Tachidesk');
|
||||
@@ -57,9 +58,7 @@ export default function App() {
|
||||
<ThemeProvider theme={theme}>
|
||||
<NavBarTitle.Provider value={navTitleContext}>
|
||||
<CssBaseline />
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<NavBar />
|
||||
</DarkTheme.Provider>
|
||||
<NavBar />
|
||||
<Container maxWidth={false} disableGutters>
|
||||
<Switch>
|
||||
<Route path="/sources/:sourceId/search/">
|
||||
@@ -69,10 +68,10 @@ export default function App() {
|
||||
<Extensions />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/popular/">
|
||||
<MangaList popular />
|
||||
<SourceMangas popular />
|
||||
</Route>
|
||||
<Route path="/sources/:sourceId/latest/">
|
||||
<MangaList popular={false} />
|
||||
<SourceMangas popular={false} />
|
||||
</Route>
|
||||
<Route path="/sources">
|
||||
<Sources />
|
||||
@@ -86,9 +85,21 @@ export default function App() {
|
||||
<Route path="/library">
|
||||
<Library />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
<Route path="/settings/categories">
|
||||
<Categories />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<Settings />
|
||||
</DarkTheme.Provider>
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => (
|
||||
<Redirect to="/library" />
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Container>
|
||||
</NavBarTitle.Provider>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
paper: {
|
||||
maxHeight: 435,
|
||||
width: '80%',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
open: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
mangaId: number
|
||||
}
|
||||
|
||||
interface ICategoryInfo {
|
||||
category: ICategory
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export default function CategorySelect(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { open, setOpen, mangaId } = props;
|
||||
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||
|
||||
useEffect(() => {
|
||||
let tmpCategoryInfos: ICategoryInfo[] = [];
|
||||
fetch('http://127.0.0.1:4567/api/v1/category/')
|
||||
.then((response) => response.json())
|
||||
.then((data: ICategory[]) => {
|
||||
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
||||
})
|
||||
.then(() => {
|
||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/`)
|
||||
.then((response) => response.json())
|
||||
.then((data: ICategory[]) => {
|
||||
data.forEach((category) => {
|
||||
tmpCategoryInfos[category.order - 1].selected = true;
|
||||
});
|
||||
setCategoryInfos(tmpCategoryInfos);
|
||||
});
|
||||
});
|
||||
}, [updateTriggerHolder, open]);
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOk = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
fetch(`http://127.0.0.1:4567/api/v1/manga/${mangaId}/category/${categoryId}`, {
|
||||
method: checked ? 'GET' : 'DELETE', mode: 'cors',
|
||||
})
|
||||
.then(() => triggerUpdate());
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
classes={classes}
|
||||
maxWidth="xs"
|
||||
open={open}
|
||||
>
|
||||
<DialogTitle>Set categories</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormGroup>
|
||||
{categoryInfos.map((categoryInfo) => (
|
||||
<FormControlLabel
|
||||
control={(
|
||||
<Checkbox
|
||||
checked={categoryInfo.selected}
|
||||
onChange={(e) => handleChange(e, categoryInfo.category.id)}
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
label={categoryInfo.category.name}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button autoFocus onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleOk} color="primary">
|
||||
Ok
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,31 @@
|
||||
* 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 { Button } from '@material-ui/core';
|
||||
import { Button, createStyles, makeStyles } from '@material-ui/core';
|
||||
import React, { useState } from 'react';
|
||||
import CategorySelect from './CategorySelect';
|
||||
|
||||
const useStyles = makeStyles(() => createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
'& button': {
|
||||
marginLeft: 10,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps{
|
||||
manga: IManga
|
||||
}
|
||||
|
||||
export default function MangaDetails(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const { manga } = props;
|
||||
const [inLibrary, setInLibrary] = useState<string>(
|
||||
manga.inLibrary ? 'In Library' : 'Not In Library',
|
||||
);
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState<boolean>(false);
|
||||
|
||||
function addToLibrary() {
|
||||
setInLibrary('adding');
|
||||
@@ -38,13 +51,21 @@ export default function MangaDetails(props: IProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1>
|
||||
{manga && manga.title}
|
||||
</h1>
|
||||
<div style={{ display: 'flex', flexDirection: 'row-reverse' }}>
|
||||
<div className={classes.root}>
|
||||
<Button variant="outlined" onClick={() => handleButtonClick()}>{inLibrary}</Button>
|
||||
{inLibrary === 'In Library'
|
||||
&& <Button variant="outlined" onClick={() => setCategoryDialogOpen(true)}>Edit Categories</Button>}
|
||||
|
||||
</div>
|
||||
</>
|
||||
<CategorySelect
|
||||
open={categoryDialogOpen}
|
||||
setOpen={setCategoryDialogOpen}
|
||||
mangaId={manga.id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// TODO: remove above!
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
@@ -45,7 +47,7 @@ export default function NavBar() {
|
||||
const { title } = useContext(NavBarTitle);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||
const { darkTheme } = useContext(DarkTheme);
|
||||
|
||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -72,15 +74,15 @@ export default function NavBar() {
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton
|
||||
{/* <IconButton
|
||||
onClick={handleMenu}
|
||||
aria-label="display more actions"
|
||||
edge="end"
|
||||
color="inherit"
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
</IconButton> */}
|
||||
{/* <Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
@@ -107,7 +109,7 @@ export default function NavBar() {
|
||||
Light Theme
|
||||
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Menu> */}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<TemporaryDrawer drawerOpen={drawerOpen} setDrawerOpen={setDrawerOpen} />
|
||||
|
||||
@@ -60,6 +60,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
|
||||
<ListItemText primary="Sources" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="settings">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
{/* <Link to="/search" style={{ color: 'inherit', textDecoration: 'none' }}>
|
||||
<ListItem button key="Search">
|
||||
<ListItemIcon>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<h1>
|
||||
Hint: Click Tn The Top Left Menu Button
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
@@ -2,33 +2,156 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Tab, Tabs } from '@material-ui/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
|
||||
export default function MangaList() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
interface IMangaCategory {
|
||||
category: ICategory
|
||||
mangas: IManga[]
|
||||
}
|
||||
|
||||
interface TabPanelProps {
|
||||
children: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const {
|
||||
children, value, index,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Library() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
const [tabs, setTabs] = useState<IMangaCategory[]>([]);
|
||||
const [tabNum, setTabNum] = useState<number>(0);
|
||||
|
||||
// a hack so MangaGrid doesn't stop working. I won't change it in case
|
||||
// if I do manga pagination for library..
|
||||
const [lastPageNum, setLastPageNum] = useState<number>(1);
|
||||
useEffect(() => {
|
||||
setTitle('Library');
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const fetchAndSetMangas = (tabs: IMangaCategory[], tab: IMangaCategory, index: number) => {
|
||||
fetch(`http://127.0.0.1:4567/api/v1/category/${tab.category.id}`)
|
||||
.then((response) => response.json())
|
||||
.then((data: IManga[]) => {
|
||||
const tabsClone = JSON.parse(JSON.stringify(tabs));
|
||||
tabsClone[index].mangas = data;
|
||||
setTabs(tabsClone); // clone the object
|
||||
});
|
||||
};
|
||||
|
||||
const handleTabChange = (newTab: number) => {
|
||||
setTabNum(newTab);
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.category.order === newTab && tab.mangas.length === 0) {
|
||||
// mangas are empty, fetch the mangas
|
||||
fetchAndSetMangas(tabs, tab, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://127.0.0.1:4567/api/v1/library')
|
||||
.then((response) => response.json())
|
||||
.then((data: IManga[]) => {
|
||||
setMangas(data);
|
||||
});
|
||||
}, [lastPageNum]);
|
||||
// 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
|
||||
}
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
/>
|
||||
);
|
||||
// no default category so the first tab is 1
|
||||
setTabNum(1);
|
||||
return [];
|
||||
})
|
||||
.then(
|
||||
(newTabs: IMangaCategory[]) => {
|
||||
fetch('http://127.0.0.1:4567/api/v1/category')
|
||||
.then((response) => response.json())
|
||||
.then((data: ICategory[]) => {
|
||||
const mangaCategories = data.map((category) => ({
|
||||
category,
|
||||
mangas: [] as IManga[],
|
||||
}));
|
||||
const newNewTabs = [...newTabs, ...mangaCategories];
|
||||
setTabs(newNewTabs);
|
||||
|
||||
// if no default category, we must fetch the first tab now...
|
||||
// eslint-disable-next-line max-len
|
||||
if (newTabs.length === 0) { fetchAndSetMangas(newNewTabs, newNewTabs[0], 0); }
|
||||
});
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
let toRender;
|
||||
if (tabs.length > 1) {
|
||||
// eslint-disable-next-line max-len
|
||||
const tabDefines = tabs.map((tab) => (<Tab label={tab.category.name} value={tab.category.order} />));
|
||||
|
||||
const tabBodies = tabs.map((tab) => (
|
||||
<TabPanel value={tabNum} index={tab.category.order}>
|
||||
<MangaGrid
|
||||
mangas={tab.mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
/>
|
||||
</TabPanel>
|
||||
));
|
||||
|
||||
// 160px is min-width for viewport width of >600
|
||||
const scrollableTabs = window.innerWidth < tabs.length * 160;
|
||||
toRender = (
|
||||
<>
|
||||
<Tabs
|
||||
value={tabNum}
|
||||
onChange={(e, newTab) => handleTabChange(newTab)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
centered={!scrollableTabs}
|
||||
variant={scrollableTabs ? 'scrollable' : 'fullWidth'}
|
||||
scrollButtons="on"
|
||||
>
|
||||
{tabDefines}
|
||||
</Tabs>
|
||||
{tabBodies}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const mangas = tabs.length === 1 ? tabs[0].mangas : [];
|
||||
toRender = (
|
||||
<MangaGrid
|
||||
mangas={mangas}
|
||||
hasNextPage={false}
|
||||
lastPageNum={lastPageNum}
|
||||
setLastPageNum={setLastPageNum}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return toRender;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem, { ListItemProps } from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import InboxIcon from '@material-ui/icons/Inbox';
|
||||
import Brightness6Icon from '@material-ui/icons/Brightness6';
|
||||
import { ListItemSecondaryAction, Switch } from '@material-ui/core';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
import DarkTheme from '../context/DarkTheme';
|
||||
|
||||
function ListItemLink(props: ListItemProps<'a', { button?: true }>) {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return <ListItem button component="a" {...props} />;
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
setTitle('Settings');
|
||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<List component="nav" style={{ padding: 0 }}>
|
||||
<ListItemLink href="/settings/categories">
|
||||
<ListItemIcon>
|
||||
<InboxIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Categories" />
|
||||
</ListItemLink>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Brightness6Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dark Theme" />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={darkTheme}
|
||||
onChange={() => setDarkTheme(!darkTheme)}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
|
||||
import MangaGrid from '../components/MangaGrid';
|
||||
import NavBarTitle from '../context/NavbarTitle';
|
||||
|
||||
export default function MangaList(props: { popular: boolean }) {
|
||||
export default function SourceMangas(props: { popular: boolean }) {
|
||||
const { sourceId } = useParams<{sourceId: string}>();
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
const [mangas, setMangas] = useState<IManga[]>([]);
|
||||
@@ -0,0 +1,235 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
IconButton,
|
||||
} from '@material-ui/core';
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||
import DragHandleIcon from '@material-ui/icons/DragHandle';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import Fab from '@material-ui/core/Fab';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import NavBarTitle from '../../context/NavbarTitle';
|
||||
|
||||
const getItemStyle = (isDragging, draggableStyle, palette) => ({
|
||||
// styles we need to apply on draggables
|
||||
...draggableStyle,
|
||||
|
||||
...(isDragging && {
|
||||
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
|
||||
}),
|
||||
});
|
||||
|
||||
export default function Categories() {
|
||||
const { setTitle } = useContext(NavBarTitle);
|
||||
setTitle('Categories');
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
const [dialogValue, setDialogValue] = useState('');
|
||||
const theme = useTheme();
|
||||
|
||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialogOpen) {
|
||||
fetch('http://127.0.0.1:4567/api/v1/category/')
|
||||
.then((response) => response.json())
|
||||
.then((data) => setCategories(data));
|
||||
}
|
||||
}, [updateTriggerHolder]);
|
||||
|
||||
const categoryReorder = (list, from, to) => {
|
||||
const category = list[from];
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('from', from + 1);
|
||||
formData.append('to', to + 1);
|
||||
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}/reorder`, {
|
||||
method: 'PATCH',
|
||||
mode: 'cors',
|
||||
body: formData,
|
||||
}).finally(() => triggerUpdate());
|
||||
|
||||
// also move it in local state to avoid jarring moving behviour...
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(from, 1);
|
||||
result.splice(to, 0, removed);
|
||||
return result;
|
||||
};
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
// dropped outside the list?
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCategories(categoryReorder(
|
||||
categories,
|
||||
result.source.index,
|
||||
result.destination.index,
|
||||
));
|
||||
};
|
||||
|
||||
const handleDialogOpen = () => {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const resetDialog = () => {
|
||||
setDialogOpen(false);
|
||||
setDialogValue('');
|
||||
setCategoryToEdit(-1);
|
||||
};
|
||||
|
||||
const handleDialogCancel = () => {
|
||||
resetDialog();
|
||||
};
|
||||
|
||||
const handleDialogSubmit = () => {
|
||||
resetDialog();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', dialogValue);
|
||||
|
||||
if (categoryToEdit === -1) {
|
||||
fetch('http://127.0.0.1:4567/api/v1/category/', {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
body: formData,
|
||||
}).finally(() => triggerUpdate());
|
||||
} else {
|
||||
const category = categories[categoryToEdit];
|
||||
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
|
||||
method: 'PATCH',
|
||||
mode: 'cors',
|
||||
body: formData,
|
||||
}).finally(() => triggerUpdate());
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCategory = (index) => {
|
||||
const category = categories[index];
|
||||
fetch(`http://127.0.0.1:4567/api/v1/category/${category.id}`, {
|
||||
method: 'DELETE',
|
||||
mode: 'cors',
|
||||
}).finally(() => triggerUpdate());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<List ref={provided.innerRef}>
|
||||
{categories.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.id}
|
||||
draggableId={item.id.toString()}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<ListItem
|
||||
ContainerComponent="li"
|
||||
ContainerProps={{ ref: provided.innerRef }}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={getItemStyle(
|
||||
snapshot.isDragging,
|
||||
provided.draggableProps.style,
|
||||
theme.palette,
|
||||
)}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DragHandleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.name}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setCategoryToEdit(index);
|
||||
handleDialogOpen();
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteCategory(index);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</List>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="add"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2),
|
||||
}}
|
||||
onClick={handleDialogOpen}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">
|
||||
{categoryToEdit === -1 ? 'New Catalog' : `Rename: ${categories[categoryToEdit].name}`}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Enter new category name.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Category Name"
|
||||
type="text"
|
||||
fullWidth
|
||||
value={dialogValue}
|
||||
onChange={(e) => setDialogValue(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDialogCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleDialogSubmit} color="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
Vendored
+7
@@ -38,3 +38,10 @@ interface IChapter {
|
||||
mangaId: number
|
||||
pageCount: number
|
||||
}
|
||||
|
||||
interface ICategory {
|
||||
id: number
|
||||
order: number
|
||||
name: String
|
||||
isLanding: boolean
|
||||
}
|
||||
|
||||
+57
-3
@@ -3744,6 +3744,13 @@ css-blank-pseudo@^0.1.4:
|
||||
dependencies:
|
||||
postcss "^7.0.5"
|
||||
|
||||
css-box-model@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||
dependencies:
|
||||
tiny-invariant "^1.0.6"
|
||||
|
||||
css-color-names@0.0.4, css-color-names@^0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
|
||||
@@ -7344,6 +7351,11 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||
|
||||
memoize-one@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
|
||||
memory-fs@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||
@@ -9208,6 +9220,11 @@ querystringify@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
|
||||
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
|
||||
|
||||
raf-schd@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
|
||||
|
||||
raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
@@ -9257,6 +9274,19 @@ react-app-polyfill@^2.0.0:
|
||||
regenerator-runtime "^0.13.7"
|
||||
whatwg-fetch "^3.4.1"
|
||||
|
||||
react-beautiful-dnd@^13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
|
||||
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.4"
|
||||
css-box-model "^1.2.0"
|
||||
memoize-one "^5.1.1"
|
||||
raf-schd "^4.0.2"
|
||||
react-redux "^7.1.1"
|
||||
redux "^4.0.4"
|
||||
use-memo-one "^1.1.1"
|
||||
|
||||
react-dev-utils@^11.0.1:
|
||||
version "11.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-11.0.1.tgz#30106c2055acfd6b047d2dc478a85c356e66fe45"
|
||||
@@ -9301,7 +9331,7 @@ react-error-overlay@^6.0.8:
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"
|
||||
integrity sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==
|
||||
|
||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@@ -9311,6 +9341,17 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||
|
||||
react-redux@^7.1.1:
|
||||
version "7.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.2.tgz#03862e803a30b6b9ef8582dadcc810947f74b736"
|
||||
integrity sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.1"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.13.1"
|
||||
|
||||
react-refresh@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
@@ -9518,6 +9559,14 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
symbol-observable "^1.2.0"
|
||||
|
||||
regenerate-unicode-properties@^8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
|
||||
@@ -10672,7 +10721,7 @@ svgo@^1.0.0, svgo@^1.2.2:
|
||||
unquote "~1.1.1"
|
||||
util.promisify "~1.0.0"
|
||||
|
||||
symbol-observable@1.2.0:
|
||||
symbol-observable@1.2.0, symbol-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
@@ -10823,7 +10872,7 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||
|
||||
tiny-invariant@^1.0.2:
|
||||
tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||
@@ -11176,6 +11225,11 @@ url@^0.11.0:
|
||||
punycode "1.3.2"
|
||||
querystring "0.2.0"
|
||||
|
||||
use-memo-one@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
||||
|
||||
use@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
|
||||
Reference in New Issue
Block a user