From 98e4c24939bf4a84c32e8102cd4148755df25cb2 Mon Sep 17 00:00:00 2001 From: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:26:17 +0700 Subject: [PATCH] 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> --- .../settings/screen/SettingsDiscordScreen.kt | 20 ++- .../tachiyomi/ui/manga/EditMangaDialog.kt | 14 ++- .../tachiyomi/ui/reader/ReaderActivity.kt | 3 +- .../core/common/util/system/UrlUtils.kt | 118 ++++++++++++++++++ 4 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 core/common/src/main/kotlin/tachiyomi/core/common/util/system/UrlUtils.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDiscordScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDiscordScreen.kt index b5c6ef786..143e5cc67 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDiscordScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDiscordScreen.kt @@ -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) + }, + ) + }, ), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt index 3e7b5f78c..8dca7725f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/EditMangaDialog.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 2d7dc8f89..b5f974dd2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -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 { diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/UrlUtils.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/UrlUtils.kt new file mode 100644 index 000000000..a021ac302 --- /dev/null +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/UrlUtils.kt @@ -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) + } +}