This commit is contained in:
Syer10
2025-12-06 12:25:43 -05:00
parent b5664f34ad
commit 472ecb9431
36 changed files with 523 additions and 142 deletions

View File

@@ -242,13 +242,14 @@ class JavaSharedPreferences(
}
notify(it.key)
}
is Action.Remove -> {
preferences.remove(it.key)
/**
* Set<String> are stored like
* key.0 = value1
* key.1 = value2
* key.size = 2
/*
Set<String> are stored like
key.0 = value1
key.1 = value2
key.size = 2
*/
preferences.keys.forEach { key ->
if (key.startsWith(it.key + ".")) {
@@ -258,7 +259,10 @@ class JavaSharedPreferences(
notify(it.key)
}
Action.Clear -> preferences.clear()
Action.Clear -> {
preferences.clear()
}
}
}
}

View File

@@ -72,8 +72,12 @@ internal class RateLimitInterceptor(
val request = chain.request()
when (host) {
null, request.url.host -> {} // need rate limit
else -> return chain.proceed(request)
// need rate limit
null, request.url.host -> {}
else -> {
return chain.proceed(request)
}
}
try {

View File

@@ -110,6 +110,7 @@ class LocalSource(
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
}
is OrderBy.Latest -> {
mangaDirs =
if (filter.state!!.ascending) {
@@ -246,6 +247,7 @@ class LocalSource(
}
}
}
is Format.Rar -> {
JunrarArchive(chapter).use { rar ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
@@ -255,6 +257,7 @@ class LocalSource(
}
}
}
else -> {}
}
}
@@ -337,6 +340,7 @@ class LocalSource(
)
}
}
is Format.Zip -> {
val loader = ZipPageLoader(format.file)
val pages = loader.getPages()
@@ -344,6 +348,7 @@ class LocalSource(
pages
}
is Format.Rar -> {
val loader = RarPageLoader(format.file)
val pages = loader.getPages()
@@ -351,6 +356,7 @@ class LocalSource(
pages
}
is Format.Epub -> {
val loader = EpubPageLoader(format.file)
val pages = loader.getPages()
@@ -390,6 +396,7 @@ class LocalSource(
entry?.let { coverManager.update(manga, it.inputStream()) }
}
is Format.Zip -> {
ZipFile.builder().setFile(format.file).get().use { zip ->
val entry =
@@ -401,6 +408,7 @@ class LocalSource(
entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
JunrarArchive(format.file).use { archive ->
val entry =
@@ -411,6 +419,7 @@ class LocalSource(
entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
EpubFile(format.file).use { epub ->
val entry =

View File

@@ -58,6 +58,7 @@ object ChapterRecognition {
numberMatch.none() -> {
return chapterNumber ?: -1.0
}
numberMatch.count() > 1 -> {
// Remove unwanted tags.
unwanted.replace(cleanChapterName, "").let { name ->

View File

@@ -367,37 +367,127 @@ class KcefWebView {
): KeyEvent? {
val code =
when (char.uppercaseChar()) {
in 'A'..'Z', in '0'..'9' -> char.uppercaseChar().code
'&' -> KeyEvent.VK_AMPERSAND
'*' -> KeyEvent.VK_ASTERISK
'@' -> KeyEvent.VK_AT
'\\' -> KeyEvent.VK_BACK_SLASH
'{' -> KeyEvent.VK_BRACELEFT
'}' -> KeyEvent.VK_BRACERIGHT
'^' -> KeyEvent.VK_CIRCUMFLEX
']' -> KeyEvent.VK_CLOSE_BRACKET
':' -> KeyEvent.VK_COLON
',' -> KeyEvent.VK_COMMA
'$' -> KeyEvent.VK_DOLLAR
'=' -> KeyEvent.VK_EQUALS
'€' -> KeyEvent.VK_EURO_SIGN
'!' -> KeyEvent.VK_EXCLAMATION_MARK
'>' -> KeyEvent.VK_GREATER
'(' -> KeyEvent.VK_LEFT_PARENTHESIS
'<' -> KeyEvent.VK_LESS
'-' -> KeyEvent.VK_MINUS
'#' -> KeyEvent.VK_NUMBER_SIGN
'[' -> KeyEvent.VK_OPEN_BRACKET
'.' -> KeyEvent.VK_PERIOD
'+' -> KeyEvent.VK_PLUS
'\'' -> KeyEvent.VK_QUOTE
'"' -> KeyEvent.VK_QUOTEDBL
')' -> KeyEvent.VK_RIGHT_PARENTHESIS
';' -> KeyEvent.VK_SEMICOLON
'/' -> KeyEvent.VK_SLASH
' ' -> KeyEvent.VK_SPACE
'_' -> KeyEvent.VK_UNDERSCORE
else ->
in 'A'..'Z', in '0'..'9' -> {
char.uppercaseChar().code
}
'&' -> {
KeyEvent.VK_AMPERSAND
}
'*' -> {
KeyEvent.VK_ASTERISK
}
'@' -> {
KeyEvent.VK_AT
}
'\\' -> {
KeyEvent.VK_BACK_SLASH
}
'{' -> {
KeyEvent.VK_BRACELEFT
}
'}' -> {
KeyEvent.VK_BRACERIGHT
}
'^' -> {
KeyEvent.VK_CIRCUMFLEX
}
']' -> {
KeyEvent.VK_CLOSE_BRACKET
}
':' -> {
KeyEvent.VK_COLON
}
',' -> {
KeyEvent.VK_COMMA
}
'$' -> {
KeyEvent.VK_DOLLAR
}
'=' -> {
KeyEvent.VK_EQUALS
}
'€' -> {
KeyEvent.VK_EURO_SIGN
}
'!' -> {
KeyEvent.VK_EXCLAMATION_MARK
}
'>' -> {
KeyEvent.VK_GREATER
}
'(' -> {
KeyEvent.VK_LEFT_PARENTHESIS
}
'<' -> {
KeyEvent.VK_LESS
}
'-' -> {
KeyEvent.VK_MINUS
}
'#' -> {
KeyEvent.VK_NUMBER_SIGN
}
'[' -> {
KeyEvent.VK_OPEN_BRACKET
}
'.' -> {
KeyEvent.VK_PERIOD
}
'+' -> {
KeyEvent.VK_PLUS
}
'\'' -> {
KeyEvent.VK_QUOTE
}
'"' -> {
KeyEvent.VK_QUOTEDBL
}
')' -> {
KeyEvent.VK_RIGHT_PARENTHESIS
}
';' -> {
KeyEvent.VK_SEMICOLON
}
'/' -> {
KeyEvent.VK_SLASH
}
' ' -> {
KeyEvent.VK_SPACE
}
'_' -> {
KeyEvent.VK_UNDERSCORE
}
else -> {
when (strKey) {
"Alt" -> KeyEvent.VK_ALT
"Backspace" -> KeyEvent.VK_BACK_SPACE
@@ -436,6 +526,7 @@ class KcefWebView {
else -> KeyEvent.VK_UNDEFINED
}
}
}
if (id == KeyEvent.KEY_TYPED) {
if (char == KeyEvent.CHAR_UNDEFINED && code != KeyEvent.VK_ENTER) return null
return KeyEvent(

View File

@@ -104,18 +104,23 @@ object WebView : Websocket<String>() {
dr.resize(event.width, event.height)
logger.debug { "Loading URL $url" }
}
is ResizeMessage -> {
dr.resize(event.width, event.height)
}
is JsEventMessage -> {
dr.event(event)
}
is JsPasteMessage -> {
dr.paste(event.data)
}
is JsCopyMessage -> {
dr.copy()
}
is JsPingMessage -> {
notifyAllClients("{\"type\":\"pong\"}")
}

View File

@@ -135,9 +135,11 @@ class SourceMutation {
filters = updateFilterList(source, filters),
)
}
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)

View File

@@ -61,8 +61,14 @@ class TrackQuery {
cursor: Cursor,
): Boolean =
when (this) {
ID -> tracker.id > cursor.value.toInt()
NAME -> tracker.name > cursor.value
ID -> {
tracker.id > cursor.value.toInt()
}
NAME -> {
tracker.name > cursor.value
}
IS_LOGGED_IN -> {
val value = cursor.value.substringAfter('-').toBooleanStrict()
!value || tracker.isLoggedIn
@@ -74,8 +80,14 @@ class TrackQuery {
cursor: Cursor,
): Boolean =
when (this) {
ID -> tracker.id < cursor.value.toInt()
NAME -> tracker.name < cursor.value
ID -> {
tracker.id < cursor.value.toInt()
}
NAME -> {
tracker.name < cursor.value
}
IS_LOGGED_IN -> {
val value = cursor.value.substringAfter('-').toBooleanStrict()
value || !tracker.isLoggedIn
@@ -159,13 +171,15 @@ class TrackQuery {
res =
when (orderType) {
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST ->
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST -> {
when (orderBy) {
TrackerOrderBy.ID -> res.sortedByDescending { it.id }
TrackerOrderBy.NAME -> res.sortedByDescending { it.name }
TrackerOrderBy.IS_LOGGED_IN -> res.sortedByDescending { it.isLoggedIn }
}
SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST ->
}
SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST -> {
when (orderBy) {
TrackerOrderBy.ID -> res.sortedBy { it.id }
TrackerOrderBy.NAME -> res.sortedBy { it.name }
@@ -174,6 +188,7 @@ class TrackQuery {
}
}
}
}
val total = res.size
val firstResult = res.firstOrNull()

View File

@@ -61,6 +61,7 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
is GraphQLRequest -> {
request.copy(variables = request.variables?.modifyFiles(mapItems))
}
is GraphQLBatchRequest -> {
request.copy(
requests =

View File

@@ -63,10 +63,16 @@ class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() {
override fun willGenerateGraphQLType(type: KType): GraphQLType? =
when (type.classifier as? KClass<*>) {
Long::class -> GraphQLLongAsString // encode to string for JS
Duration::class -> GraphQLDurationAsString // encode Duration as ISO-8601 string
// encode to string for JS
Long::class -> GraphQLLongAsString
// encode Duration as ISO-8601 string
Duration::class -> GraphQLDurationAsString
Cursor::class -> GraphQLCursor
UploadedFile::class -> GraphQLUpload
else -> super.willGenerateGraphQLType(type)
}
}

View File

@@ -25,7 +25,9 @@ private class GraphqlDurationAsStringCoercing : Coercing<Duration, String> {
private fun toStringImpl(input: Any): String =
when (input) {
is Duration -> input.toIsoString()
is String -> Duration.parse(input).toIsoString()
else -> throw CoercingSerializeException(
"Expected a Duration or String but was ${CoercingUtil.typeName(input)}",
)

View File

@@ -31,46 +31,59 @@ data class BackupRestoreStatus(
fun ProtoBackupImport.BackupRestoreState.toStatus(): BackupRestoreStatus =
when (this) {
ProtoBackupImport.BackupRestoreState.Idle ->
ProtoBackupImport.BackupRestoreState.Idle -> {
BackupRestoreStatus(
state = BackupRestoreState.IDLE,
totalManga = 0,
mangaProgress = 0,
)
is ProtoBackupImport.BackupRestoreState.Success ->
}
is ProtoBackupImport.BackupRestoreState.Success -> {
BackupRestoreStatus(
state = BackupRestoreState.SUCCESS,
totalManga = 0,
mangaProgress = 0,
)
is ProtoBackupImport.BackupRestoreState.Failure ->
}
is ProtoBackupImport.BackupRestoreState.Failure -> {
BackupRestoreStatus(
state = BackupRestoreState.FAILURE,
totalManga = 0,
mangaProgress = 0,
)
is ProtoBackupImport.BackupRestoreState.RestoringCategories ->
}
is ProtoBackupImport.BackupRestoreState.RestoringCategories -> {
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_CATEGORIES,
totalManga = totalManga,
mangaProgress = current,
)
is ProtoBackupImport.BackupRestoreState.RestoringMeta ->
}
is ProtoBackupImport.BackupRestoreState.RestoringMeta -> {
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_META,
totalManga = totalManga,
mangaProgress = current,
)
is ProtoBackupImport.BackupRestoreState.RestoringSettings ->
}
is ProtoBackupImport.BackupRestoreState.RestoringSettings -> {
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_SETTINGS,
totalManga = totalManga,
mangaProgress = current,
)
is ProtoBackupImport.BackupRestoreState.RestoringManga ->
}
is ProtoBackupImport.BackupRestoreState.RestoringManga -> {
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_MANGA,
totalManga = totalManga,
mangaProgress = current,
)
}
}

View File

@@ -204,12 +204,27 @@ data class GroupFilter(
fun filterOf(filter: SourceFilter<*>): Filter =
when (filter) {
is SourceFilter.Header -> HeaderFilter(filter.name)
is SourceFilter.Separator -> SeparatorFilter(filter.name)
is SourceFilter.Select<*> -> SelectFilter(filter.name, filter.displayValues, filter.state)
is SourceFilter.Text -> TextFilter(filter.name, filter.state)
is SourceFilter.CheckBox -> CheckBoxFilter(filter.name, filter.state)
is SourceFilter.TriState ->
is SourceFilter.Header -> {
HeaderFilter(filter.name)
}
is SourceFilter.Separator -> {
SeparatorFilter(filter.name)
}
is SourceFilter.Select<*> -> {
SelectFilter(filter.name, filter.displayValues, filter.state)
}
is SourceFilter.Text -> {
TextFilter(filter.name, filter.state)
}
is SourceFilter.CheckBox -> {
CheckBoxFilter(filter.name, filter.state)
}
is SourceFilter.TriState -> {
TriStateFilter(
filter.name,
when (filter.state) {
@@ -218,13 +233,22 @@ fun filterOf(filter: SourceFilter<*>): Filter =
else -> TriState.IGNORE
},
)
is SourceFilter.Group<*> ->
}
is SourceFilter.Group<*> -> {
GroupFilter(
filter.name,
filter.state.map { filterOf(it as SourceFilter<*>) },
)
is SourceFilter.Sort -> SortFilter(filter.name, filter.values.asList(), filter.state?.let(SortFilter::SortSelection))
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
is SourceFilter.Sort -> {
SortFilter(filter.name, filter.values.asList(), filter.state?.let(SortFilter::SortSelection))
}
else -> {
throw RuntimeException("sealed class cannot have more subtypes!")
}
}
/*sealed interface FilterChange {
@@ -286,25 +310,31 @@ fun updateFilterList(
is SourceFilter.Header -> {
// NOOP
}
is SourceFilter.Separator -> {
// NOOP
}
is SourceFilter.Select<*> -> {
filter.state = change.selectState
?: throw Exception("Expected select state change at position ${change.position}")
}
is SourceFilter.Text -> {
filter.state = change.textState
?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.CheckBox -> {
filter.state = change.checkBoxState
?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
filter.state = change.triState?.ordinal
?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Group<*> -> {
val groupChange =
change.groupChange
@@ -315,20 +345,24 @@ fun updateFilterList(
groupFilter.state = groupChange.checkBoxState
?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
groupFilter.state = groupChange.triState?.ordinal
?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Text -> {
groupFilter.state = groupChange.textState
?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.Select<*> -> {
groupFilter.state = groupChange.selectState
?: throw Exception("Expected select state change at position ${change.position}")
}
}
}
is SourceFilter.Sort -> {
filter.state = change.sortState?.run {
SourceFilter.Sort.Selection(index, ascending)
@@ -402,7 +436,7 @@ data class MultiSelectListPreference(
fun preferenceOf(preference: SourcePreference): Preference =
when (preference) {
is SourceSwitchPreference ->
is SourceSwitchPreference -> {
SwitchPreference(
preference.key,
preference.title?.toString(),
@@ -412,7 +446,9 @@ fun preferenceOf(preference: SourcePreference): Preference =
preference.currentValue as Boolean,
preference.defaultValue as Boolean,
)
is SourceCheckBoxPreference ->
}
is SourceCheckBoxPreference -> {
CheckBoxPreference(
preference.key,
preference.title?.toString(),
@@ -422,7 +458,9 @@ fun preferenceOf(preference: SourcePreference): Preference =
preference.currentValue as Boolean,
preference.defaultValue as Boolean,
)
is SourceEditTextPreference ->
}
is SourceEditTextPreference -> {
EditTextPreference(
preference.key,
preference.title?.toString(),
@@ -435,7 +473,9 @@ fun preferenceOf(preference: SourcePreference): Preference =
preference.dialogMessage?.toString(),
preference.text,
)
is SourceListPreference ->
}
is SourceListPreference -> {
ListPreference(
preference.key,
preference.title?.toString(),
@@ -447,7 +487,9 @@ fun preferenceOf(preference: SourcePreference): Preference =
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() },
)
is SourceMultiSelectListPreference ->
}
is SourceMultiSelectListPreference -> {
MultiSelectListPreference(
preference.key,
preference.title?.toString(),
@@ -461,5 +503,9 @@ fun preferenceOf(preference: SourcePreference): Preference =
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() },
)
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
else -> {
throw RuntimeException("sealed class cannot have more subtypes!")
}
}

View File

@@ -513,25 +513,33 @@ object Chapter {
// Make sure some filter is defined
val condition =
when {
mangaId != null ->
mangaId != null -> {
// mangaId is not null, scope query under manga
when {
input.chapterIds != null ->
input.chapterIds != null -> {
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
}
input.chapterIndexes != null ->
input.chapterIndexes != null -> {
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
}
else -> null
else -> {
null
}
}
}
else -> {
// mangaId is null, only chapterIndexes is valid for this case
when {
input.chapterIds != null ->
input.chapterIds != null -> {
Op.build { (ChapterTable.id inList input.chapterIds) }
}
else -> null
else -> {
null
}
}
}
} ?: return

View File

@@ -372,10 +372,11 @@ object Manga {
val sourceId = mangaEntry[MangaTable.sourceReference]
return when (val source = getCatalogueSourceOrStub(sourceId)) {
is HttpSource ->
is HttpSource -> {
getImageResponse(cacheSaveDir, fileName) {
fetchHttpSourceMangaThumbnail(source, mangaEntry)
}
}
is LocalSource -> {
val imageFile =
@@ -393,7 +394,7 @@ object Manga {
imageFile.inputStream() to contentType
}
is StubSource ->
is StubSource -> {
getImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl =
mangaEntry[MangaTable.thumbnail_url]
@@ -403,8 +404,11 @@ object Manga {
GET(thumbnailUrl, cache = CacheControl.FORCE_NETWORK),
).await()
}
}
else -> throw IllegalArgumentException("Unknown source")
else -> {
throw IllegalArgumentException("Unknown source")
}
}
}

View File

@@ -86,7 +86,10 @@ object Search {
},
)
}
else -> it
else -> {
it
}
},
)
}
@@ -122,13 +125,27 @@ object Search {
is Filter.Header -> {
// NOOP
}
is Filter.Separator -> {
// NOOP
}
is Filter.Select<*> -> filter.state = change.state.toInt()
is Filter.Text -> filter.state = change.state
is Filter.CheckBox -> filter.state = change.state.toBooleanStrict()
is Filter.TriState -> filter.state = change.state.toInt()
is Filter.Select<*> -> {
filter.state = change.state.toInt()
}
is Filter.Text -> {
filter.state = change.state
}
is Filter.CheckBox -> {
filter.state = change.state.toBooleanStrict()
}
is Filter.TriState -> {
filter.state = change.state.toInt()
}
is Filter.Group<*> -> {
val groupChange = jsonMapper.fromJsonString<FilterChange>(change.state)
@@ -139,6 +156,7 @@ object Search {
is Filter.Select<*> -> groupFilter.state = groupChange.state.toInt()
}
}
is Filter.Sort -> {
filter.state = jsonMapper.fromJsonString(change.state, Filter.Sort.Selection::class.java)
}

View File

@@ -120,8 +120,11 @@ object DownloadManager {
fun handleRequest(ctx: WsMessageContext) {
when (ctx.message()) {
"STATUS" -> notifyClient(ctx)
else ->
"STATUS" -> {
notifyClient(ctx)
}
else -> {
ctx.send(
"""
|Invalid command.
@@ -133,6 +136,7 @@ object DownloadManager {
)
}
}
}
private val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

View File

@@ -49,6 +49,7 @@ sealed class FileType {
is RegularFile -> {
this.file.name
}
is ZipFile -> {
this.entry.name
}
@@ -59,6 +60,7 @@ sealed class FileType {
is RegularFile -> {
this.file.extension
}
is ZipFile -> {
this.entry.name.substringAfterLast(".")
}

View File

@@ -149,6 +149,7 @@ object ExtensionsList {
this[ExtensionTable.hasUpdate] = true
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
}
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
// somehow the user installed an invalid version
this[ExtensionTable.isObsolete] = true

View File

@@ -150,6 +150,7 @@ object KoreaderSyncService {
null
}
}
KoreaderSyncChecksumMethod.FILENAME -> {
logger.debug { "[KOSYNC HASH] No hash for chapterId=$chapterId. Generating from filename." }
(ChapterTable innerJoin MangaTable)

View File

@@ -69,56 +69,82 @@ class Anilist(
when (trackPreferences.getScoreType(this)) {
// 10 point
POINT_10 -> IntRange(0, 10).map(Int::toString)
// 100 point
POINT_100 -> IntRange(0, 100).map(Int::toString)
// 5 stars
POINT_5 -> IntRange(0, 5).map { "$it" }
// Smiley
POINT_3 -> listOf("-", "😦", "😐", "😊")
// 10 point decimal
POINT_10_DECIMAL -> IntRange(0, 100).map { (it / 10f).toString() }
else -> throw Exception("Unknown score type")
}
override fun indexToScore(index: Int): Double =
when (trackPreferences.getScoreType(this)) {
// 10 point
POINT_10 -> index * 10.0
POINT_10 -> {
index * 10.0
}
// 100 point
POINT_100 -> index.toDouble()
POINT_100 -> {
index.toDouble()
}
// 5 stars
POINT_5 ->
POINT_5 -> {
when (index) {
0 -> 0.0
else -> index * 20.0 - 10.0
}
}
// Smiley
POINT_3 ->
POINT_3 -> {
when (index) {
0 -> 0.0
else -> index * 25.0 + 10.0
}
}
// 10 point decimal
POINT_10_DECIMAL -> index.toDouble()
else -> throw Exception("Unknown score type")
POINT_10_DECIMAL -> {
index.toDouble()
}
else -> {
throw Exception("Unknown score type")
}
}
override fun displayScore(track: Track): String {
val score = track.score
return when (val type = trackPreferences.getScoreType(this)) {
POINT_5 ->
POINT_5 -> {
when (score) {
0.0 -> "0 ★"
else -> "${((score + 10) / 20).toInt()}"
}
POINT_3 ->
}
POINT_3 -> {
when {
score == 0.0 -> "0"
score <= 35 -> "😦"
score <= 60 -> "😐"
else -> "😊"
}
else -> track.toApiScore(type)
}
else -> {
track.toApiScore(type)
}
}
}

View File

@@ -16,11 +16,17 @@ fun Track.toApiStatus() =
fun Track.toApiScore(scoreType: String?): String =
when (scoreType) {
// 10 point
"POINT_10" -> (score.toInt() / 10).toString()
"POINT_10" -> {
(score.toInt() / 10).toString()
}
// 100 point
"POINT_100" -> score.toInt().toString()
"POINT_100" -> {
score.toInt().toString()
}
// 5 stars
"POINT_5" ->
"POINT_5" -> {
when {
score == 0.0 -> "0"
score < 30 -> "1"
@@ -29,15 +35,24 @@ fun Track.toApiScore(scoreType: String?): String =
score < 90 -> "4"
else -> "5"
}
}
// Smiley
"POINT_3" ->
"POINT_3" -> {
when {
score == 0.0 -> "0"
score <= 35 -> ":("
score <= 60 -> ":|"
else -> ":)"
}
// 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw NotImplementedError("Unknown score type")
}
// 10 point decimal
"POINT_10_DECIMAL" -> {
(score / 10).toString()
}
else -> {
throw NotImplementedError("Unknown score type")
}
}

View File

@@ -24,15 +24,22 @@ class MangaUpdates(
(0..10)
.flatMap { decimal ->
when (decimal) {
0 -> listOf("-")
10 -> listOf("10.0")
else ->
0 -> {
listOf("-")
}
10 -> {
listOf("10.0")
}
else -> {
(0..9).map { fraction ->
"$decimal.$fraction"
}
}
}
}
}
private val interceptor by lazy { MangaUpdatesInterceptor(this) }

View File

@@ -26,8 +26,11 @@ object UpdaterSocket : Websocket<UpdateStatus>() {
override fun handleRequest(ctx: WsMessageContext) {
when (ctx.message()) {
"STATUS" -> notifyClient(ctx, updater.statusDeprecated.value)
else ->
"STATUS" -> {
notifyClient(ctx, updater.statusDeprecated.value)
}
else -> {
ctx.send(
"""
|Invalid command.
@@ -39,6 +42,7 @@ object UpdaterSocket : Websocket<UpdateStatus>() {
)
}
}
}
override fun addClient(ctx: WsContext) {
logger.info { ctx.sessionId() }

View File

@@ -48,7 +48,10 @@ private fun getChapterDir(
chapterEntry[ChapterTable.scanlator] != null -> {
"${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
}
else -> chapterEntry[ChapterTable.name]
else -> {
chapterEntry[ChapterTable.name]
}
},
)

View File

@@ -319,6 +319,7 @@ object OpdsEntryBuilder {
}
titlePrefix = statusKey.localized(locale)
}
is ProgressSource.Remote -> {
idSuffix = ":remote"
titlePrefix = MR.strings.opds_chapter_status_synced.localized(locale, progressSource.device)
@@ -378,6 +379,7 @@ object OpdsEntryBuilder {
}
titleRes.localized(locale)
}
progressSource is ProgressSource.Local -> {
val titleRes =
if (progressSource.lastPageRead > 0) {
@@ -387,6 +389,7 @@ object OpdsEntryBuilder {
}
titleRes.localized(locale)
}
progressSource is ProgressSource.Remote -> {
val titleRes =
if (progressSource.lastPageRead > 0) {
@@ -396,7 +399,11 @@ object OpdsEntryBuilder {
}
titleRes.localized(locale, progressSource.device)
}
else -> "" // Should not happen
else -> {
// Should not happen
""
}
}
links.add(

View File

@@ -155,30 +155,40 @@ object OpdsFeedBuilder {
val feedTitle =
when (criteria.primaryFilter) {
PrimaryFilterType.SOURCE ->
PrimaryFilterType.SOURCE -> {
MR.strings.opds_feeds_library_source_specific_title.localized(
locale,
result.feedTitleComponent ?: criteria.sourceId.toString(),
)
PrimaryFilterType.CATEGORY ->
}
PrimaryFilterType.CATEGORY -> {
MR.strings.opds_feeds_category_specific_title.localized(
locale,
result.feedTitleComponent ?: criteria.categoryId.toString(),
)
PrimaryFilterType.GENRE ->
}
PrimaryFilterType.GENRE -> {
MR.strings.opds_feeds_genre_specific_title.localized(
locale,
result.feedTitleComponent ?: "Unknown",
)
}
PrimaryFilterType.STATUS -> {
val statusName = NavigationRepository.getStatuses(locale).find { it.id == criteria.statusId }?.title
MR.strings.opds_feeds_status_specific_title.localized(locale, statusName ?: criteria.statusId.toString())
}
PrimaryFilterType.LANGUAGE -> {
val langName = Locale.forLanguageTag(criteria.langCode ?: "").getDisplayName(locale)
MR.strings.opds_feeds_language_specific_title.localized(locale, langName)
}
else -> MR.strings.opds_feeds_all_series_in_library_title.localized(locale)
else -> {
MR.strings.opds_feeds_all_series_in_library_title.localized(locale)
}
}
val feedUrl =

View File

@@ -108,7 +108,7 @@ object MangaRepository {
// Efficiently get the name of the primary filter item
val specificFilterName =
when (criteria.primaryFilter) {
PrimaryFilterType.SOURCE ->
PrimaryFilterType.SOURCE -> {
criteria.sourceId?.let {
SourceTable
.select(SourceTable.name)
@@ -116,7 +116,9 @@ object MangaRepository {
.firstOrNull()
?.get(SourceTable.name)
}
PrimaryFilterType.CATEGORY ->
}
PrimaryFilterType.CATEGORY -> {
criteria.categoryId?.let {
CategoryTable
.select(CategoryTable.name)
@@ -124,10 +126,25 @@ object MangaRepository {
.firstOrNull()
?.get(CategoryTable.name)
}
PrimaryFilterType.GENRE -> criteria.genre
PrimaryFilterType.STATUS -> criteria.statusId.toString() // Controller will map this to a localized string
PrimaryFilterType.LANGUAGE -> criteria.langCode // Controller will map this to a display name
else -> null
}
PrimaryFilterType.GENRE -> {
criteria.genre
}
// Controller will map this to a localized string
PrimaryFilterType.STATUS -> {
criteria.statusId.toString()
}
// Controller will map this to a display name
PrimaryFilterType.LANGUAGE -> {
criteria.langCode
}
else -> {
null
}
}
applyMangaLibrarySortAndFilter(query, sort, filter)

View File

@@ -44,10 +44,18 @@ object OpdsStringUtil {
if (serverConfig.opdsUseBinaryFileSizes.value) {
// Binary notation (base 1024)
when {
size >= 1_125_899_906_842_624 -> "%.2f TiB".format(size / 1_125_899_906_842_624.0) // 1024^4
size >= 1_073_741_824 -> "%.2f GiB".format(size / 1_073_741_824.0) // 1024^3
size >= 1_048_576 -> "%.2f MiB".format(size / 1_048_576.0) // 1024^2
size >= 1024 -> "%.2f KiB".format(size / 1024.0) // 1024
// 1024^4
size >= 1_125_899_906_842_624 -> "%.2f TiB".format(size / 1_125_899_906_842_624.0)
// 1024^3
size >= 1_073_741_824 -> "%.2f GiB".format(size / 1_073_741_824.0)
// 1024^2
size >= 1_048_576 -> "%.2f MiB".format(size / 1_048_576.0)
// 1024
size >= 1024 -> "%.2f KiB".format(size / 1024.0)
else -> "$size bytes"
}
} else {

View File

@@ -52,6 +52,7 @@ object DBManager {
addDataSourceProperty("cachePrepStmts", "true")
addDataSourceProperty("useServerPrepStmts", "true")
}
DatabaseType.H2 -> {
jdbcUrl = "jdbc:h2:${applicationDirs.dataRoot}/database"
driverClassName = "org.h2.Driver"
@@ -103,7 +104,7 @@ object DBManager {
.connect(hikariDataSource!!, databaseConfig = dbConfig)
} else {
when (serverConfig.databaseType.value) {
DatabaseType.POSTGRESQL ->
DatabaseType.POSTGRESQL -> {
Database.connect(
"jdbc:${serverConfig.databaseUrl.value}",
"org.postgresql.Driver",
@@ -111,13 +112,16 @@ object DBManager {
password = serverConfig.databasePassword.value,
databaseConfig = dbConfig,
)
DatabaseType.H2 ->
}
DatabaseType.H2 -> {
Database.connect(
"jdbc:h2:${Injekt.get<ApplicationDirs>().dataRoot}/database",
"org.h2.Driver",
databaseConfig = dbConfig,
)
}
}
}.also { db = it }
}

View File

@@ -6,7 +6,9 @@ import suwayomi.tachidesk.server.serverConfig
val UNLIMITED_TEXT
get() =
when (serverConfig.databaseType.value) {
DatabaseType.H2 -> "VARCHAR" // the default length is `Integer.MAX_VALUE`
// the default length is `Integer.MAX_VALUE`
DatabaseType.H2 -> "VARCHAR"
DatabaseType.POSTGRESQL -> "TEXT"
}

View File

@@ -25,8 +25,14 @@ fun UserType.requireUser(): Int =
fun UserType.requireUserWithBasicFallback(ctx: Context): Int =
when (this) {
is UserType.Admin -> id
UserType.Visitor if ctx.getAttribute(Attribute.TachideskBasic) -> 1
is UserType.Admin -> {
id
}
UserType.Visitor if ctx.getAttribute(Attribute.TachideskBasic) -> {
1
}
UserType.Visitor -> {
ctx.header("WWW-Authenticate", "Basic")
throw UnauthorizedException()
@@ -53,8 +59,14 @@ fun getUserFromContext(ctx: Context): UserType {
return when (serverConfig.authMode.value) {
// NOTE: Basic Auth is expected to have been validated by JavalinSetup
AuthMode.NONE, AuthMode.BASIC_AUTH -> UserType.Admin(1)
AuthMode.SIMPLE_LOGIN -> if (cookieValid()) UserType.Admin(1) else UserType.Visitor
AuthMode.NONE, AuthMode.BASIC_AUTH -> {
UserType.Admin(1)
}
AuthMode.SIMPLE_LOGIN -> {
if (cookieValid()) UserType.Admin(1) else UserType.Visitor
}
AuthMode.UI_LOGIN -> {
val authentication = ctx.header(Header.AUTHORIZATION) ?: ctx.cookie("suwayomi-server-token")
val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token")
@@ -72,8 +84,14 @@ fun getUserFromWsContext(ctx: WsConnectContext): UserType {
return when (serverConfig.authMode.value) {
// NOTE: Basic Auth is expected to have been validated by JavalinSetup
AuthMode.NONE, AuthMode.BASIC_AUTH -> UserType.Admin(1)
AuthMode.SIMPLE_LOGIN -> if (cookieValid()) UserType.Admin(1) else UserType.Visitor
AuthMode.NONE, AuthMode.BASIC_AUTH -> {
UserType.Admin(1)
}
AuthMode.SIMPLE_LOGIN -> {
if (cookieValid()) UserType.Admin(1) else UserType.Visitor
}
AuthMode.UI_LOGIN -> {
val authentication =
ctx.header(Header.AUTHORIZATION) ?: ctx.header("Sec-WebSocket-Protocol") ?: ctx.cookie("suwayomi-server-token")

View File

@@ -72,6 +72,7 @@ object AppMutex {
AppMutexState.Clear -> {
logger.info { "Mutex status is clear, Resuming startup." }
}
AppMutexState.TachideskInstanceRunning -> {
logger.info { "Another instance of Suwayomi-Server is running on $appIP:${serverConfig.port.value}" }
@@ -82,6 +83,7 @@ object AppMutex {
shutdownApp(MutexCheckFailedTachideskRunning)
}
AppMutexState.OtherApplicationRunning -> {
logger.error { "A non Suwayomi-Server application is running on $appIP:${serverConfig.port.value}, aborting startup." }
shutdownApp(MutexCheckFailedAnotherAppRunning)

View File

@@ -42,12 +42,30 @@ fun <T> getParam(
}
val typedItem: Any? =
when (val clazz = param.clazz as Class<T>) {
String::class.java, java.lang.String::class.java -> getSimpleParamItem(ctx, param) ?: param.defaultValue
Int::class.java, java.lang.Integer::class.java -> getSimpleParamItem(ctx, param)?.toIntOrNull() ?: param.defaultValue
Long::class.java, java.lang.Long::class.java -> getSimpleParamItem(ctx, param)?.toLongOrNull() ?: param.defaultValue
Boolean::class.java, java.lang.Boolean::class.java -> getSimpleParamItem(ctx, param)?.toBoolean() ?: param.defaultValue
Float::class.java, java.lang.Float::class.java -> getSimpleParamItem(ctx, param)?.toFloatOrNull() ?: param.defaultValue
Double::class.java, java.lang.Double::class.java -> getSimpleParamItem(ctx, param)?.toDoubleOrNull() ?: param.defaultValue
String::class.java, java.lang.String::class.java -> {
getSimpleParamItem(ctx, param) ?: param.defaultValue
}
Int::class.java, java.lang.Integer::class.java -> {
getSimpleParamItem(ctx, param)?.toIntOrNull() ?: param.defaultValue
}
Long::class.java, java.lang.Long::class.java -> {
getSimpleParamItem(ctx, param)?.toLongOrNull() ?: param.defaultValue
}
Boolean::class.java, java.lang.Boolean::class.java -> {
getSimpleParamItem(ctx, param)?.toBoolean() ?: param.defaultValue
}
Float::class.java, java.lang.Float::class.java -> {
getSimpleParamItem(ctx, param)?.toFloatOrNull() ?: param.defaultValue
}
Double::class.java, java.lang.Double::class.java -> {
getSimpleParamItem(ctx, param)?.toDoubleOrNull() ?: param.defaultValue
}
else -> {
when (param) {
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)

View File

@@ -12,6 +12,7 @@ import android.content.Context
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.lang.launchIO
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.reactivecircus.cache4k.Cache
@@ -193,7 +194,7 @@ object WebInterfaceManager {
}
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
GlobalScope.launchIO {
setupWebUI()
isSetupComplete = true
}
@@ -400,7 +401,7 @@ object WebInterfaceManager {
try {
downloadVersion(flavor, getVersion())
true
} catch (e: Exception) {
} catch (_: Exception) {
false
} ||
isLocalWebUIValid
@@ -427,7 +428,7 @@ object WebInterfaceManager {
try {
setupBundledWebUI()
} catch (e: Exception) {
} catch (_: Exception) {
throw Exception("Unable to setup a webUI")
}
}
@@ -498,7 +499,7 @@ object WebInterfaceManager {
private fun getLocalVersion(path: String = applicationDirs.webUIRoot): String =
try {
File("$path/revision").readText().trim()
} catch (e: Exception) {
} catch (_: Exception) {
"r-1"
}
@@ -585,7 +586,7 @@ object WebInterfaceManager {
.string()
.trim()
})
} catch (e: Exception) {
} catch (_: Exception) {
""
}
@@ -712,7 +713,7 @@ object WebInterfaceManager {
version: String,
) {
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
GlobalScope.launchIO {
downloadVersion(flavor, version)
serveWebUI()
}

View File

@@ -61,9 +61,11 @@ class TestExtensionCompatibility {
it.obsolete -> {
uninstallExtension(it.pkgName)
}
it.hasUpdate -> {
updateExtension(it.pkgName)
}
else -> {
uninstallExtension(it.pkgName)
installExtension(it.pkgName)