Compare commits

..

21 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
Aria Moradi b0d43ffe69 anime filter everywhere
CI Publish / Validate Gradle Wrapper (push) Successful in 12s
CI Publish / Build artifacts and release (push) Failing after 17s
2021-05-28 03:02:14 +04:30
Aria Moradi 16cb0184a4 fix catalog source imports 2021-05-28 02:53:36 +04:30
Aria Moradi f211a33ea3 bump to v0.4.1 2021-05-28 02:49:01 +04:30
Aria Moradi 440c815189 missed from previous commit 2021-05-28 02:46:19 +04:30
Aria Moradi 25829aacfd new anime library 2021-05-28 02:43:30 +04:30
Aria Moradi 700a739f95 probably fixes http leaks (by @Syer10) 2021-05-27 22:45:44 +04:30
Aria Moradi d9620bec05 fix getManga returning false for inLibrary 2021-05-27 22:30:29 +04:30
63 changed files with 1444 additions and 255 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ sourceSets {
}
// should be bumped with each stable release
val tachideskVersion = "v0.4.0"
val tachideskVersion = "v0.4.2"
// counts commit count on master
val tachideskRevision = runCatching {
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source
package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import rx.Observable
interface AnimeCatalogueSource : AnimeSource {
@@ -30,7 +30,7 @@ interface AnimeCatalogueSource : AnimeSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
fun fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable<AnimesPage>
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
/**
* Returns an observable containing a page with a list of latest anime updates.
@@ -42,5 +42,5 @@ interface AnimeCatalogueSource : AnimeSource {
/**
* Returns the list of filters for the source.
*/
fun getFilterList(): FilterList
fun getFilterList(): AnimeFilterList
}
@@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source
package eu.kanade.tachiyomi.animesource
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import rx.Observable
/**
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source
package eu.kanade.tachiyomi.animesource
/**
* A factory for creating sources at runtime.
@@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.source
package eu.kanade.tachiyomi.animesource
import android.content.Context
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import rx.Observable
open class AnimeSourceManager(private val context: Context) {
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source
package eu.kanade.tachiyomi.animesource
import android.support.v7.preference.PreferenceScreen
@@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.animesource.model
sealed class AnimeFilter<T>(val name: String, var state: T) {
open class Header(name: String) : AnimeFilter<Any>(name, 0)
open class Separator(name: String = "") : AnimeFilter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : AnimeFilter<Int>(name, state)
abstract class Text(name: String, state: String = "") : AnimeFilter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : AnimeFilter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : AnimeFilter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE
companion object {
const val STATE_IGNORE = 0
const val STATE_INCLUDE = 1
const val STATE_EXCLUDE = 2
}
}
abstract class Group<V>(name: String, state: List<V>) : AnimeFilter<List<V>>(name, state)
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
AnimeFilter<Sort.Selection?>(name, state) {
data class Selection(val index: Int, val ascending: Boolean)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AnimeFilter<*>) return false
return name == other.name && state == other.state
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (state?.hashCode() ?: 0)
return result
}
}
@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.animesource.model
data class AnimeFilterList(val list: List<AnimeFilter<*>>) : List<AnimeFilter<*>> by list {
constructor(vararg fs: AnimeFilter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
}
@@ -1,3 +1,3 @@
package eu.kanade.tachiyomi.source.model
package eu.kanade.tachiyomi.animesource.model
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source.model
package eu.kanade.tachiyomi.animesource.model
import java.io.Serializable
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source.model
package eu.kanade.tachiyomi.animesource.model
class SAnimeImpl : SAnime {
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source.model
package eu.kanade.tachiyomi.animesource.model
import java.io.Serializable
@@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.source.model
package eu.kanade.tachiyomi.animesource.model
class SEpisodeImpl : SEpisode {
@@ -1,15 +1,15 @@
package eu.kanade.tachiyomi.source.online
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
@@ -118,7 +118,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchAnime(page: Int, query: String, filters: FilterList): Observable<AnimesPage> {
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return client.newCall(searchAnimeRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
@@ -133,7 +133,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: FilterList): Request
protected abstract fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request
/**
* Parses the response from the site and returns a [AnimesPage] object.
@@ -380,7 +380,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList()
override fun getFilterList() = AnimeFilterList()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
@@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.source.online
package eu.kanade.tachiyomi.animesource.online
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
@@ -7,8 +7,8 @@ package suwayomi.anime.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SAnime
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
@@ -102,7 +102,7 @@ object Anime {
fetchedAnime.description,
fetchedAnime.genre,
AnimeStatus.valueOf(fetchedAnime.status).name,
false,
animeEntry[AnimeTable.inLibrary],
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
true
)
@@ -7,7 +7,7 @@ package suwayomi.anime.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
@@ -7,8 +7,8 @@ package suwayomi.anime.impl
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.source.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
@@ -8,11 +8,11 @@ package suwayomi.anime.impl.extension
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.net.Uri
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AnimeCatalogueSource
import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.AnimeSourceFactory
import mu.KotlinLogging
import okhttp3.Request
import okio.buffer
@@ -13,6 +13,8 @@ import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Request
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.anime.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.anime.model.dataclass.AnimeExtensionDataClass
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
import uy.kohesive.injekt.injectLazy
@@ -21,15 +23,12 @@ object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = 1.3
private const val LIB_VERSION_MAX = 1.3
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json
.map { it.asJsonObject }
.filter { element ->
val versionName = element["version"].string
val libVersion = versionName.substringBeforeLast('.').toDouble()
val libVersion = versionName.substringBeforeLast('.').toInt()
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
}
.map { element ->
@@ -1,5 +1,12 @@
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(
val name: String,
val pkgName: String,
@@ -7,9 +7,9 @@ package suwayomi.anime.impl.util
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.AnimeSource
import eu.kanade.tachiyomi.source.AnimeSourceFactory
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.kodein.di.DI
@@ -40,8 +40,8 @@ object PackageTools {
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.3
const val LIB_VERSION_MIN = 10
const val LIB_VERSION_MAX = 10
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
var trustedSignatures = mutableSetOf<String>() + officialSignature
@@ -7,7 +7,7 @@ package suwayomi.anime.model.table
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnime
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
@@ -1,6 +1,6 @@
package suwayomi.server.database.migration
import eu.kanade.tachiyomi.source.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnime
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
@@ -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
/*
* 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 kotlin.system.exitProcess
@@ -33,6 +33,7 @@ import suwayomi.tachidesk.impl.Source.getSourceList
import suwayomi.tachidesk.impl.backup.BackupFlags
import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup
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.installExtension
import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension
@@ -383,15 +384,56 @@ object TachideskAPI {
// Download queue stats
app.ws("/api/v1/downloads") { ws ->
ws.onConnect { ctx ->
// TODO: send current stat
// TODO: add to downlad subscribers
DownloadManager.addClient(ctx)
DownloadManager.notifyClient(ctx)
}
ws.onMessage {
// TODO: send current stat
ws.onMessage { ctx ->
DownloadManager.handleRequest(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.PageTable
import suwayomi.tachidesk.model.table.toDataClass
import java.time.Instant
object Chapter {
/** 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`
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
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 {
if (it[ChapterTable.chapterIndex] >= chapterList.size ||
@@ -122,9 +123,14 @@ object Chapter {
dbChapter[ChapterTable.isRead],
dbChapter[ChapterTable.isBookmarked],
dbChapter[ChapterTable.lastPageRead],
dbChapter[ChapterTable.lastReadAt],
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)
}.first()
}
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
return if (!chapterEntry[ChapterTable.isDownloaded]) {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
val pageList = source.fetchPageList(
SChapter.create().apply {
url = chapterEntry[ChapterTable.url]
name = chapterEntry[ChapterTable.name]
}
).awaitSingle()
// update page list for this chapter
transaction {
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[chapter] = chapterId
}
} else {
PageTable.update({ (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }) {
it[url] = page.url
it[imageUrl] = page.imageUrl
val chapterId = chapterEntry[ChapterTable.id].value
val chapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
// update page list for this chapter
transaction {
pageList.forEach { page ->
val pageEntry = transaction { PageTable.select { (PageTable.chapter eq chapterId) and (PageTable.index eq page.index) }.firstOrNull() }
if (pageEntry == null) {
PageTable.insert {
it[index] = page.index
it[url] = page.url
it[imageUrl] = page.imageUrl
it[chapter] = chapterId
}
} 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?) {
@@ -198,6 +218,7 @@ object Chapter {
}
lastPageRead?.also {
update[ChapterTable.lastPageRead] = it
update[ChapterTable.lastReadAt] = Instant.now().epochSecond
}
}
}
@@ -102,7 +102,7 @@ object Manga {
fetchedManga.description,
fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name,
false,
mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]),
true
)
@@ -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,28 +1,81 @@
package suwayomi.tachidesk.impl.download
import org.jetbrains.exposed.sql.ResultRow
import java.util.concurrent.LinkedBlockingQueue
/*
* 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 Download(
val chapter: ResultRow,
)
import kotlinx.coroutines.runBlocking
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() {
TODO()
fun step() {
notifier()
synchronized(shouldStop) {
if (shouldStop) throw DownloadShouldStopException()
}
}
fun stop() {
TODO()
override fun run() {
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>,
)
@@ -13,6 +13,8 @@ import com.google.gson.JsonArray
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Request
import suwayomi.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
import suwayomi.tachidesk.model.dataclass.ExtensionDataClass
import uy.kohesive.injekt.injectLazy
@@ -21,9 +23,6 @@ object ExtensionGithubApi {
const val BASE_URL = "https://raw.githubusercontent.com"
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
private const val LIB_VERSION_MIN = 1.2
private const val LIB_VERSION_MAX = 1.2
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
return json
.map { it.asJsonObject }
@@ -1,5 +1,12 @@
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(
val name: String,
val pkgName: String,
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.impl.util.storage
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import okhttp3.Response
import okhttp3.internal.closeQuietly
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
@@ -41,20 +42,22 @@ object CachedImageResponse {
val response = fetcher()
if (response.code == 200) {
val fullPath = "$filePath.tmp"
val saveFile = File(fullPath)
response.body!!.source().saveTo(saveFile)
val tmpSavePath = "$filePath.tmp"
val tmpSaveFile = File(tmpSavePath)
response.body!!.source().saveTo(tmpSaveFile)
// find image type
val imageType = response.headers["content-type"]
?: ImageUtil.findImageType { saveFile.inputStream() }?.mime
?: ImageUtil.findImageType { tmpSaveFile.inputStream() }?.mime
?: "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 {
response.closeQuietly()
throw Exception("request error! ${response.code}")
}
}
@@ -24,12 +24,19 @@ data class ChapterDataClass(
/** last read page, zero means not read/no data */
val lastPageRead: Int,
/** last read page, zero means not read/no data */
val lastReadAt: Long,
/** this chapter's index, starts with 1 */
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 */
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.sql.ResultRow
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.model.dataclass.ChapterDataClass
object ChapterTable : IntIdTable() {
@@ -21,10 +23,15 @@ object ChapterTable : IntIdTable() {
val isRead = bool("read").default(false)
val isBookmarked = bool("bookmark").default(false)
val lastPageRead = integer("last_page_read").default(0)
val lastReadAt = long("last_read_at").default(0)
// index is reserved by a function
val chapterIndex = integer("index")
val isDownloaded = bool("is_downloaded").default(false)
val pageCount = integer("page_count").default(-1)
val manga = reference("manga", MangaTable)
}
@@ -39,5 +46,9 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
chapterEntry[isRead],
chapterEntry[isBookmarked],
chapterEntry[lastPageRead],
chapterEntry[lastReadAt],
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": {
"@types/react": "^17.0.2",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.2",
"@types/react-lazyload": "^3.1.0",
"@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 Player from 'screens/anime/Player';
import AnimeExtensions from 'screens/anime/AnimeExtensions';
import DownloadQueue from 'screens/manga/DownloadQueue';
export default function App() {
const [title, setTitle] = useState<string>('Tachidesk');
@@ -125,6 +126,9 @@ export default function App() {
<Route path="/manga/sources">
<MangaSources />
</Route>
<Route path="/manga/downloads">
<DownloadQueue />
</Route>
<Route path="/manga/:mangaId/chapter/:chapterNum">
<></>
</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 ExploreIcon from '@material-ui/icons/Explore';
import ExtensionIcon from '@material-ui/icons/Extension';
import GetAppIcon from '@material-ui/icons/GetApp';
import ListItemText from '@material-ui/core/ListItemText';
import SettingsIcon from '@material-ui/icons/Settings';
import { Link } from 'react-router-dom';
@@ -87,6 +88,14 @@ export default function TemporaryDrawer({ drawerOpen, setDrawerOpen }: IProps) {
<ListItemText primary="Anime Sources" />
</ListItem>
</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' }}>
<ListItem button key="settings">
<ListItemIcon>
@@ -47,12 +47,13 @@ const useStyles = makeStyles((theme) => ({
interface IProps{
chapter: IChapter
triggerChaptersUpdate: () => void
downloadingString: string
}
export default function ChapterCard(props: IProps) {
const classes = useStyles();
const theme = useTheme();
const { chapter, triggerChaptersUpdate } = props;
const { chapter, triggerChaptersUpdate, downloadingString } = props;
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
@@ -75,6 +76,11 @@ export default function ChapterCard(props: IProps) {
.then(() => triggerChaptersUpdate());
};
const downloadChapter = () => {
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
handleClose();
};
const readChapterColor = theme.palette.type === 'dark' ? '#acacac' : '#b0b0b0';
return (
<>
@@ -101,6 +107,7 @@ export default function ChapterCard(props: IProps) {
{chapter.scanlator}
{chapter.scanlator && ' '}
{dateStr}
{downloadingString}
</Typography>
</div>
</div>
@@ -115,7 +122,8 @@ export default function ChapterCard(props: IProps) {
open={Boolean(anchorEl)}
onClose={handleClose}
>
{/* <MenuItem onClick={handleClose}>Download</MenuItem> */}
{downloadingString.length === 0
&& <MenuItem onClick={downloadChapter}>Download</MenuItem> }
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
{chapter.bookmarked && 'Remove bookmark'}
{!chapter.bookmarked && 'Bookmark'}
+10 -6
View File
@@ -9,11 +9,11 @@ import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardMedia from '@material-ui/core/CardMedia';
import Typography from '@material-ui/core/Typography';
import { Link } from 'react-router-dom';
import { Grid } from '@material-ui/core';
import useLocalStorage from 'util/useLocalStorage';
import SpinnerImage from 'components/SpinnerImage';
const useStyles = makeStyles({
root: {
@@ -43,6 +43,11 @@ const useStyles = makeStyles({
height: '100%',
width: '100%',
},
spinner: {
minHeight: '400px',
padding: '180px calc(50% - 20px)',
},
});
interface IProps {
@@ -63,12 +68,11 @@ const MangaCard = React.forwardRef((props: IProps, ref) => {
<Card className={classes.root} ref={ref}>
<CardActionArea>
<div className={classes.wrapper}>
<CardMedia
className={classes.image}
component="img"
<SpinnerImage
alt={title}
image={serverAddress + thumbnailUrl}
title={title}
src={serverAddress + thumbnailUrl}
spinnerClassName={classes.spinner}
imgClassName={classes.image}
/>
<div className={classes.gradient} />
<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.leftRight}>
<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 className={classes.rightSide}>
<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
* 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 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({
loading: {
@@ -22,87 +50,67 @@ const useStyles = (settings: IReaderSettings) => makeStyles({
backgroundColor: '#525252',
marginBottom: 10,
},
image: {
display: 'block',
marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0,
minWidth: '50vw',
width: '100%',
maxWidth: '100%',
},
image: imageStyle(settings),
});
interface IProps {
src: string
index: number
onImageLoad: () => void
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}
function LazyImage(props: IProps) {
const Page = React.forwardRef((props: IProps, ref: any) => {
const {
src, index, setCurPage, settings,
src, index, onImageLoad, setCurPage, settings,
} = props;
const classes = useStyles(settings)();
const [imageSrc, setImagsrc] = useState<string>('');
const ref = useRef<HTMLImageElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const handleScroll = () => {
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
const handleVerticalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
setCurPage(index);
}
}
};
useEffect(() => {
if (settings.readerType === 'Webtoon' || settings.readerType === 'ContinuesVertical') {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
} return () => {};
}, [handleScroll]);
const handleHorizontalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.left <= window.innerWidth / 2 && rect.right > window.innerWidth / 2) {
setCurPage(index);
}
}
};
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => setImagsrc(src);
}, [src]);
if (imageSrc.length === 0) {
return (
<div className={`${classes.image} ${classes.loadingImage}`}>
<CircularProgress thickness={5} />
</div>
);
}
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;
switch (settings.readerType) {
case 'Webtoon':
case 'ContinuesVertical':
window.addEventListener('scroll', handleVerticalScroll);
return () => window.removeEventListener('scroll', handleVerticalScroll);
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
window.addEventListener('scroll', handleHorizontalScroll);
return () => window.removeEventListener('scroll', handleHorizontalScroll);
default:
return () => {};
}
}, [handleVerticalScroll]);
return (
<div ref={ref} style={{ margin: '0 auto' }}>
<LazyImage
<SpinnerImage
src={src}
index={index}
setCurPage={setCurPage}
settings={settings}
onImageLoad={onImageLoad}
alt={`Page #${index}`}
imgRef={imgRef}
spinnerClassName={`${classes.image} ${classes.loadingImage}`}
imgClassName={classes.image}
/>
</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/. */
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import React, { useEffect, useRef } from 'react';
import Page from '../Page';
const useStyles = makeStyles({
const useStyles = (settings: IReaderSettings) => makeStyles({
reader: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
flexDirection: (settings.readerType === 'ContinuesHorizontalLTR') ? 'row' : 'row-reverse',
justifyContent: (settings.readerType === 'ContinuesHorizontalLTR') ? 'flex-start' : 'flex-end',
margin: '0 auto',
width: '100%',
height: '100vh',
overflowX: 'scroll',
width: 'auto',
height: 'auto',
overflowX: 'visible',
userSelect: 'none',
},
});
interface IProps {
pages: Array<IReaderPage>
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}
export default function HorizontalPager(props: IReaderProps) {
const {
pages, curPage, settings, setCurPage, prevChapter, nextChapter,
} = props;
export default function HorizontalPager(props: IProps) {
const { pages, settings, setCurPage } = props;
const classes = useStyles(settings)();
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 (
<div className={classes.reader}>
<div ref={selfRef} className={classes.reader}>
{
pages.map((page) => (
<Page
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
))
}
@@ -38,7 +38,27 @@ export default function PagedReader(props: IReaderProps) {
}
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) {
@@ -48,10 +68,10 @@ export default function PagedReader(props: IReaderProps) {
nextPage();
break;
case 'ArrowRight':
nextPage();
goRight();
break;
case 'ArrowLeft':
prevPage();
goLeft();
break;
default:
break;
@@ -60,9 +80,9 @@ export default function PagedReader(props: IReaderProps) {
function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
nextPage();
goRight();
} else {
prevPage();
goLeft();
}
}
@@ -74,13 +94,14 @@ export default function PagedReader(props: IReaderProps) {
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
};
}, [selfRef, curPage]);
}, [selfRef, curPage, settings.readerType]);
return (
<div ref={selfRef} className={classes.reader}>
<Page
key={curPage}
index={curPage}
onImageLoad={() => {}}
src={pages[curPage].src}
setCurPage={setCurPage}
settings={settings}
@@ -29,6 +29,10 @@ export default function VerticalReader(props: IReaderProps) {
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);
useEffect(() => {
pagesRef.current = pagesRef.current.slice(0, pages.length);
}, [pages.length]);
function nextPage() {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView();
@@ -104,7 +108,7 @@ export default function VerticalReader(props: IReaderProps) {
if (initialPage > -1) {
pagesRef.current[initialPage].scrollIntoView();
}
}, []);
}, [pagesRef.current.length]);
return (
<div ref={selfRef} className={classes.reader}>
@@ -114,6 +118,7 @@ export default function VerticalReader(props: IReaderProps) {
key={page.index}
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
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)}
>
<MenuItem value="SingleLTR">
Left to right
Single Page (LTR)
</MenuItem>
{/* <MenuItem value="SingleRTL">
Right to left(WIP)
<MenuItem value="SingleRTL">
Single Page (RTL)
</MenuItem> */}
</MenuItem>
{/* <MenuItem value="SingleVertical">
Vertical(WIP)
</MenuItem> */}
<MenuItem value="DoubleLTR">
Double Page (LTR)
</MenuItem>
<MenuItem value="DoubleRTL">
Double Page (RTL)
</MenuItem>
<MenuItem value="Webtoon">
Webtoon
@@ -305,10 +313,14 @@ export default function ReaderNavBar(props: IProps) {
Continues Vertical
</MenuItem>
{/* <MenuItem value="ContinuesHorizontal">
Horizontal(WIP)
<MenuItem value="ContinuesHorizontalLTR">
Horizontal (LTR)
</MenuItem> */}
</MenuItem>
<MenuItem value="ContinuesHorizontalRTL">
Horizontal (RTL)
</MenuItem>
</Select>
</ListItem>
</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() {
const classes = useStyles();
@@ -55,10 +61,48 @@ export default function Manga() {
const [noChaptersFound, setNoChaptersFound] = useState(false);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
const [, setWsClient] = useState<WebSocket>();
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
function triggerChaptersUpdate() {
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(() => {
if (manga === undefined || !manga.freshData) {
client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
@@ -105,6 +149,7 @@ export default function Manga() {
itemContent={(index:number) => (
<ChapterCard
chapter={chapters[index]}
downloadingString={downloadingStringFor(chapters[index])}
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 HorizontalPager from 'components/manga/reader/pager/HorizontalPager';
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 ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar';
import NavbarContext from 'context/NavbarContext';
@@ -32,21 +33,21 @@ const useStyles = (settings: IReaderSettings) => makeStyles({
const getReaderComponent = (readerType: ReaderType) => {
switch (readerType) {
case 'ContinuesVertical':
return VerticalPager;
break;
case 'Webtoon':
return VerticalPager;
break;
case 'SingleVertical':
return WebtoonPager;
break;
case 'SingleRTL':
return WebtoonPager;
break;
case 'SingleLTR':
return WebtoonPager;
return PagedPager;
break;
case 'ContinuesHorizontal':
case 'DoubleVertical':
case 'DoubleRTL':
case 'DoubleLTR':
return DoublePagedPager;
break;
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
return HorizontalPager;
default:
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
*
@@ -5,9 +8,6 @@
* 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,
@@ -16,7 +16,9 @@ import {
ListItemIcon,
IconButton,
} 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 EditIcon from '@material-ui/icons/Edit';
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 Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import NavbarContext from '../../context/NavbarContext';
import client from '../../util/client';
import NavbarContext from 'context/NavbarContext';
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
...draggableStyle,
@@ -47,14 +51,14 @@ export default function Categories() {
const { setTitle, setAction } = useContext(NavbarContext);
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
const [categories, setCategories] = useState([]);
const [categoryToEdit, setCategoryToEdit] = useState(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogName, setDialogName] = useState('');
const [dialogDefault, setDialogDefault] = useState(false);
const [categories, setCategories] = useState<ICategory[]>([]);
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [dialogName, setDialogName] = useState<string>('');
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
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
useEffect(() => {
@@ -65,12 +69,12 @@ export default function Categories() {
}
}, [updateTriggerHolder]);
const categoryReorder = (list, from, to) => {
const categoryReorder = (list: ICategory[], from: number, to: number) => {
const category = list[from];
const formData = new FormData();
formData.append('from', from + 1);
formData.append('to', to + 1);
formData.append('from', `${from + 1}`);
formData.append('to', `${to + 1}`);
client.post(`/api/v1/category/${category.id}/reorder`, formData)
.finally(() => triggerUpdate());
@@ -81,7 +85,7 @@ export default function Categories() {
return result;
};
const onDragEnd = (result) => {
const onDragEnd = (result: DropResult) => {
// dropped outside the list?
if (!result.destination) {
return;
@@ -105,7 +109,7 @@ export default function Categories() {
setDialogOpen(true);
};
const handleEditDialogOpen = (index) => {
const handleEditDialogOpen = (index:number) => {
setDialogName(categories[index].name);
setDialogDefault(categories[index].default);
setCategoryToEdit(index);
@@ -121,7 +125,7 @@ export default function Categories() {
const formData = new FormData();
formData.append('name', dialogName);
formData.append('default', dialogDefault);
formData.append('default', dialogDefault.toString());
if (categoryToEdit === -1) {
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];
client.delete(`/api/v1/category/${category.id}`)
.finally(() => triggerUpdate());
@@ -153,8 +157,7 @@ export default function Categories() {
>
{(provided, snapshot) => (
<ListItem
ContainerComponent="li"
ContainerProps={{ ref: provided.innerRef }}
ContainerProps={{ ref: provided.innerRef } as any}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(
+20 -2
View File
@@ -67,6 +67,7 @@ interface IChapter {
index: number
chapterCount: number
pageCount: number
downloaded: boolean
}
interface IEpisode {
@@ -99,7 +100,7 @@ interface IPartialEpisode {
interface ICategory {
id: number
order: number
name: String
name: string
default: boolean
}
@@ -114,7 +115,11 @@ type ReaderType =
'SingleVertical' |
'SingleRTL' |
'SingleLTR' |
'ContinuesHorizontal';
'DoubleVertical' |
'DoubleRTL' |
'DoubleLTR' |
'ContinuesHorizontalLTR'|
'ContinuesHorizontalRTL';
interface IReaderSettings{
staticNav: boolean
@@ -149,3 +154,16 @@ interface IAbout {
github: 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"
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":
version "17.0.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.5.tgz#df44eed5b8d9e0b13bb0cd38e0ea6572a1231227"