Compare commits

...

14 Commits

Author SHA1 Message Date
Aria Moradi dfa59a1691 bump version to v0.4.2
CI Publish / Validate Gradle Wrapper (push) Successful in 11s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-30 04:04:11 +04:30
Aria Moradi 5023e96301 Implemented Dowloads front-end 2021-05-30 04:01:49 +04:30
Aria Moradi 224c24ee9f a little reminder 2021-05-30 02:21:43 +04:30
Aria Moradi e3b154cf9e Merge branch 'master' of github.com:Suwayomi/Tachidesk 2021-05-29 23:59:29 +04:30
Aria Moradi d249867c4c finishing touches of download backend, done @jipfr's requests 2021-05-29 23:57:22 +04:30
Aria Moradi b56045e984 downloader backend done 2021-05-29 23:05:51 +04:30
Manchewable 3777cc646e Improve continuous horizontal reader (#110)
* differentiate ContinuesHorizontalLTR and ContinuesHorizontalRTL

* fix displaying pages in horizontal viewer

* add scroll handler for horizontal mode

* update curPage when images pass through center of the screen

* add click events to navigate pages

* remove console.log

* fix click mapping for ContinuesHorizontalRTL

* remove disable eslint inline comment

* fix ContinuesHorizontalRTL not updating curPage on scroll

* add ability to click to drag

* add margin in between images
2021-05-29 19:41:59 +04:30
Manchewable aa5a1083d0 fit images to height (#108) 2021-05-28 23:27:31 +04:30
Manchewable 2ae5e0742e reference to img elements directly (#106) 2021-05-28 23:25:04 +04:30
Aria Moradi e5e875c54a closes #100 2021-05-28 20:21:05 +04:30
Aria Moradi 1a99ec76e4 spinner image, closes #77 2021-05-28 19:37:26 +04:30
Manchewable 1b122d1157 Add a Double Page Viewer (#105)
* add double page reader

* implement singleRTL

* add on image load handler

* add retry display time interval

* remove comments

* add double page wrapper

* fix image getting out of bounds

* remove comments

* remove unused styles

* return imageStyle as type CSSProperties

* rename DoublePagedReader to DoublePagedPager
2021-05-28 17:06:55 +04:30
Aria Moradi 77f2f8cc18 add copyright notice to files that miss it 2021-05-28 16:23:26 +04:30
Aria Moradi f0a99980b6 fixed issue with clearing up orphan chapters 2021-05-28 03:46:32 +04:30
37 changed files with 1342 additions and 200 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ sourceSets {
} }
// should be bumped with each stable release // should be bumped with each stable release
val tachideskVersion = "v0.4.1" val tachideskVersion = "v0.4.2"
// counts commit count on master // counts commit count on master
val tachideskRevision = runCatching { val tachideskRevision = runCatching {
@@ -1,5 +1,12 @@
package suwayomi.anime.impl.extension.github package suwayomi.anime.impl.extension.github
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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/. */
data class OnlineExtension( data class OnlineExtension(
val name: String, val name: String,
val pkgName: String, val pkgName: String,
@@ -0,0 +1,24 @@
package suwayomi.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
import suwayomi.server.database.migration.lib.Migration
@Suppress("ClassName", "unused")
class M0007_ChapterIsDownloaded : Migration() {
/** this migration added IS_DOWNLOADED to CHAPTER */
override fun run() {
with(TransactionManager.current()) {
exec("ALTER TABLE CHAPTER ADD COLUMN IS_DOWNLOADED BOOLEAN DEFAULT FALSE")
commit()
currentDialect.resetCaches()
}
}
}
@@ -0,0 +1,24 @@
package suwayomi.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
import suwayomi.server.database.migration.lib.Migration
@Suppress("ClassName", "unused")
class M0008_ChapterPageCount : Migration() {
/** this migration added PAGE_COUNT to CHAPTER */
override fun run() {
with(TransactionManager.current()) {
exec("ALTER TABLE CHAPTER ADD COLUMN PAGE_COUNT INT DEFAULT -1")
commit()
currentDialect.resetCaches()
}
}
}
@@ -0,0 +1,25 @@
package suwayomi.server.database.migration
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
import suwayomi.server.database.migration.lib.Migration
@Suppress("ClassName", "unused")
class M0009_ChapterLastReadAt : Migration() {
/** this migration added PAGE_COUNT to CHAPTER */
override fun run() {
with(TransactionManager.current()) {
// BIGINT == Long
exec("ALTER TABLE CHAPTER ADD COLUMN LAST_READ_AT BIGINT DEFAULT 0")
commit()
currentDialect.resetCaches()
}
}
}
@@ -1,5 +1,12 @@
package suwayomi.server.util package suwayomi.server.util
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 mu.KotlinLogging import mu.KotlinLogging
import kotlin.system.exitProcess import kotlin.system.exitProcess
@@ -33,6 +33,7 @@ import suwayomi.tachidesk.impl.Source.getSourceList
import suwayomi.tachidesk.impl.backup.BackupFlags import suwayomi.tachidesk.impl.backup.BackupFlags
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup
import suwayomi.tachidesk.impl.download.DownloadManager
import suwayomi.tachidesk.impl.extension.Extension.getExtensionIcon import suwayomi.tachidesk.impl.extension.Extension.getExtensionIcon
import suwayomi.tachidesk.impl.extension.Extension.installExtension import suwayomi.tachidesk.impl.extension.Extension.installExtension
import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension
@@ -383,15 +384,56 @@ object TachideskAPI {
// Download queue stats // Download queue stats
app.ws("/api/v1/downloads") { ws -> app.ws("/api/v1/downloads") { ws ->
ws.onConnect { ctx -> ws.onConnect { ctx ->
// TODO: send current stat DownloadManager.addClient(ctx)
// TODO: add to downlad subscribers DownloadManager.notifyClient(ctx)
} }
ws.onMessage { ws.onMessage { ctx ->
// TODO: send current stat DownloadManager.handleRequest(ctx)
} }
ws.onClose { ctx -> ws.onClose { ctx ->
// TODO: remove from subscribers DownloadManager.removeClient(ctx)
} }
} }
// Start the downloader
app.get("/api/v1/downloads/start") { ctx ->
DownloadManager.start()
ctx.status(200)
}
// Stop the downloader
app.get("/api/v1/downloads/stop") { ctx ->
DownloadManager.stop()
ctx.status(200)
}
// clear download queue
app.get("/api/v1/downloads/clear") { ctx ->
DownloadManager.clear()
ctx.status(200)
}
// Queue chapter for download
app.get("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
DownloadManager.enqueue(chapterIndex, mangaId)
ctx.status(200)
}
// delete chapter from download queue
app.delete("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx ->
val chapterIndex = ctx.pathParam("chapterIndex").toInt()
val mangaId = ctx.pathParam("mangaId").toInt()
DownloadManager.unqueue(chapterIndex, mangaId)
ctx.status(200)
}
} }
} }
@@ -24,6 +24,7 @@ import suwayomi.tachidesk.model.table.ChapterTable
import suwayomi.tachidesk.model.table.MangaTable import suwayomi.tachidesk.model.table.MangaTable
import suwayomi.tachidesk.model.table.PageTable import suwayomi.tachidesk.model.table.PageTable
import suwayomi.tachidesk.model.table.toDataClass import suwayomi.tachidesk.model.table.toDataClass
import java.time.Instant
object Chapter { object Chapter {
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
@@ -88,7 +89,7 @@ object Chapter {
// clear any orphaned chapters that are in the db but not in `chapterList` // clear any orphaned chapters that are in the db but not in `chapterList`
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
if (dbChapterCount > chapterCount) { // we got some clean up due if (dbChapterCount > chapterCount) { // we got some clean up due
val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId } } val dbChapterList = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.toList() }
dbChapterList.forEach { dbChapterList.forEach {
if (it[ChapterTable.chapterIndex] >= chapterList.size || if (it[ChapterTable.chapterIndex] >= chapterList.size ||
@@ -122,9 +123,14 @@ object Chapter {
dbChapter[ChapterTable.isRead], dbChapter[ChapterTable.isRead],
dbChapter[ChapterTable.isBookmarked], dbChapter[ChapterTable.isBookmarked],
dbChapter[ChapterTable.lastPageRead], dbChapter[ChapterTable.lastPageRead],
dbChapter[ChapterTable.lastReadAt],
chapterCount - index, chapterCount - index,
chapterList.size dbChapter[ChapterTable.isDownloaded],
dbChapter[ChapterTable.pageCount],
chapterList.size,
) )
} }
} }
@@ -136,54 +142,68 @@ object Chapter {
(ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId) (ChapterTable.chapterIndex eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first() }.first()
} }
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList( return if (!chapterEntry[ChapterTable.isDownloaded]) {
SChapter.create().apply { val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
url = chapterEntry[ChapterTable.url] val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
val chapterId = chapterEntry[ChapterTable.id].value val pageList = source.fetchPageList(
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() } SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
// update page list for this chapter val chapterId = chapterEntry[ChapterTable.id].value
transaction { val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() } // update page list for this chapter
if (pageEntry == null) { transaction {
PageTable.insert { pageList.forEach { page ->
it[index] = page.index val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
it[url] = page.url if (pageEntry == null) {
it[imageUrl] = page.imageUrl PageTable.insert {
it[chapter] = chapterId it[index] = page.index
} it[url] = page.url
} else { it[imageUrl] = page.imageUrl
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) { it[chapter] = chapterId
it[url] = page.url }
it[imageUrl] = page.imageUrl } else {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
}
} }
} }
} }
val pageCount = pageList.count()
transaction {
ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }) {
it[ChapterTable.pageCount] = pageCount
}
}
return ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.lastReadAt],
chapterEntry[ChapterTable.chapterIndex],
chapterEntry[ChapterTable.isDownloaded],
pageCount,
chapterCount.toInt()
)
} else {
ChapterTable.toDataClass(chapterEntry)
} }
return ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
mangaId,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.chapterIndex],
chapterCount.toInt(),
pageList.count()
)
} }
fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) { fun modifyChapter(mangaId: Int, chapterIndex: Int, isRead: Boolean?, isBookmarked: Boolean?, markPrevRead: Boolean?, lastPageRead: Int?) {
@@ -198,6 +218,7 @@ object Chapter {
} }
lastPageRead?.also { lastPageRead?.also {
update[ChapterTable.lastPageRead] = it update[ChapterTable.lastPageRead] = it
update[ChapterTable.lastReadAt] = Instant.now().epochSecond
} }
} }
} }
@@ -0,0 +1,129 @@
package suwayomi.tachidesk.impl.download
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 io.javalin.websocket.WsContext
import io.javalin.websocket.WsMessageContext
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.impl.download.model.DownloadChapter
import suwayomi.tachidesk.impl.download.model.DownloadState.Downloading
import suwayomi.tachidesk.impl.download.model.DownloadStatus
import suwayomi.tachidesk.model.table.ChapterTable
import suwayomi.tachidesk.model.table.toDataClass
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
object DownloadManager {
private val clients = ConcurrentHashMap<String, WsContext>()
private val downloadQueue = CopyOnWriteArrayList<DownloadChapter>()
private var downloader: Downloader? = null
fun addClient(ctx: WsContext) {
clients[ctx.sessionId] = ctx
}
fun removeClient(ctx: WsContext) {
clients.remove(ctx.sessionId)
}
fun notifyClient(ctx: WsContext) {
ctx.send(
getStatus()
)
}
fun handleRequest(ctx: WsMessageContext) {
when (ctx.message()) {
"STATUS" -> notifyClient(ctx)
else -> ctx.send(
"""
|Invalid command.
|Supported commands are:
| - STATUS
| sends the current download status
|""".trimMargin()
)
}
}
private fun notifyAllClients() {
val status = getStatus()
clients.forEach {
it.value.send(status)
}
}
private fun getStatus(): DownloadStatus {
return DownloadStatus(
if (downloader == null ||
downloadQueue.none { it.state == Downloading }
) "Stopped" else "Started",
downloadQueue
)
}
fun enqueue(chapterIndex: Int, mangaId: Int) {
if (downloadQueue.none { it.mangaId == mangaId && it.chapterIndex == chapterIndex }) {
downloadQueue.add(
DownloadChapter(
chapterIndex,
mangaId,
chapter = ChapterTable.toDataClass(
transaction {
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.chapterIndex eq chapterIndex) }
.first()
}
)
)
)
start()
}
notifyAllClients()
}
fun unqueue(chapterIndex: Int, mangaId: Int) {
downloadQueue.removeIf { it.mangaId == mangaId && it.chapterIndex == chapterIndex }
notifyAllClients()
}
fun start() {
if (downloader != null && !downloader?.isAlive!!) // doesn't exist or is dead
downloader = null
if (downloader == null) {
downloader = Downloader(downloadQueue) { notifyAllClients() }
downloader!!.start()
}
notifyAllClients()
}
fun stop() {
downloader?.let {
synchronized(it.shouldStop) {
it.shouldStop = true
}
}
downloader = null
notifyAllClients()
}
fun clear() {
stop()
downloadQueue.clear()
notifyAllClients()
}
}
enum class DownloaderState(val state: Int) {
Stopped(0),
Running(1),
Paused(2),
}
@@ -1,8 +1,5 @@
package suwayomi.tachidesk.impl.download package suwayomi.tachidesk.impl.download
import org.jetbrains.exposed.sql.ResultRow
import java.util.concurrent.LinkedBlockingQueue
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
@@ -10,19 +7,75 @@ import java.util.concurrent.LinkedBlockingQueue
* 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/. */
data class Download( import kotlinx.coroutines.runBlocking
val chapter: ResultRow, import org.jetbrains.exposed.sql.and
) import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.impl.Chapter.getChapter
import suwayomi.tachidesk.impl.Page.getPageImage
import suwayomi.tachidesk.impl.download.model.DownloadChapter
import suwayomi.tachidesk.impl.download.model.DownloadState.Downloading
import suwayomi.tachidesk.impl.download.model.DownloadState.Error
import suwayomi.tachidesk.impl.download.model.DownloadState.Finished
import suwayomi.tachidesk.impl.download.model.DownloadState.Queued
import suwayomi.tachidesk.model.table.ChapterTable
import java.util.concurrent.CopyOnWriteArrayList
private val downloadQueue = LinkedBlockingQueue<Download>() class Downloader(private val downloadQueue: CopyOnWriteArrayList<DownloadChapter>, val notifier: () -> Unit) : Thread() {
var shouldStop: Boolean = false
class Downloader { class DownloadShouldStopException : Exception()
fun start() { fun step() {
TODO() notifier()
synchronized(shouldStop) {
if (shouldStop) throw DownloadShouldStopException()
}
} }
fun stop() { override fun run() {
TODO() do {
val download = downloadQueue.firstOrNull {
it.state == Queued ||
(it.state == Error && it.tries < 3) // 3 re-tries per download
} ?: break
try {
download.state = Downloading
step()
download.chapter = runBlocking { getChapter(download.chapterIndex, download.mangaId) }
step()
val pageCount = download.chapter!!.pageCount
for (pageNum in 0 until pageCount) {
runBlocking { getPageImage(download.mangaId, download.chapterIndex, pageNum) }
// TODO: retry on error with 2,4,8 seconds of wait
// TODO: download multiple pages at once, possible solution: rx observer's strategy is used in Tachiyomi
// TODO: fine grained download percentage
download.progress = (pageNum + 1).toFloat() / pageCount
step()
}
download.state = Finished
transaction {
ChapterTable.update({ (ChapterTable.manga eq download.mangaId) and (ChapterTable.chapterIndex eq download.chapterIndex) }) {
it[isDownloaded] = true
}
}
step()
downloadQueue.removeIf { it.mangaId == download.mangaId && it.chapterIndex == download.chapterIndex }
step()
} catch (e: DownloadShouldStopException) {
println("Downloader was stopped")
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Queued }
} catch (e: Exception) {
println("Downloader faced an exception")
downloadQueue.filter { it.state == Downloading }.forEach { it.state = Error; it.tries++ }
e.printStackTrace()
} finally {
notifier()
}
} while (!shouldStop)
} }
} }
@@ -0,0 +1,19 @@
package suwayomi.tachidesk.impl.download.model
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 suwayomi.tachidesk.model.dataclass.ChapterDataClass
class DownloadChapter(
val chapterIndex: Int,
val mangaId: Int,
var state: DownloadState = DownloadState.Queued,
var progress: Float = 0f,
var tries: Int = 0,
var chapter: ChapterDataClass? = null,
)
@@ -0,0 +1,15 @@
package suwayomi.tachidesk.impl.download.model
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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/. */
enum class DownloadState(val state: Int) {
Queued(0),
Downloading(1),
Finished(2),
Error(3),
}
@@ -0,0 +1,13 @@
package suwayomi.tachidesk.impl.download.model
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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/. */
data class DownloadStatus(
val status: String,
val queue: List<DownloadChapter>,
)
@@ -1,5 +1,12 @@
package suwayomi.tachidesk.impl.extension.github package suwayomi.tachidesk.impl.extension.github
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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/. */
data class OnlineExtension( data class OnlineExtension(
val name: String, val name: String,
val pkgName: String, val pkgName: String,
@@ -42,19 +42,20 @@ object CachedImageResponse {
val response = fetcher() val response = fetcher()
if (response.code == 200) { if (response.code == 200) {
val fullPath = "$filePath.tmp" val tmpSavePath = "$filePath.tmp"
val saveFile = File(fullPath) val tmpSaveFile = File(tmpSavePath)
response.body!!.source().saveTo(saveFile) response.body!!.source().saveTo(tmpSaveFile)
// find image type // find image type
val imageType = response.headers["content-type"] val imageType = response.headers["content-type"]
?: ImageUtil.findImageType { saveFile.inputStream() }?.mime ?: ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
?: "image/jpeg" ?: "image/jpeg"
.substringAfter("image/")
saveFile.renameTo(File("$filePath.$imageType")) val actualSavePath = "$filePath.${imageType.substringAfter("/")}"
return pathToInputStream(fullPath) to imageType tmpSaveFile.renameTo(File(actualSavePath))
return pathToInputStream(actualSavePath) to imageType
} else { } else {
response.closeQuietly() response.closeQuietly()
throw Exception("request error! ${response.code}") throw Exception("request error! ${response.code}")
@@ -24,12 +24,19 @@ data class ChapterDataClass(
/** last read page, zero means not read/no data */ /** last read page, zero means not read/no data */
val lastPageRead: Int, val lastPageRead: Int,
/** last read page, zero means not read/no data */
val lastReadAt: Long,
/** this chapter's index, starts with 1 */ /** this chapter's index, starts with 1 */
val index: Int, val index: Int,
/** is chapter downloaded */
val downloaded: Boolean,
/** used to construct pages in the front-end */
val pageCount: Int = -1,
/** total chapter count, used to calculate if there's a next and prev chapter */ /** total chapter count, used to calculate if there's a next and prev chapter */
val chapterCount: Int? = null, val chapterCount: Int? = null,
/** used to construct pages in the front-end */
val pageCount: Int? = null,
) )
@@ -9,6 +9,8 @@ package suwayomi.tachidesk.model.table
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.model.dataclass.ChapterDataClass import suwayomi.tachidesk.model.dataclass.ChapterDataClass
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
@@ -21,10 +23,15 @@ object ChapterTable : IntIdTable() {
val isRead = bool("read").default(false) val isRead = bool("read").default(false)
val isBookmarked = bool("bookmark").default(false) val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0) val lastPageRead = integer("last_page_read").default(0)
val lastReadAt = long("last_read_at").default(0)
// index is reserved by a function // index is reserved by a function
val chapterIndex = integer("index") val chapterIndex = integer("index")
val isDownloaded = bool("is_downloaded").default(false)
val pageCount = integer("page_count").default(-1)
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
@@ -39,5 +46,9 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
chapterEntry[isRead], chapterEntry[isRead],
chapterEntry[isBookmarked], chapterEntry[isBookmarked],
chapterEntry[lastPageRead], chapterEntry[lastPageRead],
chapterEntry[lastReadAt],
chapterEntry[chapterIndex], chapterEntry[chapterIndex],
chapterEntry[isDownloaded],
chapterEntry[pageCount],
transaction { ChapterTable.select { ChapterTable.manga eq chapterEntry[manga].value }.count().toInt() },
) )
+1
View File
@@ -38,6 +38,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^17.0.2", "@types/react": "^17.0.2",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.2", "@types/react-dom": "^17.0.2",
"@types/react-lazyload": "^3.1.0", "@types/react-lazyload": "^3.1.0",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
+4
View File
@@ -34,6 +34,7 @@ import SourceAnimes from 'screens/anime/SourceAnimes';
import Reader from 'screens/manga/Reader'; import Reader from 'screens/manga/Reader';
import Player from 'screens/anime/Player'; import Player from 'screens/anime/Player';
import AnimeExtensions from 'screens/anime/AnimeExtensions'; import AnimeExtensions from 'screens/anime/AnimeExtensions';
import DownloadQueue from 'screens/manga/DownloadQueue';
export default function App() { export default function App() {
const [title, setTitle] = useState<string>('Tachidesk'); const [title, setTitle] = useState<string>('Tachidesk');
@@ -125,6 +126,9 @@ export default function App() {
<Route path="/manga/sources"> <Route path="/manga/sources">
<MangaSources /> <MangaSources />
</Route> </Route>
<Route path="/manga/downloads">
<DownloadQueue />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterNum"> <Route path="/manga/:mangaId/chapter/:chapterNum">
<></> <></>
</Route> </Route>
@@ -0,0 +1,67 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 CircularProgress from '@material-ui/core/CircularProgress';
interface IProps {
src: string
alt: string
imgRef?: React.RefObject<HTMLImageElement>
spinnerClassName?: string
imgClassName?: string
onImageLoad?: () => void
}
export default function SpinnerImage(props: IProps) {
const {
src, alt, onImageLoad, imgRef, spinnerClassName, imgClassName,
} = props;
const [imageSrc, setImagsrc] = useState<string>('');
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setImagsrc(src);
onImageLoad?.();
};
return () => {
img.onload = null;
};
}, [src]);
if (imageSrc.length === 0) {
return (
// <div className={`${classes.image} ${classes.loadingImage}`}>
<div className={spinnerClassName}>
<CircularProgress thickness={5} />
</div>
);
}
return (
<img
className={imgClassName}
ref={imgRef}
src={imageSrc}
alt={alt}
/>
);
}
SpinnerImage.defaultProps = {
spinnerClassName: '',
imgClassName: '',
onImageLoad: () => {},
imgRef: undefined,
};
@@ -14,6 +14,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark'; import CollectionsBookmarkIcon from '@material-ui/icons/CollectionsBookmark';
import ExploreIcon from '@material-ui/icons/Explore'; import ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension'; import ExtensionIcon from '@material-ui/icons/Extension';
import GetAppIcon from '@material-ui/icons/GetApp';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import SettingsIcon from '@material-ui/icons/Settings'; import SettingsIcon from '@material-ui/icons/Settings';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -87,6 +88,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Anime Sources" /> <ListItemText primary="Anime Sources" />
</ListItem> </ListItem>
</Link> </Link>
<Link to="/manga/downloads" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="Manga Download Queue">
<ListItemIcon>
<GetAppIcon />
</ListItemIcon>
<ListItemText primary="Manga Download Queue" />
</ListItem>
</Link>
<Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}> <Link to="/settings" style={{ color: 'inherit', textDecoration: 'none' }}>
<ListItem button key="settings"> <ListItem button key="settings">
<ListItemIcon> <ListItemIcon>
@@ -47,12 +47,13 @@ const useStyles = makeStyles((theme) => ({
interface IProps{ interface IProps{
chapter: IChapter chapter: IChapter
triggerChaptersUpdate: () => void triggerChaptersUpdate: () => void
downloadingString: string
} }
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const { chapter, triggerChaptersUpdate } = props; const { chapter, triggerChaptersUpdate, downloadingString } = props;
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10); const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
@@ -75,6 +76,11 @@ export default function ChapterCard(props: IProps) {
.then(() => triggerChaptersUpdate()); .then(() => triggerChaptersUpdate());
}; };
const downloadChapter = () => {
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
handleClose();
};
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0'; const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
return ( return (
<> <>
@@ -101,6 +107,7 @@ export default function ChapterCard(props: IProps) {
{chapter.scanlator} {chapter.scanlator}
{chapter.scanlator && ' '} {chapter.scanlator && ' '}
{dateStr} {dateStr}
{downloadingString}
</Typography> </Typography>
</div> </div>
</div> </div>
@@ -115,7 +122,8 @@ export default function ChapterCard(props: IProps) {
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
onClose={handleClose} onClose={handleClose}
> >
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */} {downloadingString.length === 0
&& <MenuItem onClick={downloadChapter}>Download</MenuItem> }
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}> <MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
{chapter.bookmarked && 'Remove bookmark'} {chapter.bookmarked && 'Remove bookmark'}
{!chapter.bookmarked && 'Bookmark'} {!chapter.bookmarked && 'Bookmark'}
+10 -6
View File
@@ -9,11 +9,11 @@ import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card'; import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea'; import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core'; import { Grid } from '@material-ui/core';
import useLocalStorage from 'util/useLocalStorage'; import useLocalStorage from 'util/useLocalStorage';
import SpinnerImage from 'components/SpinnerImage';
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
@@ -43,6 +43,11 @@ const useStyles = makeStyles({
height: '100%', height: '100%',
width: '100%', width: '100%',
}, },
spinner: {
minHeight: '400px',
padding: '180px calc(50% - 20px)',
},
}); });
interface IProps { interface IProps {
@@ -63,12 +68,11 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
<Card className={classes.root} ref={ref}> <Card className={classes.root} ref={ref}>
<CardActionArea> <CardActionArea>
<div className={classes.wrapper}> <div className={classes.wrapper}>
<CardMedia <SpinnerImage
className={classes.image}
component="img"
alt={title} alt={title}
image={serverAddress + thumbnailUrl} src={serverAddress + thumbnailUrl}
title={title} spinnerClassName={classes.spinner}
imgClassName={classes.image}
/> />
<div className={classes.gradient} /> <div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography> <Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
@@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
<div className={classes.top}> <div className={classes.top}>
<div className={classes.leftRight}> <div className={classes.leftRight}>
<div className={classes.leftSide}> <div className={classes.leftSide}>
<img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" /> <img src={`${serverAddress}${manga.thumbnailUrl}`} alt="Manga Thumbnail" />
</div> </div>
<div className={classes.rightSide}> <div className={classes.rightSide}>
<h1> <h1>
@@ -0,0 +1,62 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 { makeStyles } from '@material-ui/core/styles';
import React from 'react';
const useStyles = (settings: IReaderSettings) => makeStyles({
image: {
display: 'block',
marginBottom: 0,
width: 'auto',
minHeight: '99vh',
height: 'auto',
maxHeight: '99vh',
objectFit: 'contain',
},
page: {
display: 'flex',
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
justifyContent: 'center',
margin: '0 auto',
width: 'auto',
height: 'auto',
overflowX: 'scroll',
},
});
interface IProps {
index: number
image1src: string
image2src: string
settings: IReaderSettings
}
const DoublePage = React.forwardRef((props: IProps, ref: any) => {
const {
image1src, image2src, index, settings,
} = props;
const classes = useStyles(settings)();
return (
<div ref={ref} className={classes.page}>
<img
className={classes.image}
src={image1src}
alt={`Page #${index}`}
/>
<img
className={classes.image}
src={image2src}
alt={`Page #${index + 1}`}
/>
</div>
);
});
export default DoublePage;
@@ -5,9 +5,37 @@
* 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 CircularProgress from '@material-ui/core/CircularProgress';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useRef, useState } from 'react'; import { CSSProperties } from '@material-ui/core/styles/withStyles';
import React, { useEffect, useRef } from 'react';
import SpinnerImage from 'components/SpinnerImage';
function imageStyle(settings: IReaderSettings): CSSProperties {
if (settings.readerType === 'DoubleLTR'
|| settings.readerType === 'DoubleRTL'
|| settings.readerType === 'ContinuesHorizontalLTR'
|| settings.readerType === 'ContinuesHorizontalRTL') {
return {
display: 'block',
marginLeft: '7px',
marginRight: '7px',
width: 'auto',
minHeight: '99vh',
height: 'auto',
maxHeight: '99vh',
objectFit: 'contain',
pointerEvents: 'none',
};
}
return {
display: 'block',
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
minWidth: '50vw',
width: '100%',
maxWidth: '100%',
};
}
const useStyles = (settings: IReaderSettings) => makeStyles({ const useStyles = (settings: IReaderSettings) => makeStyles({
loading: { loading: {
@@ -22,87 +50,67 @@ const useStyles = (settings: IReaderSettings) => makeStyles({
backgroundColor: '#525252', backgroundColor: '#525252',
marginBottom: 10, marginBottom: 10,
}, },
image: { image: imageStyle(settings),
display: 'block',
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
minWidth: '50vw',
width: '100%',
maxWidth: '100%',
},
}); });
interface IProps { interface IProps {
src: string src: string
index: number index: number
onImageLoad: () => void
setCurPage: React.Dispatch<React.SetStateAction<number>> setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings settings: IReaderSettings
} }
function LazyImage(props: IProps) { const Page = React.forwardRef((props: IProps, ref: any) => {
const { const {
src, index, setCurPage, settings, src, index, onImageLoad, setCurPage, settings,
} = props; } = props;
const classes = useStyles(settings)(); const classes = useStyles(settings)();
const [imageSrc, setImagsrc] = useState<string>(''); const imgRef = useRef<HTMLImageElement>(null);
const ref = useRef<HTMLImageElement>(null);
const handleScroll = () => { const handleVerticalScroll = () => {
if (ref.current) { if (imgRef.current) {
const rect = ref.current.getBoundingClientRect(); const rect = imgRef.current.getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) { if (rect.y < 0 && rect.y + rect.height > 0) {
setCurPage(index); setCurPage(index);
} }
} }
}; };
useEffect(() => { const handleHorizontalScroll = () => {
if (settings.readerType === 'Webtoon' || settings.readerType === 'ContinuesVertical') { if (imgRef.current) {
window.addEventListener('scroll', handleScroll); const rect = imgRef.current.getBoundingClientRect();
if (rect.left <= window.innerWidth / 2 && rect.right > window.innerWidth / 2) {
return () => { setCurPage(index);
window.removeEventListener('scroll', handleScroll); }
}; }
} return () => {}; };
}, [handleScroll]);
useEffect(() => { useEffect(() => {
const img = new Image(); switch (settings.readerType) {
img.src = src; case 'Webtoon':
case 'ContinuesVertical':
img.onload = () => setImagsrc(src); window.addEventListener('scroll', handleVerticalScroll);
}, [src]); return () => window.removeEventListener('scroll', handleVerticalScroll);
case 'ContinuesHorizontalLTR':
if (imageSrc.length === 0) { case 'ContinuesHorizontalRTL':
return ( window.addEventListener('scroll', handleHorizontalScroll);
<div className={`${classes.image} ${classes.loadingImage}`}> return () => window.removeEventListener('scroll', handleHorizontalScroll);
<CircularProgress thickness={5} /> default:
</div> return () => {};
); }
} }, [handleVerticalScroll]);
return (
<img
className={classes.image}
ref={ref}
src={imageSrc}
alt={`Page #${index}`}
/>
);
}
const Page = React.forwardRef((props: IProps, ref: any) => {
const {
src, index, setCurPage, settings,
} = props;
return ( return (
<div ref={ref} style={{ margin: '0 auto' }}> <div ref={ref} style={{ margin: '0 auto' }}>
<LazyImage <SpinnerImage
src={src} src={src}
index={index} onImageLoad={onImageLoad}
setCurPage={setCurPage} alt={`Page #${index}`}
settings={settings} imgRef={imgRef}
spinnerClassName={`${classes.image} ${classes.loadingImage}`}
imgClassName={classes.image}
/> />
</div> </div>
); );
@@ -0,0 +1,212 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 { makeStyles } from '@material-ui/core/styles';
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import Page from '../Page';
import DoublePage from '../DoublePage';
const useStyles = (settings: IReaderSettings) => makeStyles({
preload: {
display: 'none',
},
reader: {
display: 'flex',
flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse',
justifyContent: 'center',
margin: '0 auto',
width: 'auto',
height: 'auto',
overflowX: 'scroll',
},
});
export default function DoublePagedPager(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
} = props;
const classes = useStyles(settings)();
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLImageElement[]>([]);
const pagesDisplayed = useRef<number>(0);
const pageLoaded = useRef<boolean[]>(Array(pages.length).fill(false));
function setPagesToDisplay() {
pagesDisplayed.current = 0;
if (curPage < pages.length && pagesRef.current[curPage]) {
if (pageLoaded.current[curPage]) {
pagesDisplayed.current = 1;
const imgElem = pagesRef.current[curPage];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return;
}
}
}
if (curPage + 1 < pages.length && pagesRef.current[curPage + 1]) {
if (pageLoaded.current[curPage + 1]) {
const imgElem = pagesRef.current[curPage + 1];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return;
}
pagesDisplayed.current = 2;
}
}
}
function displayPages() {
if (pagesDisplayed.current === 2) {
ReactDOM.render(
<DoublePage
key={curPage}
index={curPage}
image1src={pages[curPage].src}
image2src={pages[curPage + 1].src}
settings={settings}
/>,
document.getElementById('display'),
);
} else {
ReactDOM.render(
<Page
key={curPage}
index={curPage}
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
/>,
document.getElementById('display'),
);
}
}
function pagesToGoBack() {
for (let i = 1; i <= 2; i++) {
if (curPage - i > 0 && pagesRef.current[curPage - i]) {
if (pageLoaded.current[curPage - i]) {
const imgElem = pagesRef.current[curPage - i];
const aspectRatio = imgElem.height / imgElem.width;
if (aspectRatio < 1) {
return 1;
}
}
}
}
return 2;
}
function nextPage() {
if (curPage < pages.length - 1) {
const nextCurPage = curPage + pagesDisplayed.current;
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
const nextCurPage = curPage - pagesToGoBack();
setCurPage((nextCurPage < 0) ? 0 : nextCurPage);
} else {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'DoubleLTR') {
prevPage();
} else {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'DoubleLTR') {
nextPage();
} else {
prevPage();
}
}
function keyboardControl(e:KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
nextPage();
break;
case 'ArrowRight':
goRight();
break;
case 'ArrowLeft':
goLeft();
break;
default:
break;
}
}
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
goRight();
} else {
goLeft();
}
}
function handleImageLoad(index: number) {
return () => {
pageLoaded.current[index] = true;
};
}
useEffect(() => {
const retryDisplay = setInterval(() => {
const isLastPage = (curPage === pages.length - 1);
if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1])
|| pageLoaded.current[curPage]) {
setPagesToDisplay();
displayPages();
clearInterval(retryDisplay);
}
}, 50);
document.addEventListener('keydown', keyboardControl);
selfRef.current?.addEventListener('click', clickControl);
return () => {
clearInterval(retryDisplay);
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage, settings.readerType]);
return (
<div ref={selfRef}>
<div id="preload" className={classes.preload}>
{
pages.map((page) => (
<img
ref={(e:HTMLImageElement) => { pagesRef.current[page.index] = e; }}
key={`${page.index}`}
src={page.src}
onLoad={handleImageLoad(page.index)}
alt={`${page.index}`}
/>
))
}
</div>
<div id="display" className={classes.reader} />
</div>
);
}
@@ -6,42 +6,139 @@
* 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 { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import React from 'react'; import React, { useEffect, useRef } from 'react';
import Page from '../Page'; import Page from '../Page';
const useStyles = makeStyles({ const useStyles = (settings: IReaderSettings) => makeStyles({
reader: { reader: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
justifyContent: 'center', justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
margin: '0 auto', margin: '0 auto',
width: '100%', width: 'auto',
height: '100vh', height: 'auto',
overflowX: 'scroll', overflowX: 'visible',
userSelect: 'none',
}, },
}); });
interface IProps { export default function HorizontalPager(props: IReaderProps) {
pages: Array<IReaderPage> const {
setCurPage: React.Dispatch<React.SetStateAction<number>> pages, curPage, settings, setCurPage, prevChapter, nextChapter,
settings: IReaderSettings } = props;
}
export default function HorizontalPager(props: IProps) { const classes = useStyles(settings)();
const { pages, settings, setCurPage } = props;
const classes = useStyles(); const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);
function nextPage() {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
setCurPage((page) => page + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
function prevPage() {
if (curPage > 0) {
pagesRef.current[curPage - 1]?.scrollIntoView({ inline: 'center' });
setCurPage(curPage - 1);
} else if (curPage === 0) {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'ContinuesHorizontalLTR') {
prevPage();
} else {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'ContinuesHorizontalLTR') {
nextPage();
} else {
prevPage();
}
}
const mouseXPos = useRef<number>(0);
function dragScreen(e: MouseEvent) {
window.scrollBy(mouseXPos.current - e.pageX, 0);
}
function dragControl(e:MouseEvent) {
mouseXPos.current = e.pageX;
selfRef.current?.addEventListener('mousemove', dragScreen);
}
function removeDragControl() {
selfRef.current?.removeEventListener('mousemove', dragScreen);
}
function clickControl(e:MouseEvent) {
if (e.clientX >= window.innerWidth * 0.85) {
goRight();
} else if (e.clientX <= window.innerWidth * 0.15) {
goLeft();
}
}
const handleLoadNextonEnding = () => {
if (settings.readerType === 'ContinuesHorizontalLTR') {
if (window.scrollX + window.innerWidth >= document.body.scrollWidth) {
nextChapter();
}
} else if (settings.readerType === 'ContinuesHorizontalRTL') {
if (window.scrollX <= window.innerWidth) {
nextChapter();
}
}
};
useEffect(() => {
pagesRef.current[curPage]?.scrollIntoView({ inline: 'center' });
}, [settings.readerType]);
useEffect(() => {
selfRef.current?.addEventListener('mousedown', dragControl);
selfRef.current?.addEventListener('mouseup', removeDragControl);
return () => {
selfRef.current?.removeEventListener('mousedown', dragControl);
selfRef.current?.removeEventListener('mouseup', removeDragControl);
};
}, [selfRef]);
useEffect(() => {
if (settings.loadNextonEnding) {
document.addEventListener('scroll', handleLoadNextonEnding);
}
selfRef.current?.addEventListener('mousedown', clickControl);
return () => {
document.removeEventListener('scroll', handleLoadNextonEnding);
selfRef.current?.removeEventListener('mousedown', clickControl);
};
}, [selfRef, curPage]);
return ( return (
<div className={classes.reader}> <div ref={selfRef} className={classes.reader}>
{ {
pages.map((page) => ( pages.map((page) => (
<Page <Page
key={page.index} key={page.index}
index={page.index} index={page.index}
src={page.src} src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage} setCurPage={setCurPage}
settings={settings} settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/> />
)) ))
} }
@@ -38,7 +38,27 @@ export default function PagedReader(props: IReaderProps) {
} }
function prevPage() { function prevPage() {
if (curPage > 0) { setCurPage(curPage - 1); } else if (curPage === 0) { prevChapter(); } if (curPage > 0) {
setCurPage(curPage - 1);
} else {
prevChapter();
}
}
function goLeft() {
if (settings.readerType === 'SingleLTR') {
prevPage();
} else if (settings.readerType === 'SingleRTL') {
nextPage();
}
}
function goRight() {
if (settings.readerType === 'SingleLTR') {
nextPage();
} else if (settings.readerType === 'SingleRTL') {
prevPage();
}
} }
function keyboardControl(e:KeyboardEvent) { function keyboardControl(e:KeyboardEvent) {
@@ -48,10 +68,10 @@ export default function PagedReader(props: IReaderProps) {
nextPage(); nextPage();
break; break;
case 'ArrowRight': case 'ArrowRight':
nextPage(); goRight();
break; break;
case 'ArrowLeft': case 'ArrowLeft':
prevPage(); goLeft();
break; break;
default: default:
break; break;
@@ -60,9 +80,9 @@ export default function PagedReader(props: IReaderProps) {
function clickControl(e:MouseEvent) { function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) { if (e.clientX > window.innerWidth / 2) {
nextPage(); goRight();
} else { } else {
prevPage(); goLeft();
} }
} }
@@ -74,13 +94,14 @@ export default function PagedReader(props: IReaderProps) {
document.removeEventListener('keydown', keyboardControl); document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl); selfRef.current?.removeEventListener('click', clickControl);
}; };
}, [selfRef, curPage]); }, [selfRef, curPage, settings.readerType]);
return ( return (
<div ref={selfRef} className={classes.reader}> <div ref={selfRef} className={classes.reader}>
<Page <Page
key={curPage} key={curPage}
index={curPage} index={curPage}
onImageLoad={() => {}}
src={pages[curPage].src} src={pages[curPage].src}
setCurPage={setCurPage} setCurPage={setCurPage}
settings={settings} settings={settings}
@@ -29,6 +29,10 @@ export default function VerticalReader(props: IReaderProps) {
const selfRef = useRef<HTMLDivElement>(null); const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]); const pagesRef = useRef<HTMLDivElement[]>([]);
useEffect(() => {
pagesRef.current = pagesRef.current.slice(0, pages.length);
}, [pages.length]);
function nextPage() { function nextPage() {
if (curPage < pages.length - 1) { if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView(); pagesRef.current[curPage + 1]?.scrollIntoView();
@@ -104,7 +108,7 @@ export default function VerticalReader(props: IReaderProps) {
if (initialPage > -1) { if (initialPage > -1) {
pagesRef.current[initialPage].scrollIntoView(); pagesRef.current[initialPage].scrollIntoView();
} }
}, []); }, [pagesRef.current.length]);
return ( return (
<div ref={selfRef} className={classes.reader}> <div ref={selfRef} className={classes.reader}>
@@ -114,6 +118,7 @@ export default function VerticalReader(props: IReaderProps) {
key={page.index} key={page.index}
index={page.index} index={page.index}
src={page.src} src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage} setCurPage={setCurPage}
settings={settings} settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }} ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
@@ -286,17 +286,25 @@ export default function ReaderNavBar(props: IProps) {
onChange={(e) => setSettingValue('readerType', e.target.value)} onChange={(e) => setSettingValue('readerType', e.target.value)}
> >
<MenuItem value="SingleLTR"> <MenuItem value="SingleLTR">
Left to right Single Page (LTR)
</MenuItem> </MenuItem>
{/* <MenuItem value="SingleRTL"> <MenuItem value="SingleRTL">
Right to left(WIP) Single Page (RTL)
</MenuItem> */} </MenuItem>
{/* <MenuItem value="SingleVertical"> {/* <MenuItem value="SingleVertical">
Vertical(WIP) Vertical(WIP)
</MenuItem> */} </MenuItem> */}
<MenuItem value="DoubleLTR">
Double Page (LTR)
</MenuItem>
<MenuItem value="DoubleRTL">
Double Page (RTL)
</MenuItem>
<MenuItem value="Webtoon"> <MenuItem value="Webtoon">
Webtoon Webtoon
@@ -305,10 +313,14 @@ export default function ReaderNavBar(props: IProps) {
Continues Vertical Continues Vertical
</MenuItem> </MenuItem>
{/* <MenuItem value="ContinuesHorizontal"> <MenuItem value="ContinuesHorizontalLTR">
Horizontal(WIP) Horizontal (LTR)
</MenuItem> */} </MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select> </Select>
</ListItem> </ListItem>
</List> </List>
@@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/*
* Copyright (C) Contributors to the Suwayomi project
*
* 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 NavbarContext from 'context/NavbarContext';
import React, { useContext, useEffect, useState } from 'react';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import PauseIcon from '@material-ui/icons/Pause';
import IconButton from '@material-ui/core/IconButton';
import client from 'util/client';
import {
DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle,
} from 'react-beautiful-dnd';
import { useTheme } from '@material-ui/core/styles';
import { Palette } from '@material-ui/core/styles/createPalette';
import List from '@material-ui/core/List';
import DragHandleIcon from '@material-ui/icons/DragHandle';
import ListItem from '@material-ui/core/ListItem';
import { ListItemIcon } from '@material-ui/core';
import ListItemText from '@material-ui/core/ListItemText';
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
const getItemStyle = (isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
// styles we need to apply on draggables
...draggableStyle,
...(isDragging && {
background: palette.type === 'dark' ? '#424242' : 'rgb(235,235,235)',
}),
});
const initialQueue = {
status: 'Stopped',
queue: [],
} as IQueue;
export default function DownloadQueue() {
const [, setWsClient] = useState<WebSocket>();
const [queueState, setQueueState] = useState<IQueue>(initialQueue);
const { queue, status } = queueState;
const theme = useTheme();
const { setTitle, setAction } = useContext(NavbarContext);
const toggleQueueStatus = () => {
if (status === 'Stopped') {
client.get('/api/v1/downloads/start');
} else {
client.get('/api/v1/downloads/stop');
}
};
useEffect(() => {
setTitle('Download Queue');
setAction(() => {
if (status === 'Stopped') {
return (
<IconButton onClick={toggleQueueStatus}>
<PlayArrowIcon />
</IconButton>
);
}
return (
<IconButton onClick={toggleQueueStatus}>
<PauseIcon />
</IconButton>
);
});
}, [status]);
useEffect(() => {
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
wsc.onmessage = (e) => {
setQueueState(JSON.parse(e.data));
};
setWsClient(wsc);
}, []);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onDragEnd = (result: DropResult) => {
};
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<List ref={provided.innerRef}>
{queue.map((item, index) => (
<Draggable
key={`${item.mangaId}-${item.chapterIndex}`}
draggableId={`${item.mangaId}-${item.chapterIndex}`}
index={index}
>
{(provided, snapshot) => (
<ListItem
ContainerProps={{ ref: provided.innerRef } as any}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style,
theme.palette,
)}
ref={provided.innerRef}
>
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
<ListItemText
primary={
`${item.chapter.name} | `
+ ` (${item.progress * 100}%)`
+ ` => state: ${item.state}`
}
/>
{/* <IconButton
onClick={() => {
handleEditDialogOpen(index);
}}
>
<EditIcon />
</IconButton>
<IconButton
onClick={() => {
deleteCategory(index);
}}
>
<DeleteIcon />
</IconButton> */}
</ListItem>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
</>
);
}
+45
View File
@@ -41,6 +41,12 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
})); }));
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
const initialQueue = {
status: 'Stopped',
queue: [],
} as IQueue;
export default function Manga() { export default function Manga() {
const classes = useStyles(); const classes = useStyles();
@@ -55,10 +61,48 @@ export default function Manga() {
const [noChaptersFound, setNoChaptersFound] = useState(false); const [noChaptersFound, setNoChaptersFound] = useState(false);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0); const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
const [, setWsClient] = useState<WebSocket>();
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
function triggerChaptersUpdate() { function triggerChaptersUpdate() {
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1); setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
} }
useEffect(() => {
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
wsc.onmessage = (e) => {
const data = JSON.parse(e.data) as IQueue;
setQueueState(data);
let shouldUpdate = false;
data.queue.forEach((q) => {
if (q.mangaId === manga?.id && q.state === 'Finished') {
shouldUpdate = true;
}
});
if (shouldUpdate) {
triggerChaptersUpdate();
}
};
setWsClient(wsc);
return () => wsc.close();
}, [queue.length]);
const downloadingStringFor = (chapter: IChapter) => {
let rtn = '';
if (chapter.downloaded) {
rtn = ' • Downloaded';
}
queue.forEach((q) => {
if (chapter.index === q.chapterIndex && chapter.mangaId === q.mangaId) {
rtn = ` • Downloading (${q.progress * 100}%)`;
}
});
return rtn;
};
useEffect(() => { useEffect(() => {
if (manga === undefined || !manga.freshData) { if (manga === undefined || !manga.freshData) {
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`) client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
@@ -105,6 +149,7 @@ export default function Manga() {
itemContent={(index:number) => ( itemContent={(index:number) => (
<ChapterCard <ChapterCard
chapter={chapters[index]} chapter={chapters[index]}
downloadingString={downloadingStringFor(chapters[index])}
triggerChaptersUpdate={triggerChaptersUpdate} triggerChaptersUpdate={triggerChaptersUpdate}
/> />
)} )}
+10 -9
View File
@@ -11,7 +11,8 @@ import React, { useContext, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import HorizontalPager from 'components/manga/reader/pager/HorizontalPager'; import HorizontalPager from 'components/manga/reader/pager/HorizontalPager';
import PageNumber from 'components/manga/reader/PageNumber'; import PageNumber from 'components/manga/reader/PageNumber';
import WebtoonPager from 'components/manga/reader/pager/PagedPager'; import PagedPager from 'components/manga/reader/pager/PagedPager';
import DoublePagedPager from 'components/manga/reader/pager/DoublePagedPager';
import VerticalPager from 'components/manga/reader/pager/VerticalPager'; import VerticalPager from 'components/manga/reader/pager/VerticalPager';
import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar'; import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar';
import NavbarContext from 'context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
@@ -32,21 +33,21 @@ const useStyles = (settings: IReaderSettings) => makeStyles({
const getReaderComponent = (readerType: ReaderType) => { const getReaderComponent = (readerType: ReaderType) => {
switch (readerType) { switch (readerType) {
case 'ContinuesVertical': case 'ContinuesVertical':
return VerticalPager;
break;
case 'Webtoon': case 'Webtoon':
return VerticalPager; return VerticalPager;
break; break;
case 'SingleVertical': case 'SingleVertical':
return WebtoonPager;
break;
case 'SingleRTL': case 'SingleRTL':
return WebtoonPager;
break;
case 'SingleLTR': case 'SingleLTR':
return WebtoonPager; return PagedPager;
break; break;
case 'ContinuesHorizontal': case 'DoubleVertical':
case 'DoubleRTL':
case 'DoubleLTR':
return DoublePagedPager;
break;
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
return HorizontalPager; return HorizontalPager;
default: default:
return VerticalPager; return VerticalPager;
@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
/* /*
* Copyright (C) Contributors to the Suwayomi project * Copyright (C) Contributors to the Suwayomi project
* *
@@ -5,9 +8,6 @@
* 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/. */
/* 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 React, { useState, useContext, useEffect } from 'react';
import { import {
List, List,
@@ -16,7 +16,9 @@ import {
ListItemIcon, ListItemIcon,
IconButton, IconButton,
} from '@material-ui/core'; } from '@material-ui/core';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import {
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
} from 'react-beautiful-dnd';
import DragHandleIcon from '@material-ui/icons/DragHandle'; import DragHandleIcon from '@material-ui/icons/DragHandle';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
import { useTheme } from '@material-ui/core/styles'; import { useTheme } from '@material-ui/core/styles';
@@ -31,10 +33,12 @@ import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from '@material-ui/core/DialogTitle';
import Checkbox from '@material-ui/core/Checkbox'; import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
import NavbarContext from '../../context/NavbarContext'; import NavbarContext from 'context/NavbarContext';
import client from '../../util/client'; import client from 'util/client';
import { Palette } from '@material-ui/core/styles/createPalette';
const getItemStyle = (isDragging, draggableStyle, palette) => ({ const getItemStyle = (isDragging: boolean,
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
// styles we need to apply on draggables // styles we need to apply on draggables
...draggableStyle, ...draggableStyle,
@@ -47,14 +51,14 @@ export default function Categories() {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []); useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState<ICategory[]>([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [dialogName, setDialogName] = useState(''); const [dialogName, setDialogName] = useState<string>('');
const [dialogDefault, setDialogDefault] = useState(false); const [dialogDefault, setDialogDefault] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
useEffect(() => { useEffect(() => {
@@ -65,12 +69,12 @@ export default function Categories() {
} }
}, [updateTriggerHolder]); }, [updateTriggerHolder]);
const categoryReorder = (list, from, to) => { const categoryReorder = (list: ICategory[], from: number, to: number) => {
const category = list[from]; const category = list[from];
const formData = new FormData(); const formData = new FormData();
formData.append('from', from + 1); formData.append('from', `${from + 1}`);
formData.append('to', to + 1); formData.append('to', `${to + 1}`);
client.post(`/api/v1/category/${category.id}/reorder`, formData) client.post(`/api/v1/category/${category.id}/reorder`, formData)
.finally(() => triggerUpdate()); .finally(() => triggerUpdate());
@@ -81,7 +85,7 @@ export default function Categories() {
return result; return result;
}; };
const onDragEnd = (result) => { const onDragEnd = (result: DropResult) => {
// dropped outside the list? // dropped outside the list?
if (!result.destination) { if (!result.destination) {
return; return;
@@ -105,7 +109,7 @@ export default function Categories() {
setDialogOpen(true); setDialogOpen(true);
}; };
const handleEditDialogOpen = (index) => { const handleEditDialogOpen = (index:number) => {
setDialogName(categories[index].name); setDialogName(categories[index].name);
setDialogDefault(categories[index].default); setDialogDefault(categories[index].default);
setCategoryToEdit(index); setCategoryToEdit(index);
@@ -121,7 +125,7 @@ export default function Categories() {
const formData = new FormData(); const formData = new FormData();
formData.append('name', dialogName); formData.append('name', dialogName);
formData.append('default', dialogDefault); formData.append('default', dialogDefault.toString());
if (categoryToEdit === -1) { if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData) client.post('/api/v1/category/', formData)
@@ -133,7 +137,7 @@ export default function Categories() {
} }
}; };
const deleteCategory = (index) => { const deleteCategory = (index:number) => {
const category = categories[index]; const category = categories[index];
client.delete(`/api/v1/category/${category.id}`) client.delete(`/api/v1/category/${category.id}`)
.finally(() => triggerUpdate()); .finally(() => triggerUpdate());
@@ -153,8 +157,7 @@ export default function Categories() {
> >
{(provided, snapshot) => ( {(provided, snapshot) => (
<ListItem <ListItem
ContainerComponent="li" ContainerProps={{ ref: provided.innerRef } as any}
ContainerProps={{ ref: provided.innerRef }}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
style={getItemStyle( style={getItemStyle(
+20 -2
View File
@@ -67,6 +67,7 @@ interface IChapter {
index: number index: number
chapterCount: number chapterCount: number
pageCount: number pageCount: number
downloaded: boolean
} }
interface IEpisode { interface IEpisode {
@@ -99,7 +100,7 @@ interface IPartialEpisode {
interface ICategory { interface ICategory {
id: number id: number
order: number order: number
name: String name: string
default: boolean default: boolean
} }
@@ -114,7 +115,11 @@ type ReaderType =
'SingleVertical' | 'SingleVertical' |
'SingleRTL' | 'SingleRTL' |
'SingleLTR' | 'SingleLTR' |
'ContinuesHorizontal'; 'DoubleVertical' |
'DoubleRTL' |
'DoubleLTR' |
'ContinuesHorizontalLTR'|
'ContinuesHorizontalRTL';
interface IReaderSettings{ interface IReaderSettings{
staticNav: boolean staticNav: boolean
@@ -149,3 +154,16 @@ interface IAbout {
github: string github: string
discord: string discord: string
} }
interface IDownloadChapter{
chapterIndex: number
mangaId: number
state: 'Queued' | 'Downloading' | 'Finished' | 'Error'
progress: number
chapter: IChapter
}
interface IQueue {
status: 'Stopped' | 'Started'
queue: IDownloadChapter[]
}
+7
View File
@@ -1891,6 +1891,13 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
"@types/react-beautiful-dnd@^13.0.0":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
dependencies:
"@types/react" "*"
"@types/react-dom@^17.0.2": "@types/react-dom@^17.0.2":
version "17.0.5" version "17.0.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227"