feat(discord-UI): Re-UI logout button and improve thumbnail URL handling (#1253)

* feat(discord): re-UI the logout button

* fix(discord): Add URL utility functions and improve thumbnail URL handling

* refactor

* Update core/common/src/main/kotlin/tachiyomi/core/common/util/system/UrlUtils.kt

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

* fix(UrlUtils): Correct comments for URL scheme checks in utility functions

* removing redundant checks

* Remove redundant comment about URL scheme validation

---------

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
This commit is contained in:
Cuong-Tran 2025-10-27 15:26:17 +07:00 committed by GitHub
parent 0fa21a3de4
commit 98e4c24939
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 149 additions and 6 deletions

View file

@ -4,11 +4,14 @@ package eu.kanade.presentation.more.settings.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -28,6 +31,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.connections.service.ConnectionsPreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.tachiyomi.data.connections.ConnectionsManager
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
@ -35,6 +39,7 @@ import kotlinx.collections.immutable.toImmutableMap
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
@ -224,9 +229,20 @@ object SettingsDiscordScreen : SearchableSettings {
),
),
),
Preference.PreferenceItem.TextPreference(
Preference.PreferenceItem.CustomPreference(
title = stringResource(MR.strings.logout),
onClick = { dialog = LogoutConnectionDialog(connectionsManager.discord) },
content = {
TextPreferenceWidget(
modifier = Modifier
.padding(horizontal = MaterialTheme.padding.large),
title = stringResource(MR.strings.logout),
icon = Icons.AutoMirrored.Filled.Logout,
iconTint = MaterialTheme.colorScheme.error,
onPreferenceClick = {
dialog = LogoutConnectionDialog(connectionsManager.discord)
},
)
},
),
)
}

View file

@ -340,12 +340,20 @@ private fun onViewCreated(
SYMR.strings.description_hint,
manga.ogDescription?.takeIf { it.isNotBlank() }?.replace("\n", " ")?.chop(20) ?: "",
)
// KMK -->
val thumbnailUrlHints = listOfNotNull(
manga.ogThumbnailUrl?.let {
it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else ""
},
"file:///storage/emulated/0/Pictures/Komikku/Cover.jpg",
)
// KMK <--
binding.thumbnailUrl.hint =
context.stringResource(
SYMR.strings.thumbnail_url_hint,
manga.ogThumbnailUrl?.let {
it.chop(40) + if (it.length > 46) "." + it.substringAfterLast(".").chop(6) else ""
} ?: "",
// KMK -->
thumbnailUrlHints.joinToString("\nor\n"),
// KMK <--
)
}
binding.mangaGenresTags.clearFocus()

View file

@ -137,6 +137,7 @@ import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNonCancellable
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.UrlUtils
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.manga.model.asMangaCover
@ -1570,7 +1571,7 @@ class ReaderActivity : BaseActivity() {
incognitoMode = viewModel.incognitoMode,
mangaId = manga.id,
mangaTitle = manga.ogTitle,
thumbnailUrl = manga.thumbnailUrl,
thumbnailUrl = manga.thumbnailUrl.takeIf { UrlUtils.isOnlineUrl(it) } ?: manga.ogThumbnailUrl,
chapterNumber = if (connectionsPreferences.useChapterTitles().get()) {
chapter.name
} else {

View file

@ -0,0 +1,118 @@
package tachiyomi.core.common.util.system
/**
* Utility functions for URL handling and validation
*/
object UrlUtils {
/**
* Detects if a URL string represents an online resource (not local storage)
*
* @param url The URL string to check
* @return true if the URL is online, false if it's local storage or invalid
*/
fun isOnlineUrl(url: String?): Boolean {
if (url.isNullOrBlank()) return false
val trimmedUrl = url.trim()
// Check for common online URL schemes
return when {
// Standard HTTP/HTTPS URLs
trimmedUrl.startsWith("http://", ignoreCase = true) -> true
trimmedUrl.startsWith("https://", ignoreCase = true) -> true
// FTP URLs (less common but still online)
trimmedUrl.startsWith("ftp://", ignoreCase = true) -> true
trimmedUrl.startsWith("ftps://", ignoreCase = true) -> true
// If none of the above patterns match, assume it's not a valid local URL
else -> false
}
}
/**
* Detects if a URL string represents a local storage resource
*
* @param url The URL string to check
* @return true if the URL is local storage, false if it's online or invalid
*/
fun isLocalUrl(url: String?): Boolean {
if (url.isNullOrBlank()) return false
val trimmedUrl = url.trim()
// Check for local storage URL schemes
return when {
// Local file schemes
trimmedUrl.startsWith("file://", ignoreCase = true) -> true
trimmedUrl.startsWith("content://", ignoreCase = true) -> true
trimmedUrl.startsWith("android_asset://", ignoreCase = true) -> true
// Intent URLs (Android specific)
trimmedUrl.startsWith("intent://", ignoreCase = true) -> true
// Local paths (absolute or relative)
trimmedUrl.startsWith("/") -> true
trimmedUrl.startsWith("./") -> true
trimmedUrl.startsWith("../") -> true
// If none of the above patterns match, assume it's not a valid embedded URL
else -> false
}
}
/**
* Detects if a URL string represents an embedded resource (such as blob or data URLs)
*
* @param url The URL string to check
* @return true if the URL is an embedded resource, false otherwise
*/
fun isEmbeddedUrl(url: String?): Boolean {
if (url.isNullOrBlank()) return false
val trimmedUrl = url.trim()
// Check for embedded URL schemes
return when {
// Blob URLs (typically local/temporary)
trimmedUrl.startsWith("blob:", ignoreCase = true) -> true
// Data URLs (embedded data)
trimmedUrl.startsWith("data:", ignoreCase = true) -> true
// If none of the above patterns match, assume it's not a valid online URL
else -> false
}
}
/**
* Gets the scheme (protocol) from a URL string
*
* @param url The URL string
* @return The scheme part of the URL, or null if not found
*/
fun getUrlScheme(url: String?): String? {
if (url.isNullOrBlank()) return null
// Find the first colon, which separates the scheme from the rest
val colonIndex = url.indexOf(':')
if (colonIndex <= 0) return null
return url.substring(0, colonIndex).lowercase()
}
/**
* Checks if a URL is a valid web URL (HTTP or HTTPS)
*
* @param url The URL string to check
* @return true if it's a valid web URL, false otherwise
*/
fun isWebUrl(url: String?): Boolean {
if (url.isNullOrBlank()) return false
val trimmedUrl = url.trim()
return trimmedUrl.startsWith("http://", ignoreCase = true) ||
trimmedUrl.startsWith("https://", ignoreCase = true)
}
}