From 36f307e3bbf5a52277fba3033a08201cd37f779a Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 6 Jan 2024 18:14:45 -0500 Subject: [PATCH 1/3] Normalize some locale names --- .../presentation/browse/GlobalSearchScreen.kt | 2 +- .../screen/appearance/AppLanguageScreen.kt | 5 ++--- .../kanade/tachiyomi/util/system/LocaleHelper.kt | 16 +++++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index b06796848..7f79dd3ed 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -77,7 +77,7 @@ internal fun GlobalSearchContent( title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name, - subtitle = LocaleHelper.getDisplayName(source.lang), + subtitle = LocaleHelper.getLocalizedDisplayName(source.lang), onClick = { onClickSource(source) }, ) { when (result) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt index 8fc49cf40..bdba9e15c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/appearance/AppLanguageScreen.kt @@ -34,7 +34,6 @@ import tachiyomi.core.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.i18n.stringResource -import java.util.Locale class AppLanguageScreen : Screen() { @@ -104,9 +103,9 @@ class AppLanguageScreen : Screen() { for (i in 0.. context.stringResource(MR.strings.pinned_sources) "other" -> context.stringResource(MR.strings.other_source) "all" -> context.stringResource(MR.strings.multi_lang) - else -> getDisplayName(lang) + else -> getLocalizedDisplayName(lang) } } + fun getDisplayName(lang: String): String { + val normalizedLang = when (lang) { + "zh-CN" -> "zh-Hans" + "zh-TW" -> "zh-Hant" + else -> lang + } + + return Locale.forLanguageTag(normalizedLang).displayName + } + /** * Returns display name of a string language code. * * @param lang empty for system language */ - fun getDisplayName(lang: String?): String { + fun getLocalizedDisplayName(lang: String?): String { if (lang == null) { return "" } From 3ea026e3116a77fd58bf656e1ecdb5e1ab6de28a Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 6 Jan 2024 18:15:17 -0500 Subject: [PATCH 2/3] Avoid hard crash if cached image file was already deleted Closes #9720 --- .../ui/reader/viewer/pager/PagerPageHolder.kt | 67 +++++++++++-------- .../viewer/webtoon/WebtoonPageHolder.kt | 51 ++++++++------ 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 21424cbcf..2c81f1387 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -17,10 +17,12 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope +import logcat.LogPriority import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.ImageUtil +import tachiyomi.core.util.system.logcat import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.InputStream @@ -136,40 +138,47 @@ class PagerPageHolder( val streamFn = page.stream ?: return - val (bais, isAnimated, background) = withIOContext { - streamFn().buffered(16).use { stream -> - process(item, stream).use { itemStream -> - val bais = ByteArrayInputStream(itemStream.readBytes()) - val isAnimated = ImageUtil.isAnimatedAndSupported(bais) - bais.reset() - val background = if (!isAnimated && viewer.config.automaticBackground) { - ImageUtil.chooseBackground(context, bais) - } else { - null + try { + val (bais, isAnimated, background) = withIOContext { + streamFn().buffered(16).use { stream -> + process(item, stream).use { itemStream -> + val bais = ByteArrayInputStream(itemStream.readBytes()) + val isAnimated = ImageUtil.isAnimatedAndSupported(bais) + bais.reset() + val background = if (!isAnimated && viewer.config.automaticBackground) { + ImageUtil.chooseBackground(context, bais) + } else { + null + } + bais.reset() + Triple(bais, isAnimated, background) } - bais.reset() - Triple(bais, isAnimated, background) } } - } - withUIContext { - bais.use { - setImage( - it, - isAnimated, - Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = viewer.config.imageScaleType, - cropBorders = viewer.config.imageCropBorders, - zoomStartPosition = viewer.config.imageZoomType, - landscapeZoom = viewer.config.landscapeZoom, - ), - ) - if (!isAnimated) { - pageBackground = background + withUIContext { + bais.use { + setImage( + it, + isAnimated, + Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = viewer.config.imageScaleType, + cropBorders = viewer.config.imageCropBorders, + zoomStartPosition = viewer.config.imageZoomType, + landscapeZoom = viewer.config.landscapeZoom, + ), + ) + if (!isAnimated) { + pageBackground = background + } } + removeErrorLayout() + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + withUIContext { + setError() } - removeErrorLayout() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index b0f2fae72..b491a7363 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -23,10 +23,12 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.suspendCancellableCoroutine +import logcat.LogPriority import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.ImageUtil +import tachiyomi.core.util.system.logcat import java.io.BufferedInputStream import java.io.InputStream @@ -184,28 +186,35 @@ class WebtoonPageHolder( val streamFn = page?.stream ?: return - val (openStream, isAnimated) = withIOContext { - val stream = streamFn().buffered(16) - val openStream = process(stream) + try { + val (openStream, isAnimated) = withIOContext { + val stream = streamFn().buffered(16) + val openStream = process(stream) - val isAnimated = ImageUtil.isAnimatedAndSupported(stream) - Pair(openStream, isAnimated) - } - withUIContext { - frame.setImage( - openStream, - isAnimated, - ReaderPageImageView.Config( - zoomDuration = viewer.config.doubleTapAnimDuration, - minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, - cropBorders = viewer.config.imageCropBorders, - ), - ) - removeErrorLayout() - } - // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled - suspendCancellableCoroutine { continuation -> - continuation.invokeOnCancellation { openStream.close() } + val isAnimated = ImageUtil.isAnimatedAndSupported(stream) + Pair(openStream, isAnimated) + } + withUIContext { + frame.setImage( + openStream, + isAnimated, + ReaderPageImageView.Config( + zoomDuration = viewer.config.doubleTapAnimDuration, + minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH, + cropBorders = viewer.config.imageCropBorders, + ), + ) + removeErrorLayout() + } + // Suspend the coroutine to close the input stream only when the WebtoonPageHolder is recycled + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { openStream.close() } + } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) + withUIContext { + setError() + } } } From 9c91ddd4e3b27d42afac91efeae9e528798f114c Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 6 Jan 2024 18:26:39 -0500 Subject: [PATCH 3/3] Add link to storage FAQ in settings screen --- .../more/onboarding/StorageStep.kt | 2 +- .../more/settings/screen/SettingsDataScreen.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt index fa1e73fe4..4b6409746 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/StorageStep.kt @@ -77,7 +77,7 @@ internal class StorageStep : OnboardingStep { Text(stringResource(MR.strings.onboarding_storage_help_info, stringResource(MR.strings.app_name))) Button( modifier = Modifier.fillMaxWidth(), - onClick = { handler.openUri("https://tachiyomi.org/docs/faq/storage") }, + onClick = { handler.openUri(SettingsDataScreen.HELP_URL) }, ) { Text(stringResource(MR.strings.onboarding_storage_help_action)) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 9f0d5fc60..43d24510c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -7,8 +7,13 @@ import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +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.outlined.HelpOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -22,6 +27,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.core.net.toUri import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -58,11 +64,23 @@ import uy.kohesive.injekt.api.get object SettingsDataScreen : SearchableSettings { val restorePreferenceKeyString = MR.strings.label_backup + const val HELP_URL = "https://tachiyomi.org/docs/faq/storage" @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.label_data_storage + @Composable + override fun RowScope.AppBarAction() { + val uriHandler = LocalUriHandler.current + IconButton(onClick = { uriHandler.openUri(HELP_URL) }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.HelpOutline, + contentDescription = stringResource(MR.strings.tracking_guide), + ) + } + } + @Composable override fun getPreferences(): List { val backupPreferences = Injekt.get()