diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt index 468082fc..64b3441c 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt @@ -112,7 +112,8 @@ open class ConfigManager { value: Any, ) { mutex.withLock { - val configValue = ConfigValueFactory.fromAnyRef(value) + val actualValue = if (value is Enum<*>) value.name else value + val configValue = ConfigValueFactory.fromAnyRef(actualValue) updateUserConfigFile(path, configValue) internalConfig = internalConfig.withValue(path, configValue) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt index 503c0f94..b7bb0d6e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -94,6 +94,9 @@ class SettingsMutation { // local source validateFilePath(settings.localSourcePath, "localSourcePath") + + // opds + validateValue(settings.opdsItemsPerPage, "opdsItemsPerPage") { it in 10..5000 } } private fun updateSetting( @@ -177,6 +180,14 @@ class SettingsMutation { updateSetting(settings.flareSolverrSessionName, serverConfig.flareSolverrSessionName) updateSetting(settings.flareSolverrSessionTtl, serverConfig.flareSolverrSessionTtl) updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback) + + // opds + updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage) + updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress) + updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload) + updateSetting(settings.opdsShowOnlyUnreadChapters, serverConfig.opdsShowOnlyUnreadChapters) + updateSetting(settings.opdsShowOnlyDownloadedChapters, serverConfig.opdsShowOnlyDownloadedChapters) + updateSetting(settings.opdsChapterSortOrder, serverConfig.opdsChapterSortOrder) } fun setSettings(input: SetSettingsInput): SetSettingsPayload { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt index 1cde794c..089630c6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.graphql.types import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated +import org.jetbrains.exposed.sql.SortOrder import suwayomi.tachidesk.graphql.server.primitives.Node import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.serverConfig @@ -92,6 +93,14 @@ interface Settings : Node { val flareSolverrSessionName: String? val flareSolverrSessionTtl: Int? val flareSolverrAsResponseFallback: Boolean? + + // opds + val opdsItemsPerPage: Int? + val opdsEnablePageReadProgress: Boolean? + val opdsMarkAsReadOnDownload: Boolean? + val opdsShowOnlyUnreadChapters: Boolean? + val opdsShowOnlyDownloadedChapters: Boolean? + val opdsChapterSortOrder: SortOrder? } data class PartialSettingsType( @@ -159,6 +168,13 @@ data class PartialSettingsType( override val flareSolverrSessionName: String?, override val flareSolverrSessionTtl: Int?, override val flareSolverrAsResponseFallback: Boolean?, + // opds + override val opdsItemsPerPage: Int?, + override val opdsEnablePageReadProgress: Boolean?, + override val opdsMarkAsReadOnDownload: Boolean?, + override val opdsShowOnlyUnreadChapters: Boolean?, + override val opdsShowOnlyDownloadedChapters: Boolean?, + override val opdsChapterSortOrder: SortOrder?, ) : Settings class SettingsType( @@ -226,6 +242,13 @@ class SettingsType( override val flareSolverrSessionName: String, override val flareSolverrSessionTtl: Int, override val flareSolverrAsResponseFallback: Boolean, + // opds + override val opdsItemsPerPage: Int, + override val opdsEnablePageReadProgress: Boolean, + override val opdsMarkAsReadOnDownload: Boolean, + override val opdsShowOnlyUnreadChapters: Boolean, + override val opdsShowOnlyDownloadedChapters: Boolean, + override val opdsChapterSortOrder: SortOrder, ) : Settings { constructor(config: ServerConfig = serverConfig) : this( config.ip.value, @@ -287,5 +310,12 @@ class SettingsType( config.flareSolverrSessionName.value, config.flareSolverrSessionTtl.value, config.flareSolverrAsResponseFallback.value, + // opds + config.opdsItemsPerPage.value, + config.opdsEnablePageReadProgress.value, + config.opdsMarkAsReadOnDownload.value, + config.opdsShowOnlyUnreadChapters.value, + config.opdsShowOnlyDownloadedChapters.value, + config.opdsChapterSortOrder.value, ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt index 7a936123..89b8f3df 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt @@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga import io.javalin.apibuilder.ApiBuilder.delete import io.javalin.apibuilder.ApiBuilder.get +import io.javalin.apibuilder.ApiBuilder.head import io.javalin.apibuilder.ApiBuilder.patch import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.post @@ -83,6 +84,7 @@ object MangaAPI { path("chapter") { post("batch", MangaController.anyChapterBatch) get("{chapterId}/download", MangaController.downloadChapter) + head("{chapterId}/download", MangaController.downloadChapter) } path("category") { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt index 89bab491..5a918891 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.controller * 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.http.HandlerType import io.javalin.http.HttpStatus import kotlinx.serialization.json.Json import suwayomi.tachidesk.manga.impl.CategoryManga @@ -434,20 +435,27 @@ object MangaController { val downloadChapter = handler( pathParam("chapterId"), + queryParam("markAsRead"), documentWith = { withOperation { summary("Download chapter as CBZ") description("Get the CBZ file of the specified chapter") } }, - behaviorOf = { ctx, chapterId -> + behaviorOf = { ctx, chapterId, markAsRead -> + val shouldMarkAsRead = if (ctx.method() == HandlerType.HEAD) false else markAsRead ctx.future { - future { ChapterDownloadHelper.getCbzForDownload(chapterId) } + future { ChapterDownloadHelper.getCbzForDownload(chapterId, shouldMarkAsRead) } .thenApply { (inputStream, fileName, fileSize) -> ctx.header("Content-Type", "application/vnd.comicbook+zip") ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"") ctx.header("Content-Length", fileSize.toString()) - ctx.result(inputStream) + if (ctx.method() == HandlerType.HEAD) { + inputStream.close() + ctx.status(200) + } else { + ctx.result(inputStream) + } } } }, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt index 3d3463c4..2c5ce377 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -57,7 +57,10 @@ object ChapterDownloadHelper { chapterId: Int, ): Pair = provider(mangaId, chapterId).getAsArchiveStream() - fun getCbzForDownload(chapterId: Int): Triple { + fun getCbzForDownload( + chapterId: Int, + markAsRead: Boolean?, + ): Triple { val (chapterData, mangaTitle) = transaction { val row = @@ -74,6 +77,17 @@ object ChapterDownloadHelper { val cbzFile = provider(chapterData.mangaId, chapterData.id).getAsArchiveStream() + if (markAsRead == true) { + Chapter.modifyChapter( + chapterData.mangaId, + chapterData.index, + isRead = true, + isBookmarked = null, + markPrevRead = null, + lastPageRead = null, + ) + } + return Triple(cbzFile.first, fileName, cbzFile.second) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/Opds.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/Opds.kt index a3e400ea..ff3765f2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/Opds.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/impl/Opds.kt @@ -9,6 +9,7 @@ import nl.adaptivity.xmlutil.serialization.XML import org.jetbrains.exposed.sql.JoinType import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.or @@ -30,6 +31,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.opds.model.OpdsXmlModels +import suwayomi.tachidesk.server.serverConfig import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.time.Instant @@ -37,22 +39,25 @@ import java.time.ZoneOffset import java.time.format.DateTimeFormatter object Opds { - private const val ITEMS_PER_PAGE = 20 + private val opdsItemsPerPageBounded: Int + get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000) fun getRootFeed(baseUrl: String): String { + val rootSection = + listOf( + "mangas" to "All Manga", + "sources" to "Sources", + "categories" to "Categories", + "genres" to "Genres", + "status" to "Status", + "languages" to "Languages", + "library-updates" to "Library Update History", + ) val builder = FeedBuilder(baseUrl, 1, "opds", "Suwayomi OPDS Catalog").apply { - totalResults = 6 + totalResults = rootSection.size.toLong() entries += - listOf( - "mangas" to "All Manga", - "sources" to "Sources", - "categories" to "Categories", - "genres" to "Genres", - "status" to "Status", - "languages" to "Languages", - "library-updates" to "Library Update History", - ).map { (id, title) -> + rootSection.map { (id, title) -> OpdsXmlModels.Entry( id = id, title = title, @@ -84,6 +89,7 @@ object Opds { .select(MangaTable.columns) .where { val conditions = mutableListOf>() + conditions += (MangaTable.inLibrary eq true) criteria?.query?.takeIf { it.isNotBlank() }?.let { q -> val lowerQ = q.lowercase() @@ -102,14 +108,14 @@ object Opds { conditions += (MangaTable.title.lowerCase() like "%${title.lowercase()}%") } - if (conditions.isEmpty()) (MangaTable.inLibrary eq true) else conditions.reduce { acc, op -> acc and op } + conditions.reduce { acc, op -> acc and op } }.groupBy(MangaTable.id) .orderBy(MangaTable.title to SortOrder.ASC) val totalCount = query.count() val mangas = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Pair(mangas, totalCount) } @@ -146,8 +152,8 @@ object Opds { val totalCount = query.count() val sources = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { SourceDataClass( id = it[SourceTable.id].value.toString(), @@ -165,7 +171,7 @@ object Opds { return FeedBuilder(baseUrl, pageNum, "sources", "Sources") .apply { - totalResults = totalCount.toLong() + totalResults = totalCount entries += sourceList.map { OpdsXmlModels.Entry( @@ -191,30 +197,33 @@ object Opds { pageNum: Int, ): String { val formattedNow = opdsDateFormatter.format(Instant.now()) - val categoryList = + val (categoryList, total) = transaction { - CategoryTable - .join(CategoryMangaTable, JoinType.INNER, onColumn = CategoryTable.id, otherColumn = CategoryMangaTable.category) - .join(MangaTable, JoinType.INNER, onColumn = CategoryMangaTable.manga, otherColumn = MangaTable.id) - .join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga) - .select(CategoryTable.id, CategoryTable.name) - .groupBy(CategoryTable.id) - .orderBy(CategoryTable.order to SortOrder.ASC) - .map { row -> - Pair(row[CategoryTable.id].value, row[CategoryTable.name]) - } - } + val query = + CategoryTable + .join(CategoryMangaTable, JoinType.INNER, onColumn = CategoryTable.id, otherColumn = CategoryMangaTable.category) + .join(MangaTable, JoinType.INNER, onColumn = CategoryMangaTable.manga, otherColumn = MangaTable.id) + .join(ChapterTable, JoinType.INNER, onColumn = MangaTable.id, otherColumn = ChapterTable.manga) + .select(CategoryTable.id, CategoryTable.name) + .groupBy(CategoryTable.id) + .orderBy(CategoryTable.order to SortOrder.ASC) - val totalCount = categoryList.size - val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE - val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount) - val paginatedCategories = if (fromIndex < totalCount) categoryList.subList(fromIndex, toIndex) else emptyList() + val total = query.count() + + val paginated = + query + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) + .map { row -> Pair(row[CategoryTable.id].value, row[CategoryTable.name]) } + + Pair(paginated, total) + } return FeedBuilder(baseUrl, pageNum, "categories", "Categories") .apply { - totalResults = totalCount.toLong() + totalResults = total entries += - paginatedCategories.map { (id, name) -> + categoryList.map { (id, name) -> OpdsXmlModels.Entry( id = "category/$id", title = name, @@ -252,8 +261,8 @@ object Opds { } val totalCount = genres.size - val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE - val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount) + val fromIndex = (pageNum - 1) * opdsItemsPerPageBounded + val toIndex = minOf(fromIndex + opdsItemsPerPageBounded, totalCount) val paginatedGenres = if (fromIndex < totalCount) genres.subList(fromIndex, toIndex) else emptyList() return serialize( @@ -263,7 +272,7 @@ object Opds { updated = formattedNow, author = OpdsXmlModels.Author("Suwayomi", "https://suwayomi.org/"), totalResults = totalCount.toLong(), - itemsPerPage = ITEMS_PER_PAGE, + itemsPerPage = opdsItemsPerPageBounded, startIndex = fromIndex + 1, links = listOf( @@ -306,8 +315,8 @@ object Opds { val statuses = MangaStatus.entries.sortedBy { it.value } val totalCount = statuses.size - val fromIndex = (pageNum - 1) * ITEMS_PER_PAGE - val toIndex = minOf(fromIndex + ITEMS_PER_PAGE, totalCount) + val fromIndex = (pageNum - 1) * opdsItemsPerPageBounded + val toIndex = minOf(fromIndex + opdsItemsPerPageBounded, totalCount) val paginatedStatuses = if (fromIndex < totalCount) statuses.subList(fromIndex, toIndex) else emptyList() return FeedBuilder(baseUrl, pageNum, "status", "Status") @@ -378,6 +387,8 @@ object Opds { baseUrl: String, pageNum: Int, ): String { + val sortOrder = serverConfig.opdsChapterSortOrder.value + val (manga, chapters, totalCount) = transaction { val mangaEntry = @@ -386,18 +397,30 @@ object Opds { .where { MangaTable.id eq mangaId } .first() val mangaData = MangaTable.toDataClass(mangaEntry, includeMangaMeta = false) + + val chapterConditions = + buildList { + if (serverConfig.opdsShowOnlyUnreadChapters.value) { + add(ChapterTable.isRead eq false) + } + + if (serverConfig.opdsShowOnlyDownloadedChapters.value) { + add(ChapterTable.isDownloaded eq true) + } + add(ChapterTable.manga eq mangaId) + }.reduce { acc, op -> acc and op } + val chaptersQuery = ChapterTable .selectAll() - .where { - (ChapterTable.manga eq mangaId) - }.orderBy(ChapterTable.sourceOrder to SortOrder.DESC) + .where { chapterConditions } + .orderBy(ChapterTable.sourceOrder to sortOrder) val total = chaptersQuery.count() val chaptersData = chaptersQuery - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { ChapterTable.toDataClass(it, includeChapterCount = false, includeChapterMeta = false) } Triple(mangaData, chaptersData, total) } @@ -489,6 +512,7 @@ object Opds { val entryTitle = when { + isMetaDataEntry -> "⬇" chapter.read -> "✅" chapter.lastPageRead > 0 -> "⌛" chapter.pageCount == 0 -> "❌" @@ -506,7 +530,9 @@ object Opds { add( OpdsXmlModels.Link( rel = "http://opds-spec.org/acquisition/open-access", - href = "/api/v1/chapter/${chapter.id}/download", + href = + "/api/v1/chapter/${chapter.id}/download" + + "?markAsRead=${serverConfig.opdsMarkAsReadOnDownload.value}", type = "application/vnd.comicbook+zip", ), ) @@ -515,7 +541,9 @@ object Opds { add( OpdsXmlModels.Link( rel = "http://vaemendis.net/opds-pse/stream", - href = "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}?updateProgress=true", + href = + "/api/v1/manga/${manga.id}/chapter/${chapter.index}/page/{pageNumber}" + + "?updateProgress=${serverConfig.opdsEnablePageReadProgress.value}", type = "image/jpeg", pseCount = chapter.pageCount, pseLastRead = chapter.lastPageRead.takeIf { it != 0 }, @@ -584,8 +612,8 @@ object Opds { val totalCount = query.count() val paginatedResults = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Triple(paginatedResults, totalCount, sourceRow) @@ -624,8 +652,8 @@ object Opds { val totalCount = query.count() val mangas = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Triple(mangas, totalCount, categoryName) } @@ -655,8 +683,8 @@ object Opds { val totalCount = query.count() val mangas = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Pair(mangas, totalCount) } @@ -692,8 +720,8 @@ object Opds { val totalCount = query.count() val mangas = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Pair(mangas, totalCount) } @@ -724,8 +752,8 @@ object Opds { val totalCount = query.count() val mangas = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { MangaTable.toDataClass(it, includeMangaMeta = false) } Pair(mangas, totalCount) } @@ -753,8 +781,8 @@ object Opds { val totalCount = query.count() val chapters = query - .limit(ITEMS_PER_PAGE) - .offset(((pageNum - 1) * ITEMS_PER_PAGE).toLong()) + .limit(opdsItemsPerPageBounded) + .offset(((pageNum - 1) * opdsItemsPerPageBounded).toLong()) .map { ChapterTable.toDataClass( it, @@ -833,7 +861,7 @@ object Opds { type = "application/atom+xml;profile=opds-catalog;kind=navigation", ) }, - (totalResults > pageNum * ITEMS_PER_PAGE).takeIf { it }?.let { + (totalResults > pageNum * opdsItemsPerPageBounded).takeIf { it }?.let { OpdsXmlModels.Link( rel = "next", href = "$baseUrl/$id?pageNumber=${pageNum + 1}", @@ -843,8 +871,8 @@ object Opds { ), entries = entries, totalResults = totalResults, - itemsPerPage = ITEMS_PER_PAGE, - startIndex = (pageNum - 1) * ITEMS_PER_PAGE + 1, + itemsPerPage = opdsItemsPerPageBounded, + startIndex = (pageNum - 1) * opdsItemsPerPageBounded + 1, ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt index 69ee6cd6..c379939a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt @@ -1,5 +1,7 @@ package suwayomi.tachidesk.server +import org.jetbrains.exposed.sql.SortOrder + interface ConfigAdapter { fun toType(configValue: String): T } @@ -19,3 +21,7 @@ object BooleanConfigAdapter : ConfigAdapter { object DoubleConfigAdapter : ConfigAdapter { override fun toType(configValue: String): Double = configValue.toDouble() } + +object SortOrderConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): SortOrder = SortOrder.valueOf(configValue) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index e8ae3b44..ae0e76d3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach +import org.jetbrains.exposed.sql.SortOrder import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import kotlin.reflect.KProperty @@ -155,6 +156,14 @@ class ServerConfig( val flareSolverrSessionTtl: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) val flareSolverrAsResponseFallback: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + // opds settings + val opdsItemsPerPage: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) + val opdsEnablePageReadProgress: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val opdsMarkAsReadOnDownload: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val opdsShowOnlyUnreadChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val opdsShowOnlyDownloadedChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val opdsChapterSortOrder: MutableStateFlow by OverrideConfigValue(SortOrderConfigAdapter) + @OptIn(ExperimentalCoroutinesApi::class) fun subscribeTo( flow: Flow, diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index 2f6cb2ae..dd6b09f1 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -70,3 +70,11 @@ server.flareSolverrTimeout = 60 # time in seconds server.flareSolverrSessionName = "suwayomi" server.flareSolverrSessionTtl = 15 # time in minutes server.flareSolverrAsResponseFallback = false + +# OPDS +server.opdsItemsPerPage = 50 # Range (10 - 5000) +server.opdsEnablePageReadProgress = true +server.opdsMarkAsReadOnDownload = false +server.opdsShowOnlyUnreadChapters = false +server.opdsShowOnlyDownloadedChapters = false +server.opdsChapterSortOrder = "DESC" # "ASC", "DESC" diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf index 764b8c58..34886ce8 100644 --- a/server/src/test/resources/server-reference.conf +++ b/server/src/test/resources/server-reference.conf @@ -67,3 +67,11 @@ server.flareSolverrTimeout = 60 # time in seconds server.flareSolverrSessionName = "suwayomi" server.flareSolverrSessionTtl = 15 # time in minutes server.flareSolverrAsResponseFallback = false + +# OPDS +server.opdsItemsPerPage = 50 # Range (10 - 5000) +server.opdsEnablePageReadProgress = true +server.opdsMarkAsReadOnDownload = false +server.opdsShowOnlyUnreadChapters = false +server.opdsShowOnlyDownloadedChapters = false +server.opdsChapterSortOrder = "DESC" # "ASC", "DESC"