Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfa59a1691 | |||
| 5023e96301 | |||
| 224c24ee9f | |||
| e3b154cf9e | |||
| d249867c4c | |||
| b56045e984 | |||
| 3777cc646e | |||
| aa5a1083d0 | |||
| 2ae5e0742e | |||
| e5e875c54a | |||
| 1a99ec76e4 | |||
| 1b122d1157 | |||
| 77f2f8cc18 | |||
| f0a99980b6 | |||
| b0d43ffe69 | |||
| 16cb0184a4 | |||
| f211a33ea3 | |||
| 440c815189 | |||
| 25829aacfd | |||
| 700a739f95 | |||
| d9620bec05 |
@@ -93,7 +93,7 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// should be bumped with each stable release
|
// should be bumped with each stable release
|
||||||
val tachideskVersion = "v0.4.0"
|
val tachideskVersion = "v0.4.2"
|
||||||
|
|
||||||
// counts commit count on master
|
// counts commit count on master
|
||||||
val tachideskRevision = runCatching {
|
val tachideskRevision = runCatching {
|
||||||
|
|||||||
+5
-5
@@ -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.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
interface AnimeCatalogueSource : AnimeSource {
|
interface AnimeCatalogueSource : AnimeSource {
|
||||||
@@ -30,7 +30,7 @@ interface AnimeCatalogueSource : AnimeSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @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.
|
* 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.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): FilterList
|
fun getFilterList(): AnimeFilterList
|
||||||
}
|
}
|
||||||
+3
-3
@@ -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.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.source.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory for creating sources at runtime.
|
* A factory for creating sources at runtime.
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.source.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.source.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class AnimeSourceManager(private val context: Context) {
|
open class AnimeSourceManager(private val context: Context) {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
import android.support.v7.preference.PreferenceScreen
|
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
-1
@@ -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)
|
data class AnimesPage(val animes: List<SAnime>, val hasNextPage: Boolean)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
class SAnimeImpl : SAnime {
|
class SAnimeImpl : SAnime {
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
class SEpisodeImpl : SEpisode {
|
class SEpisodeImpl : SEpisode {
|
||||||
|
|
||||||
+9
-9
@@ -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.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.network.newCallWithProgress
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.source.model.SEpisode
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -118,7 +118,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @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))
|
return client.newCall(searchAnimeRequest(page, query, filters))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { response ->
|
.map { response ->
|
||||||
@@ -133,7 +133,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @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.
|
* 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.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
override fun getFilterList() = FilterList()
|
override fun getFilterList() = AnimeFilterList()
|
||||||
|
|
||||||
companion object {
|
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"
|
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
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
+4
-4
@@ -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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SAnime
|
|
||||||
import eu.kanade.tachiyomi.source.model.SEpisode
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.nodes.Document
|
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
|
* 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 eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.SAnime
|
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
@@ -102,7 +102,7 @@ object Anime {
|
|||||||
fetchedAnime.description,
|
fetchedAnime.description,
|
||||||
fetchedAnime.genre,
|
fetchedAnime.genre,
|
||||||
AnimeStatus.valueOf(fetchedAnime.status).name,
|
AnimeStatus.valueOf(fetchedAnime.status).name,
|
||||||
false,
|
animeEntry[AnimeTable.inLibrary],
|
||||||
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
getAnimeSource(animeEntry[AnimeTable.sourceReference]),
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ package suwayomi.anime.impl
|
|||||||
* 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 eu.kanade.tachiyomi.source.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import org.jetbrains.exposed.sql.insertAndGetId
|
import org.jetbrains.exposed.sql.insertAndGetId
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ package suwayomi.anime.impl
|
|||||||
* 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 eu.kanade.tachiyomi.source.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.source.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import org.jetbrains.exposed.sql.SortOrder.DESC
|
import org.jetbrains.exposed.sql.SortOrder.DESC
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.deleteWhere
|
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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import android.net.Uri
|
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.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
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 mu.KotlinLogging
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.Request
|
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.anime.model.dataclass.AnimeExtensionDataClass
|
||||||
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
|
import suwayomi.tachidesk.impl.util.network.UnzippingInterceptor
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -21,15 +23,12 @@ object ExtensionGithubApi {
|
|||||||
const val BASE_URL = "https://raw.githubusercontent.com"
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
const val REPO_URL_PREFIX = "$BASE_URL/jmir1/tachiyomi-extensions/repo"
|
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> {
|
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
|
||||||
return json
|
return json
|
||||||
.map { it.asJsonObject }
|
.map { it.asJsonObject }
|
||||||
.filter { element ->
|
.filter { element ->
|
||||||
val versionName = element["version"].string
|
val versionName = element["version"].string
|
||||||
val libVersion = versionName.substringBeforeLast('.').toDouble()
|
val libVersion = versionName.substringBeforeLast('.').toInt()
|
||||||
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||||
}
|
}
|
||||||
.map { element ->
|
.map { element ->
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ package suwayomi.anime.impl.util
|
|||||||
* 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 eu.kanade.tachiyomi.source.AnimeSource
|
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||||
import eu.kanade.tachiyomi.source.AnimeSourceFactory
|
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
import org.jetbrains.exposed.sql.select
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.kodein.di.DI
|
import org.kodein.di.DI
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ object PackageTools {
|
|||||||
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
const val METADATA_SOURCE_CLASS = "tachiyomi.animeextension.class"
|
||||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
const val METADATA_SOURCE_FACTORY = "tachiyomi.animeextension.factory"
|
||||||
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
const val METADATA_NSFW = "tachiyomi.animeextension.nsfw"
|
||||||
const val LIB_VERSION_MIN = 1.3
|
const val LIB_VERSION_MIN = 10
|
||||||
const val LIB_VERSION_MAX = 1.3
|
const val LIB_VERSION_MAX = 10
|
||||||
|
|
||||||
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
|
private const val officialSignature = "50ab1d1e3a20d204d0ad6d334c7691c632e41b98dfa132bf385695fdfa63839c" // jmir1's key
|
||||||
var trustedSignatures = mutableSetOf<String>() + officialSignature
|
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
|
* 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 eu.kanade.tachiyomi.source.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
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 suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
import suwayomi.tachidesk.impl.MangaList.proxyThumbnailUrl
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
package suwayomi.server.database.migration
|
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.dao.id.IntIdTable
|
||||||
import org.jetbrains.exposed.sql.SchemaUtils
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|||||||
+24
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ object Manga {
|
|||||||
fetchedManga.description,
|
fetchedManga.description,
|
||||||
fetchedManga.genre,
|
fetchedManga.genre,
|
||||||
MangaStatus.valueOf(fetchedManga.status).name,
|
MangaStatus.valueOf(fetchedManga.status).name,
|
||||||
false,
|
mangaEntry[MangaTable.inLibrary],
|
||||||
getSource(mangaEntry[MangaTable.sourceReference]),
|
getSource(mangaEntry[MangaTable.sourceReference]),
|
||||||
true
|
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
|
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
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
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>,
|
||||||
|
)
|
||||||
+2
-3
@@ -13,6 +13,8 @@ import com.google.gson.JsonArray
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.Request
|
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.impl.util.network.UnzippingInterceptor
|
||||||
import suwayomi.tachidesk.model.dataclass.ExtensionDataClass
|
import suwayomi.tachidesk.model.dataclass.ExtensionDataClass
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -21,9 +23,6 @@ object ExtensionGithubApi {
|
|||||||
const val BASE_URL = "https://raw.githubusercontent.com"
|
const val BASE_URL = "https://raw.githubusercontent.com"
|
||||||
const val REPO_URL_PREFIX = "$BASE_URL/tachiyomiorg/tachiyomi-extensions/repo"
|
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> {
|
private fun parseResponse(json: JsonArray): List<OnlineExtension> {
|
||||||
return json
|
return json
|
||||||
.map { it.asJsonObject }
|
.map { it.asJsonObject }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.impl.util.storage
|
|||||||
* 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 okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -41,20 +42,22 @@ 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()
|
||||||
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() },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+25
-22
@@ -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(
|
||||||
Vendored
+20
-2
@@ -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[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user