From 356fc5b524f2da6957ad9bb198d15082dda8b14f Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 25 Nov 2023 20:56:15 +0700 Subject: [PATCH 01/22] Fix PTR extra offset calculation (#10172) --- .../presentation/core/components/material/PullRefresh.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index c68dd300f..dd854c3ac 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -196,7 +196,7 @@ private class PullToRefreshStateImpl( val newOffset = (distancePulled + available.y).coerceAtLeast(0f) val dragConsumed = newOffset - distancePulled distancePulled = newOffset - verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress) + verticalOffset = calculateVerticalOffset() + (extraVerticalOffset * progress.coerceIn(0f, 1f)) dragConsumed } return Offset(0f, y) From 53edae1b6b466ef145d97b616a06534d09c2a35c Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 25 Nov 2023 22:27:49 +0700 Subject: [PATCH 02/22] Fix PTR initial refreshing state (#10173) --- .../core/components/material/PullRefresh.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index dd854c3ac..0a1dccc94 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -46,6 +46,7 @@ fun PullRefresh( content: @Composable () -> Unit, ) { val state = rememberPullToRefreshState( + initialRefreshing = refreshing, extraVerticalOffset = indicatorPadding.calculateTopPadding(), enabled = enabled, ) @@ -90,6 +91,7 @@ fun PullRefresh( @Composable private fun rememberPullToRefreshState( + initialRefreshing: Boolean, extraVerticalOffset: Dp, positionalThreshold: Dp = 64.dp, enabled: () -> Boolean = { true }, @@ -108,7 +110,7 @@ private fun rememberPullToRefreshState( ), ) { PullToRefreshStateImpl( - initialRefreshing = false, + initialRefreshing = initialRefreshing, extraVerticalOffset = extraVerticalOffsetPx, positionalThreshold = positionalThresholdPx, enabled = enabled, @@ -133,18 +135,21 @@ private class PullToRefreshStateImpl( ) : PullToRefreshState { override val progress get() = adjustedDistancePulled / positionalThreshold - override var verticalOffset by mutableFloatStateOf(0f) + override var verticalOffset by mutableFloatStateOf(if (initialRefreshing) refreshingVerticalOffset else 0f) override var isRefreshing by mutableStateOf(initialRefreshing) + private val refreshingVerticalOffset: Float + get() = positionalThreshold + extraVerticalOffset + override fun startRefresh() { isRefreshing = true - verticalOffset = positionalThreshold + extraVerticalOffset + verticalOffset = refreshingVerticalOffset } suspend fun startRefreshAnimated() { isRefreshing = true - animateTo(positionalThreshold + extraVerticalOffset) + animateTo(refreshingVerticalOffset) } override fun endRefresh() { From 75314c78e01b4e93b719b2984078d558bf855745 Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 25 Nov 2023 22:54:20 +0700 Subject: [PATCH 03/22] Change default PTR colors (#10174) --- .../presentation/core/components/material/PullRefresh.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt index 0a1dccc94..b24baec43 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/PullRefresh.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.animate import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.pulltorefresh.PullToRefreshContainer import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.runtime.Composable @@ -85,6 +86,8 @@ fun PullRefresh( modifier = Modifier .align(Alignment.TopCenter) .padding(contentPadding), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, ) } } From a5c946969839c8662172ea0709aebb488d3e2bbb Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 25 Nov 2023 12:40:09 -0500 Subject: [PATCH 04/22] Avoid crashing if storage directory can't be read e.g. when first launching and there's no storage permissions yet. --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 +- .../tachiyomi/data/download/DownloadCache.kt | 7 ++-- .../data/download/DownloadProvider.kt | 36 ++++++------------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 67ed7146e..27235ba64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -87,8 +87,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { if (packageName != process) WebView.setDataDirectorySuffix(process) } - Injekt.importModule(AppModule(this)) Injekt.importModule(PreferenceModule(this)) + Injekt.importModule(AppModule(this)) Injekt.importModule(DomainModule()) setupAcra() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 407d2d505..cfcf1fe85 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -297,8 +297,11 @@ class DownloadCache( * Returns the downloads directory from the user's preferences. */ private fun getDirectoryFromPreference(): UniFile { - return UniFile.fromUri(context, storagePreferences.baseStorageDirectory().get().toUri()) - .createDirectory(StoragePreferences.DOWNLOADS_DIR) + return storagePreferences.baseStorageDirectory().get().let { + UniFile.fromUri(context, it.toUri()).also { + it?.createDirectory(StoragePreferences.DOWNLOADS_DIR) + } + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index eb444398a..2f40ab78c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -5,9 +5,6 @@ import androidx.core.net.toUri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.util.storage.DiskUtil -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import logcat.LogPriority import tachiyomi.core.i18n.stringResource import tachiyomi.core.util.system.logcat @@ -29,27 +26,14 @@ class DownloadProvider( private val storagePreferences: StoragePreferences = Injekt.get(), ) { - private val scope = MainScope() - - /** - * The root directory for downloads. - */ - private var downloadsDir = setDownloadsLocation() - - init { - storagePreferences.baseStorageDirectory().changes() - .onEach { downloadsDir = setDownloadsLocation() } - .launchIn(scope) - } - - private fun setDownloadsLocation(): UniFile { - return storagePreferences.baseStorageDirectory().get().let { - val dir = UniFile.fromUri(context, it.toUri()) - .createDirectory(StoragePreferences.DOWNLOADS_DIR) - DiskUtil.createNoMediaFile(dir, context) - dir + private val downloadsDir: UniFile? + get() = storagePreferences.baseStorageDirectory().get().let { + UniFile.fromUri(context, it.toUri()) + ?.createDirectory(StoragePreferences.DOWNLOADS_DIR) + ?.also { dir -> + DiskUtil.createNoMediaFile(dir, context) + } } - } /** * Returns the download directory for a manga. For internal use only. @@ -59,12 +43,12 @@ class DownloadProvider( */ internal fun getMangaDir(mangaTitle: String, source: Source): UniFile { try { - return downloadsDir + return downloadsDir!! .createDirectory(getSourceDirName(source)) .createDirectory(getMangaDirName(mangaTitle)) } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Invalid download directory" } - throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir)) + throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir ?: "")) } } @@ -74,7 +58,7 @@ class DownloadProvider( * @param source the source to query. */ fun findSourceDir(source: Source): UniFile? { - return downloadsDir.findFile(getSourceDirName(source), true) + return downloadsDir?.findFile(getSourceDirName(source), true) } /** From ba10093ddca8d409e43f26a80ea1f485b7767a6c Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 26 Nov 2023 00:32:54 +0600 Subject: [PATCH 05/22] Library update notification changes (#10175) Don't round up notification percentage. Why show 100% when stuff is still updating. Show same notification when hide notification content is enabled. Just exclude manga titles. --- .../data/library/LibraryUpdateNotifier.kt | 25 +++++++++---------- .../commonMain/resources/MR/base/strings.xml | 1 - 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 4a21b9bc0..d9e8dc54a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -33,12 +33,14 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy +import java.math.RoundingMode import java.text.NumberFormat class LibraryUpdateNotifier(private val context: Context) { private val preferences: SecurityPreferences by injectLazy() private val percentFormatter = NumberFormat.getPercentInstance().apply { + roundingMode = RoundingMode.DOWN maximumFractionDigits = 0 } @@ -78,20 +80,17 @@ class LibraryUpdateNotifier(private val context: Context) { * @param total the total progress. */ fun showProgressNotification(manga: List, current: Int, total: Int) { - if (preferences.hideNotificationContent().get()) { - progressNotificationBuilder - .setContentTitle(context.stringResource(MR.strings.notification_check_updates)) - .setContentText("($current/$total)") - } else { + progressNotificationBuilder + .setContentTitle( + context.stringResource( + MR.strings.notification_updating_progress, + percentFormatter.format(current.toFloat() / total), + ), + ) + + if (!preferences.hideNotificationContent().get()) { val updatingText = manga.joinToString("\n") { it.title.chop(40) } - progressNotificationBuilder - .setContentTitle( - context.stringResource( - MR.strings.notification_updating_progress, - percentFormatter.format(current.toFloat() / total), - ), - ) - .setStyle(NotificationCompat.BigTextStyle().bigText(updatingText)) + progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText)) } context.notify( diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 942894566..1d4febdd6 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -800,7 +800,6 @@ Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more. - Checking for new chapters Updating library… (%s) Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more. New chapters found From f1778ac5b4160a6965ae1a5f2e420f8b5e11fafb Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 25 Nov 2023 15:40:10 -0500 Subject: [PATCH 06/22] Bump dependencies --- gradle/androidx.versions.toml | 2 +- gradle/compose.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 8d586064e..7ef758874 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,5 +1,5 @@ [versions] -agp_version = "8.1.3" +agp_version = "8.1.4" lifecycle_version = "2.6.2" paging_version = "3.2.1" diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index bdafa0fe2..39074cb0c 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,6 +1,6 @@ [versions] compiler = "1.5.4" -compose-bom = "2023.12.00-alpha01" +compose-bom = "2023.12.00-alpha02" accompanist = "0.33.2-alpha" [libraries] From 21ae04d25d0997319b0742627ba441dfee92cc95 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 25 Nov 2023 16:51:32 -0500 Subject: [PATCH 07/22] Minor download location cleanup --- .../tachiyomi/data/download/DownloadCache.kt | 63 +++++++++---------- .../data/download/DownloadProvider.kt | 32 +++++++--- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index cfcf1fe85..efeb9e416 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download import android.app.Application import android.content.Context import android.net.Uri -import androidx.core.net.toUri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.Source @@ -19,6 +18,7 @@ import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -64,7 +64,7 @@ class DownloadCache( private val provider: DownloadProvider = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), - private val storagePreferences: StoragePreferences = Injekt.get(), + storagePreferences: StoragePreferences = Injekt.get(), ) { private val scope = CoroutineScope(Dispatchers.IO) @@ -95,16 +95,9 @@ class DownloadCache( get() = File(context.cacheDir, "dl_index_cache") private val rootDownloadsDirLock = Mutex() - private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) + private var rootDownloadsDir = RootDirectory(provider.downloadsDir) init { - storagePreferences.baseStorageDirectory().changes() - .onEach { - rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) - invalidateCache() - } - .launchIn(scope) - // Attempt to read cache file scope.launch { rootDownloadsDirLock.withLock { @@ -119,6 +112,14 @@ class DownloadCache( } } } + + storagePreferences.baseStorageDirectory().changes() + .drop(1) + .onEach { + rootDownloadsDir = RootDirectory(provider.downloadsDir) + invalidateCache() + } + .launchIn(scope) } /** @@ -293,17 +294,6 @@ class DownloadCache( renewalJob?.cancel() } - /** - * Returns the downloads directory from the user's preferences. - */ - private fun getDirectoryFromPreference(): UniFile { - return storagePreferences.baseStorageDirectory().get().let { - UniFile.fromUri(context, it.toUri()).also { - it?.createDirectory(StoragePreferences.DOWNLOADS_DIR) - } - } - } - /** * Renews the downloads cache. */ @@ -335,7 +325,7 @@ class DownloadCache( val sourceMap = sources.associate { provider.getSourceDirName(it).lowercase() to it.id } rootDownloadsDirLock.withLock { - val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty() + val sourceDirs = rootDownloadsDir.dir?.listFiles().orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } .mapNotNull { dir -> val sourceId = sourceMap[dir.name!!.lowercase()] @@ -348,12 +338,12 @@ class DownloadCache( sourceDirs.values .map { sourceDir -> async { - sourceDir.mangaDirs = sourceDir.dir.listFiles().orEmpty() + sourceDir.mangaDirs = sourceDir.dir?.listFiles().orEmpty() .filter { it.isDirectory && !it.name.isNullOrBlank() } .associate { it.name!! to MangaDirectory(it) } sourceDir.mangaDirs.values.forEach { mangaDir -> - val chapterDirs = mangaDir.dir.listFiles().orEmpty() + val chapterDirs = mangaDir.dir?.listFiles().orEmpty() .mapNotNull { when { // Ignore incomplete downloads @@ -430,7 +420,7 @@ class DownloadCache( @Serializable private class RootDirectory( @Serializable(with = UniFileAsStringSerializer::class) - val dir: UniFile, + val dir: UniFile?, var sourceDirs: Map = mapOf(), ) @@ -440,7 +430,7 @@ private class RootDirectory( @Serializable private class SourceDirectory( @Serializable(with = UniFileAsStringSerializer::class) - val dir: UniFile, + val dir: UniFile?, var mangaDirs: Map = mapOf(), ) @@ -450,17 +440,26 @@ private class SourceDirectory( @Serializable private class MangaDirectory( @Serializable(with = UniFileAsStringSerializer::class) - val dir: UniFile, + val dir: UniFile?, var chapterDirs: MutableSet = mutableSetOf(), ) -private object UniFileAsStringSerializer : KSerializer { +private object UniFileAsStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: UniFile) { - return encoder.encodeString(value.uri.toString()) + override fun serialize(encoder: Encoder, value: UniFile?) { + return if (value == null) { + encoder.encodeNull() + } else { + encoder.encodeString(value.uri.toString()) + } } - override fun deserialize(decoder: Decoder): UniFile { - return UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString())) + + override fun deserialize(decoder: Decoder): UniFile? { + return if (decoder.decodeNotNullMark()) { + UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString())) + } else { + decoder.decodeNull() + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 2f40ab78c..59ed6b2f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -5,6 +5,10 @@ import androidx.core.net.toUri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.util.storage.DiskUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import logcat.LogPriority import tachiyomi.core.i18n.stringResource import tachiyomi.core.util.system.logcat @@ -23,17 +27,27 @@ import uy.kohesive.injekt.api.get */ class DownloadProvider( private val context: Context, - private val storagePreferences: StoragePreferences = Injekt.get(), + storagePreferences: StoragePreferences = Injekt.get(), ) { - private val downloadsDir: UniFile? - get() = storagePreferences.baseStorageDirectory().get().let { - UniFile.fromUri(context, it.toUri()) - ?.createDirectory(StoragePreferences.DOWNLOADS_DIR) - ?.also { dir -> - DiskUtil.createNoMediaFile(dir, context) - } - } + private val scope = CoroutineScope(Dispatchers.IO) + + private var _downloadsDir: UniFile? = + storagePreferences.baseStorageDirectory().get().let(::getDownloadsLocation) + val downloadsDir: UniFile? + get() = _downloadsDir + + init { + storagePreferences.baseStorageDirectory().changes() + .onEach { _downloadsDir = getDownloadsLocation(it) } + .launchIn(scope) + } + + private fun getDownloadsLocation(dir: String): UniFile? { + return UniFile.fromUri(context, dir.toUri()) + ?.createDirectory(StoragePreferences.DOWNLOADS_DIR) + ?.also { DiskUtil.createNoMediaFile(it, context) } + } /** * Returns the download directory for a manga. For internal use only. From cf9e60fd92b3cea9ab4a48a3b8f62329df388e8d Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 25 Nov 2023 17:06:15 -0500 Subject: [PATCH 08/22] Use unified storage location for local source --- .../java/eu/kanade/tachiyomi/di/AppModule.kt | 4 ++- .../kanade/tachiyomi/di/PreferenceModule.kt | 3 --- .../kanade/tachiyomi/util/storage/DiskUtil.kt | 19 ------------- .../tachiyomi/source/local/LocalSource.kt | 9 +++---- .../source/local/io/LocalSourceFileSystem.kt | 27 +++++++------------ .../source/local/io/LocalSourceFileSystem.kt | 6 ++--- 6 files changed, 19 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 6aab7c7bc..7a527329f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.json.Json import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML +import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.Database import tachiyomi.data.DatabaseHandler @@ -123,7 +124,8 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { ImageSaver(app) } - addSingletonFactory { LocalSourceFileSystem(app) } + addSingletonFactory { AndroidStorageFolderProvider(app) } + addSingletonFactory { LocalSourceFileSystem(get()) } addSingletonFactory { LocalCoverManager(app, get()) } // Asynchronously init expensive components for a faster cold start diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index 91c128ae8..257ff5dd4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -54,9 +54,6 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { BackupPreferences(get()) } - addSingletonFactory { - AndroidStorageFolderProvider(app) - } addSingletonFactory { StoragePreferences( folderProvider = get(), diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index 5ea61b5a5..fc0e81e65 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.util.storage import android.content.Context import android.media.MediaScannerConnection import android.net.Uri -import android.os.Environment import android.os.StatFs -import androidx.core.content.ContextCompat import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.util.lang.Hash import java.io.File @@ -64,23 +62,6 @@ object DiskUtil { } } - /** - * Returns the root folders of all the available external storages. - */ - fun getExternalStorages(context: Context): List { - return ContextCompat.getExternalFilesDirs(context, null) - .filterNotNull() - .mapNotNull { - val file = File(it.absolutePath.substringBefore("/Android/")) - val state = Environment.getExternalStorageState(file) - if (state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY) { - file - } else { - null - } - } - } - /** * Don't display downloaded chapters in gallery apps creating `.nomedia`. */ diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index c3be92d2c..8e2dc215c 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -73,7 +73,7 @@ actual class LocalSource( override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS) override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - val baseDirsFiles = fileSystem.getFilesInBaseDirectories() + val baseDirFiles = fileSystem.getFilesInBaseDirectory() val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) { System.currentTimeMillis() - LATEST_THRESHOLD @@ -81,7 +81,7 @@ actual class LocalSource( 0L } } - var mangaDirs = baseDirsFiles + var mangaDirs = baseDirFiles // Filter out files that are hidden and is not a folder .filter { it.isDirectory && !it.name.startsWith('.') } .distinctBy { it.name } @@ -308,9 +308,8 @@ actual class LocalSource( fun getFormat(chapter: SChapter): Format { try { - return fileSystem.getBaseDirectories() - .map { dir -> File(dir, chapter.url) } - .find { it.exists() } + return File(fileSystem.getBaseDirectory(), chapter.url) + .takeIf { it.exists() } ?.let(Format.Companion::valueOf) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) } catch (e: Format.UnknownFormatException) { diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index f645f8b75..9e53bc5e2 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -1,37 +1,28 @@ package tachiyomi.source.local.io -import android.content.Context -import eu.kanade.tachiyomi.util.storage.DiskUtil -import tachiyomi.core.i18n.stringResource -import tachiyomi.i18n.MR +import tachiyomi.core.provider.FolderProvider import java.io.File actual class LocalSourceFileSystem( - private val context: Context, + private val folderProvider: FolderProvider, ) { - private val baseFolderLocation = "${context.stringResource(MR.strings.app_name)}${File.separator}local" - - actual fun getBaseDirectories(): Sequence { - return DiskUtil.getExternalStorages(context) - .map { File(it.absolutePath, baseFolderLocation) } - .asSequence() + actual fun getBaseDirectory(): File { + return File(folderProvider.directory(), "local") } - actual fun getFilesInBaseDirectories(): Sequence { - return getBaseDirectories() - // Get all the files inside all baseDir - .flatMap { it.listFiles().orEmpty().toList() } + actual fun getFilesInBaseDirectory(): List { + return getBaseDirectory().listFiles().orEmpty().toList() } actual fun getMangaDirectory(name: String): File? { - return getFilesInBaseDirectories() + return getFilesInBaseDirectory() // Get the first mangaDir or null .firstOrNull { it.isDirectory && it.name == name } } - actual fun getFilesInMangaDirectory(name: String): Sequence { - return getFilesInBaseDirectories() + actual fun getFilesInMangaDirectory(name: String): List { + return getFilesInBaseDirectory() // Filter out ones that are not related to the manga and is not a directory .filter { it.isDirectory && it.name == name } // Get all the files inside the filtered folders diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index 0440df26e..f7e46c415 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -4,11 +4,11 @@ import java.io.File expect class LocalSourceFileSystem { - fun getBaseDirectories(): Sequence + fun getBaseDirectory(): File - fun getFilesInBaseDirectories(): Sequence + fun getFilesInBaseDirectory(): List fun getMangaDirectory(name: String): File? - fun getFilesInMangaDirectory(name: String): Sequence + fun getFilesInMangaDirectory(name: String): List } From d4dfa9a2c2a6e627256e99efb08e150a6d234964 Mon Sep 17 00:00:00 2001 From: Saud-97 <39028181+Saud-97@users.noreply.github.com> Date: Sun, 26 Nov 2023 18:16:06 +0300 Subject: [PATCH 09/22] Anilist decode item description HTML (#10181) --- .../eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 46de5e735..e0103371e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -4,6 +4,7 @@ import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.util.lang.htmlDecode import kotlinx.serialization.Serializable import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat @@ -25,7 +26,7 @@ data class ALManga( title = title_user_pref total_chapters = this@ALManga.total_chapters cover_url = image_url_lge - summary = description ?: "" + summary = description?.htmlDecode() ?: "" tracking_url = AnilistApi.mangaUrl(media_id) publishing_status = this@ALManga.publishing_status publishing_type = format From f365b53a0fbfb6f4147c688416e101aee9bae7fc Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 16:04:25 -0500 Subject: [PATCH 10/22] Move automatic backups from /backup/automatic to /autobackup Removes the need to try to create child folders, which simplifies things. --- .../main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt | 1 - .../java/tachiyomi/domain/storage/service/StoragePreferences.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt index 619e30e01..a8cd3b773 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -93,7 +93,6 @@ class BackupCreator( if (isAutoBackup) { // Get dir of file and create val dir = UniFile.fromUri(context, uri) - .createDirectory("automatic") // Delete older backups dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt index c336825b8..a426caaa4 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -11,7 +11,7 @@ class StoragePreferences( fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path()) companion object { - const val BACKUP_DIR = "backup" + const val BACKUP_DIR = "autobackup" const val DOWNLOADS_DIR = "downloads" } } From 46aeab9a7ac6fd5c45d426e3a733d716dcb1d25f Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 15:56:43 -0500 Subject: [PATCH 11/22] Add extensions for handling UniFile name/file extensions --- .../eu/kanade/tachiyomi/data/download/DownloadCache.kt | 5 +++-- .../eu/kanade/tachiyomi/data/download/DownloadManager.kt | 3 ++- .../java/eu/kanade/tachiyomi/data/download/Downloader.kt | 3 ++- .../java/tachiyomi/core/storage/UniFileExtensions.kt | 9 +++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index efeb9e416..9fe7dbfee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -40,6 +40,8 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority +import tachiyomi.core.storage.extension +import tachiyomi.core.storage.nameWithoutExtension import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.system.logcat @@ -351,8 +353,7 @@ class DownloadCache( // Folder of images it.isDirectory -> it.name // CBZ files - it.isFile && it.name?.endsWith(".cbz") == true -> - it.name!!.substringBeforeLast(".cbz") + it.isFile && it.extension == "cbz" -> it.nameWithoutExtension // Anything else is irrelevant else -> null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 0b404165f..ba5c4d81a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.runBlocking import logcat.LogPriority import tachiyomi.core.i18n.stringResource +import tachiyomi.core.storage.extension import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.system.logcat import tachiyomi.domain.category.interactor.GetCategories @@ -340,7 +341,7 @@ class DownloadManager( .firstOrNull() ?: return var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator) - if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) { + if (oldDownload.isFile && oldDownload.extension == "cbz") { newName += ".cbz" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index f89146e3a..74a90b3ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -43,6 +43,7 @@ import okhttp3.Response import tachiyomi.core.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo +import tachiyomi.core.storage.extension import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.withIOContext @@ -353,7 +354,7 @@ class Downloader( // Delete all temporary (unfinished) files tmpDir.listFiles() - ?.filter { it.name!!.endsWith(".tmp") } + ?.filter { it.extension == "tmp" } ?.forEach { it.delete() } download.status = Download.State.DOWNLOADING diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt new file mode 100644 index 000000000..9a869c883 --- /dev/null +++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt @@ -0,0 +1,9 @@ +package tachiyomi.core.storage + +import com.hippo.unifile.UniFile + +val UniFile.extension: String? + get() = name?.substringAfterLast('.') + +val UniFile.nameWithoutExtension: String? + get() = name?.substringBeforeLast('.') From ca5498434409d4085c404f4ff5ed5e608f430a3b Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 15:59:31 -0500 Subject: [PATCH 12/22] Use UniFile for local source file handling --- .../java/eu/kanade/tachiyomi/di/AppModule.kt | 4 +- .../kanade/tachiyomi/di/PreferenceModule.kt | 2 +- .../ui/reader/loader/DirectoryPageLoader.kt | 11 ++- .../ui/reader/loader/DownloadPageLoader.kt | 3 +- .../ui/reader/loader/EpubPageLoader.kt | 4 +- .../ui/reader/loader/RarPageLoader.kt | 7 +- .../ui/reader/loader/ZipPageLoader.kt | 9 +-- .../kanade/tachiyomi/util/storage/EpubFile.kt | 6 +- .../AndroidStorageFolderProvider.kt | 2 +- .../{provider => storage}/FolderProvider.kt | 2 +- .../core/storage/UniFileExtensions.kt | 3 + .../tachiyomi/core/util/system/ImageUtil.kt | 4 +- .../storage/service/StoragePreferences.kt | 2 +- .../tachiyomi/source/local/LocalSource.kt | 68 +++++++++++-------- .../source/local/image/LocalCoverManager.kt | 22 +++--- .../source/local/io/LocalSourceFileSystem.kt | 20 +++--- .../source/local/image/LocalCoverManager.kt | 6 +- .../tachiyomi/source/local/io/Archive.kt | 7 +- .../tachiyomi/source/local/io/Format.kt | 13 ++-- .../source/local/io/LocalSourceFileSystem.kt | 10 +-- 20 files changed, 110 insertions(+), 95 deletions(-) rename core/src/main/java/tachiyomi/core/{provider => storage}/AndroidStorageFolderProvider.kt (94%) rename core/src/main/java/tachiyomi/core/{provider => storage}/FolderProvider.kt (76%) diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 7a527329f..2744644f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -25,7 +25,7 @@ import kotlinx.serialization.json.Json import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML -import tachiyomi.core.provider.AndroidStorageFolderProvider +import tachiyomi.core.storage.AndroidStorageFolderProvider import tachiyomi.data.AndroidDatabaseHandler import tachiyomi.data.Database import tachiyomi.data.DatabaseHandler @@ -125,7 +125,7 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { ImageSaver(app) } addSingletonFactory { AndroidStorageFolderProvider(app) } - addSingletonFactory { LocalSourceFileSystem(get()) } + addSingletonFactory { LocalSourceFileSystem(app, get()) } addSingletonFactory { LocalCoverManager(app, get()) } // Asynchronously init expensive components for a faster cold start diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index 257ff5dd4..51136226b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.util.system.isDevFlavor import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.PreferenceStore -import tachiyomi.core.provider.AndroidStorageFolderProvider +import tachiyomi.core.storage.AndroidStorageFolderProvider import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt index 837986b28..2a11f74e3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DirectoryPageLoader.kt @@ -1,25 +1,24 @@ package eu.kanade.tachiyomi.ui.reader.loader +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import tachiyomi.core.util.system.ImageUtil -import java.io.File -import java.io.FileInputStream /** * Loader used to load a chapter from a directory given on [file]. */ -internal class DirectoryPageLoader(val file: File) : PageLoader() { +internal class DirectoryPageLoader(val file: UniFile) : PageLoader() { override var isLocal: Boolean = true override suspend fun getPages(): List { return file.listFiles() - ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } - ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + ?.filter { !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } } + ?.sortedWith { f1, f2 -> f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder(f2.name.orEmpty()) } ?.mapIndexed { i, file -> - val streamFn = { FileInputStream(file) } + val streamFn = { file.openInputStream() } ReaderPage(i).apply { stream = streamFn status = Page.State.READY diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index 64b6a73f5..4fb5adb6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy -import java.io.File /** * Loader used to load a chapter from the downloaded chapters. @@ -47,7 +46,7 @@ internal class DownloadPageLoader( } private suspend fun getPagesFromArchive(chapterPath: UniFile): List { - val loader = ZipPageLoader(File(chapterPath.filePath!!)).also { zipPageLoader = it } + val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it } return loader.getPages() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index 324af51bf..cd00e3756 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -1,14 +1,14 @@ package eu.kanade.tachiyomi.ui.reader.loader +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile -import java.io.File /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(file: File) : PageLoader() { +internal class EpubPageLoader(file: UniFile) : PageLoader() { private val epub = EpubFile(file) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 056319d4e..319179117 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -2,11 +2,12 @@ package eu.kanade.tachiyomi.ui.reader.loader import com.github.junrar.Archive import com.github.junrar.rarfile.FileHeader +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import tachiyomi.core.storage.toFile import tachiyomi.core.util.system.ImageUtil -import java.io.File import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream @@ -14,9 +15,9 @@ import java.io.PipedOutputStream /** * Loader used to load a chapter from a .rar or .cbr file. */ -internal class RarPageLoader(file: File) : PageLoader() { +internal class RarPageLoader(file: UniFile) : PageLoader() { - private val rar = Archive(file) + private val rar = Archive(file.toFile()) override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index e04fe78e6..b63b32557 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -1,23 +1,24 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.os.Build +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder +import tachiyomi.core.storage.toFile import tachiyomi.core.util.system.ImageUtil -import java.io.File import java.nio.charset.StandardCharsets import java.util.zip.ZipFile /** * Loader used to load a chapter from a .zip or .cbz file. */ -internal class ZipPageLoader(file: File) : PageLoader() { +internal class ZipPageLoader(file: UniFile) : PageLoader() { private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ZipFile(file, StandardCharsets.ISO_8859_1) + ZipFile(file.toFile(), StandardCharsets.ISO_8859_1) } else { - ZipFile(file) + ZipFile(file.toFile()) } override var isLocal: Boolean = true diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt index a00ee69e7..7650f65b5 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,7 +1,9 @@ package eu.kanade.tachiyomi.util.storage +import com.hippo.unifile.UniFile import org.jsoup.Jsoup import org.jsoup.nodes.Document +import tachiyomi.core.storage.toFile import java.io.Closeable import java.io.File import java.io.InputStream @@ -11,12 +13,12 @@ import java.util.zip.ZipFile /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(file: File) : Closeable { +class EpubFile(file: UniFile) : Closeable { /** * Zip file of this epub. */ - private val zip = ZipFile(file) + private val zip = ZipFile(file.toFile()) /** * Path separator used by this epub. diff --git a/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt b/core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt similarity index 94% rename from core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt rename to core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt index fc2859868..a5d48a49d 100644 --- a/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt +++ b/core/src/main/java/tachiyomi/core/storage/AndroidStorageFolderProvider.kt @@ -1,4 +1,4 @@ -package tachiyomi.core.provider +package tachiyomi.core.storage import android.content.Context import android.os.Environment diff --git a/core/src/main/java/tachiyomi/core/provider/FolderProvider.kt b/core/src/main/java/tachiyomi/core/storage/FolderProvider.kt similarity index 76% rename from core/src/main/java/tachiyomi/core/provider/FolderProvider.kt rename to core/src/main/java/tachiyomi/core/storage/FolderProvider.kt index b4e124cee..decd1c378 100644 --- a/core/src/main/java/tachiyomi/core/provider/FolderProvider.kt +++ b/core/src/main/java/tachiyomi/core/storage/FolderProvider.kt @@ -1,4 +1,4 @@ -package tachiyomi.core.provider +package tachiyomi.core.storage import java.io.File diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt index 9a869c883..5343dfa3f 100644 --- a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt +++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt @@ -1,9 +1,12 @@ package tachiyomi.core.storage import com.hippo.unifile.UniFile +import java.io.File val UniFile.extension: String? get() = name?.substringAfterLast('.') val UniFile.nameWithoutExtension: String? get() = name?.substringBeforeLast('.') + +fun UniFile.toFile(): File? = filePath?.let { File(it) } diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index b6cbc45e6..f72a963b7 100644 --- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -37,7 +37,9 @@ import kotlin.math.min object ImageUtil { - fun isImage(name: String, openStream: (() -> InputStream)? = null): Boolean { + fun isImage(name: String?, openStream: (() -> InputStream)? = null): Boolean { + if (name == null) return false + val contentType = try { URLConnection.guessContentTypeFromName(name) } catch (e: Exception) { diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt index a426caaa4..e930ebaa4 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -1,7 +1,7 @@ package tachiyomi.domain.storage.service import tachiyomi.core.preference.PreferenceStore -import tachiyomi.core.provider.FolderProvider +import tachiyomi.core.storage.FolderProvider class StoragePreferences( private val folderProvider: FolderProvider, diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 8e2dc215c..1c91597d0 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -22,6 +22,9 @@ import tachiyomi.core.metadata.comicinfo.ComicInfo import tachiyomi.core.metadata.comicinfo.copyFromComicInfo import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails +import tachiyomi.core.storage.extension +import tachiyomi.core.storage.nameWithoutExtension +import tachiyomi.core.storage.toFile import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.logcat @@ -37,7 +40,6 @@ import tachiyomi.source.local.metadata.fillChapterMetadata import tachiyomi.source.local.metadata.fillMangaMetadata import uy.kohesive.injekt.injectLazy import java.io.File -import java.io.FileInputStream import java.io.InputStream import java.nio.charset.StandardCharsets import java.util.zip.ZipFile @@ -83,11 +85,11 @@ actual class LocalSource( } var mangaDirs = baseDirFiles // Filter out files that are hidden and is not a folder - .filter { it.isDirectory && !it.name.startsWith('.') } + .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') } .distinctBy { it.name } .filter { // Filter by query or last modified if (lastModifiedLimit == 0L) { - it.name.contains(query, ignoreCase = true) + it.name.orEmpty().contains(query, ignoreCase = true) } else { it.lastModified() >= lastModifiedLimit } @@ -97,16 +99,16 @@ actual class LocalSource( when (filter) { is OrderBy.Popular -> { mangaDirs = if (filter.state!!.ascending) { - mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) + mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }) } else { - mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name }) + mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name.orEmpty() }) } } is OrderBy.Latest -> { mangaDirs = if (filter.state!!.ascending) { - mangaDirs.sortedBy(File::lastModified) + mangaDirs.sortedBy(UniFile::lastModified) } else { - mangaDirs.sortedByDescending(File::lastModified) + mangaDirs.sortedByDescending(UniFile::lastModified) } } @@ -119,13 +121,13 @@ actual class LocalSource( // Transform mangaDirs to list of SManga val mangas = mangaDirs.map { mangaDir -> SManga.create().apply { - title = mangaDir.name - url = mangaDir.name + title = mangaDir.name.orEmpty() + url = mangaDir.name.orEmpty() // Try to find the cover - coverManager.find(mangaDir.name) - ?.takeIf(File::exists) - ?.let { thumbnail_url = it.absolutePath } + coverManager.find(mangaDir.name.orEmpty()) + ?.takeIf(UniFile::exists) + ?.let { thumbnail_url = it.uri.toString() } } } @@ -155,7 +157,7 @@ actual class LocalSource( // Manga details related override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { coverManager.find(manga.url)?.let { - manga.thumbnail_url = it.absolutePath + manga.thumbnail_url = it.uri.toString() } // Augment manga details based on metadata files @@ -174,13 +176,13 @@ actual class LocalSource( // Top level ComicInfo.xml comicInfoFile != null -> { noXmlFile?.delete() - setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga) + setMangaDetailsFromComicInfoFile(comicInfoFile.openInputStream(), manga) } // Old custom JSON format // TODO: remove support for this entirely after a while legacyJsonDetailsFile != null -> { - json.decodeFromStream(legacyJsonDetailsFile.inputStream()).run { + json.decodeFromStream(legacyJsonDetailsFile.openInputStream()).run { title?.let { manga.title = it } author?.let { manga.author = it } artist?.let { manga.artist = it } @@ -190,7 +192,7 @@ actual class LocalSource( } // Replace with ComicInfo.xml file val comicInfo = manga.getComicInfo() - UniFile.fromFile(mangaDir) + mangaDir ?.createFile(COMIC_INFO_FILE) ?.openOutputStream() ?.use { @@ -206,7 +208,7 @@ actual class LocalSource( .filter(Archive::isSupported) .toList() - val folderPath = mangaDir?.absolutePath + val folderPath = mangaDir?.filePath val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath) if (copiedFile != null) { @@ -224,11 +226,11 @@ actual class LocalSource( return@withIOContext manga } - private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? { + private fun copyComicInfoFileFromArchive(chapterArchives: List, folderPath: String?): File? { for (chapter in chapterArchives) { when (Format.valueOf(chapter)) { is Format.Zip -> { - ZipFile(chapter).use { zip: ZipFile -> + ZipFile(chapter.toFile()).use { zip: ZipFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -237,7 +239,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(chapter).use { rar -> + JunrarArchive(chapter.toFile()).use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -276,9 +278,9 @@ actual class LocalSource( SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" name = if (chapterFile.isDirectory) { - chapterFile.name + chapterFile.name.orEmpty() } else { - chapterFile.nameWithoutExtension + chapterFile.nameWithoutExtension.orEmpty() } date_upload = chapterFile.lastModified() chapter_number = ChapterRecognition @@ -308,8 +310,8 @@ actual class LocalSource( fun getFormat(chapter: SChapter): Format { try { - return File(fileSystem.getBaseDirectory(), chapter.url) - .takeIf { it.exists() } + return fileSystem.getBaseDirectory() + ?.findFile(chapter.url) ?.let(Format.Companion::valueOf) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) } catch (e: Format.UnknownFormatException) { @@ -319,18 +321,24 @@ actual class LocalSource( } } - private fun updateCover(chapter: SChapter, manga: SManga): File? { + private fun updateCover(chapter: SChapter, manga: SManga): UniFile? { return try { when (val format = getFormat(chapter)) { is Format.Directory -> { val entry = format.file.listFiles() - ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } + ?.sortedWith { f1, f2 -> + f1.name.orEmpty().compareToCaseInsensitiveNaturalOrder( + f2.name.orEmpty(), + ) + } + ?.find { + !it.isDirectory && ImageUtil.isImage(it.name) { it.openInputStream() } + } - entry?.let { coverManager.update(manga, it.inputStream()) } + entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Zip -> { - ZipFile(format.file).use { zip -> + ZipFile(format.file.toFile()).use { zip -> val entry = zip.entries().toList() .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } @@ -339,7 +347,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(format.file).use { archive -> + JunrarArchive(format.file.toFile()).use { archive -> val entry = archive.fileHeaders .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index 7683756e3..b19968fb2 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -4,9 +4,9 @@ import android.content.Context import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.storage.DiskUtil +import tachiyomi.core.storage.nameWithoutExtension import tachiyomi.core.util.system.ImageUtil import tachiyomi.source.local.io.LocalSourceFileSystem -import java.io.File import java.io.InputStream private const val DEFAULT_COVER_NAME = "cover.jpg" @@ -16,43 +16,37 @@ actual class LocalCoverManager( private val fileSystem: LocalSourceFileSystem, ) { - actual fun find(mangaUrl: String): File? { + actual fun find(mangaUrl: String): UniFile? { return fileSystem.getFilesInMangaDirectory(mangaUrl) // Get all file whose names start with "cover" .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } // Get the first actual image .firstOrNull { - ImageUtil.isImage(it.name) { it.inputStream() } + ImageUtil.isImage(it.name) { it.openInputStream() } } } actual fun update( manga: SManga, inputStream: InputStream, - ): File? { + ): UniFile? { val directory = fileSystem.getMangaDirectory(manga.url) if (directory == null) { inputStream.close() return null } - var targetFile = find(manga.url) - if (targetFile == null) { - targetFile = File(directory.absolutePath, DEFAULT_COVER_NAME) - targetFile.createNewFile() - } + val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME) - // It might not exist at this point - targetFile.parentFile?.mkdirs() inputStream.use { input -> - targetFile.outputStream().use { output -> + targetFile.openOutputStream().use { output -> input.copyTo(output) } } - DiskUtil.createNoMediaFile(UniFile.fromFile(directory), context) + DiskUtil.createNoMediaFile(directory, context) - manga.thumbnail_url = targetFile.absolutePath + manga.thumbnail_url = targetFile.uri.toString() return targetFile } } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index 9e53bc5e2..a5ce8a513 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -1,27 +1,31 @@ package tachiyomi.source.local.io -import tachiyomi.core.provider.FolderProvider -import java.io.File +import android.content.Context +import androidx.core.net.toUri +import com.hippo.unifile.UniFile +import tachiyomi.core.storage.FolderProvider actual class LocalSourceFileSystem( + private val context: Context, private val folderProvider: FolderProvider, ) { - actual fun getBaseDirectory(): File { - return File(folderProvider.directory(), "local") + actual fun getBaseDirectory(): UniFile? { + return UniFile.fromUri(context, folderProvider.path().toUri()) + ?.createDirectory("local") } - actual fun getFilesInBaseDirectory(): List { - return getBaseDirectory().listFiles().orEmpty().toList() + actual fun getFilesInBaseDirectory(): List { + return getBaseDirectory()?.listFiles().orEmpty().toList() } - actual fun getMangaDirectory(name: String): File? { + actual fun getMangaDirectory(name: String): UniFile? { return getFilesInBaseDirectory() // Get the first mangaDir or null .firstOrNull { it.isDirectory && it.name == name } } - actual fun getFilesInMangaDirectory(name: String): List { + actual fun getFilesInMangaDirectory(name: String): List { return getFilesInBaseDirectory() // Filter out ones that are not related to the manga and is not a directory .filter { it.isDirectory && it.name == name } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index fd31299c2..037d9f1dc 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -1,12 +1,12 @@ package tachiyomi.source.local.image +import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.SManga -import java.io.File import java.io.InputStream expect class LocalCoverManager { - fun find(mangaUrl: String): File? + fun find(mangaUrl: String): UniFile? - fun update(manga: SManga, inputStream: InputStream): File? + fun update(manga: SManga, inputStream: InputStream): UniFile? } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt index b28ee60b5..a8f5a0740 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Archive.kt @@ -1,12 +1,13 @@ package tachiyomi.source.local.io -import java.io.File +import com.hippo.unifile.UniFile +import tachiyomi.core.storage.extension object Archive { private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub") - fun isSupported(file: File): Boolean = with(file) { - return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES + fun isSupported(file: UniFile): Boolean { + return file.extension in SUPPORTED_ARCHIVE_TYPES } } diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt index 53406b5de..0f29ae8ab 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/Format.kt @@ -1,18 +1,19 @@ package tachiyomi.source.local.io -import java.io.File +import com.hippo.unifile.UniFile +import tachiyomi.core.storage.extension sealed interface Format { - data class Directory(val file: File) : Format - data class Zip(val file: File) : Format - data class Rar(val file: File) : Format - data class Epub(val file: File) : Format + data class Directory(val file: UniFile) : Format + data class Zip(val file: UniFile) : Format + data class Rar(val file: UniFile) : Format + data class Epub(val file: UniFile) : Format class UnknownFormatException : Exception() companion object { - fun valueOf(file: File) = with(file) { + fun valueOf(file: UniFile) = with(file) { when { isDirectory -> Directory(this) extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this) diff --git a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index f7e46c415..5aa74d851 100644 --- a/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/commonMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -1,14 +1,14 @@ package tachiyomi.source.local.io -import java.io.File +import com.hippo.unifile.UniFile expect class LocalSourceFileSystem { - fun getBaseDirectory(): File + fun getBaseDirectory(): UniFile? - fun getFilesInBaseDirectory(): List + fun getFilesInBaseDirectory(): List - fun getMangaDirectory(name: String): File? + fun getMangaDirectory(name: String): UniFile? - fun getFilesInMangaDirectory(name: String): List + fun getFilesInMangaDirectory(name: String): List } From 27c4db752ce599a99876021c1019cfb549153a30 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 25 Nov 2023 22:31:26 -0500 Subject: [PATCH 13/22] Actually use configured storage location for local source Fixes #10178 --- .../settings/screen/SettingsDataScreen.kt | 2 + .../tachiyomi/data/backup/BackupCreateJob.kt | 14 +++-- .../data/download/DownloadProvider.kt | 27 ++-------- .../java/eu/kanade/tachiyomi/di/AppModule.kt | 4 +- domain/build.gradle.kts | 2 + .../domain/storage/service/StorageManager.kt | 54 +++++++++++++++++++ .../storage/service/StoragePreferences.kt | 5 -- .../tachiyomi/source/local/LocalSource.kt | 4 +- .../source/local/io/LocalSourceFileSystem.kt | 10 ++-- 9 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt 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 461ec4eeb..341cf2bb5 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 @@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.copyToClipboard @@ -100,6 +101,7 @@ object SettingsDataScreen : SearchableSettings { val file = UniFile.fromUri(context, uri) storageDirPref.set(file.uri.toString()) + Injekt.get().invalidateCache() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index c28d12f26..825d66c93 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.util.system.workManager import logcat.LogPriority import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences -import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.storage.service.StorageManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date @@ -43,6 +43,8 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: getAutomaticBackupLocation() + ?: return Result.failure() + val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults) try { @@ -75,13 +77,9 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete ) } - private fun getAutomaticBackupLocation(): Uri { - val storagePreferences = Injekt.get() - return storagePreferences.baseStorageDirectory().get().let { - val dir = UniFile.fromUri(context, it.toUri()) - .createDirectory(StoragePreferences.BACKUP_DIR) - dir.uri - } + private fun getAutomaticBackupLocation(): Uri? { + val storageManager = Injekt.get() + return storageManager.getAutomaticBackupsDirectory()?.uri } companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 59ed6b2f8..77dbfd264 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -1,20 +1,15 @@ package eu.kanade.tachiyomi.data.download import android.content.Context -import androidx.core.net.toUri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.util.storage.DiskUtil -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import logcat.LogPriority import tachiyomi.core.i18n.stringResource import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.manga.model.Manga -import tachiyomi.domain.storage.service.StoragePreferences +import tachiyomi.domain.storage.service.StorageManager import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -27,27 +22,11 @@ import uy.kohesive.injekt.api.get */ class DownloadProvider( private val context: Context, - storagePreferences: StoragePreferences = Injekt.get(), + private val storageManager: StorageManager = Injekt.get(), ) { - private val scope = CoroutineScope(Dispatchers.IO) - - private var _downloadsDir: UniFile? = - storagePreferences.baseStorageDirectory().get().let(::getDownloadsLocation) val downloadsDir: UniFile? - get() = _downloadsDir - - init { - storagePreferences.baseStorageDirectory().changes() - .onEach { _downloadsDir = getDownloadsLocation(it) } - .launchIn(scope) - } - - private fun getDownloadsLocation(dir: String): UniFile? { - return UniFile.fromUri(context, dir.toUri()) - ?.createDirectory(StoragePreferences.DOWNLOADS_DIR) - ?.also { DiskUtil.createNoMediaFile(it, context) } - } + get() = storageManager.getDownloadsDirectory() /** * Returns the download directory for a manga. For internal use only. diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt index 2744644f8..3477cb296 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt @@ -35,6 +35,7 @@ import tachiyomi.data.Mangas import tachiyomi.data.StringListColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.source.service.SourceManager +import tachiyomi.domain.storage.service.StorageManager import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.io.LocalSourceFileSystem import uy.kohesive.injekt.api.InjektModule @@ -125,8 +126,9 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { ImageSaver(app) } addSingletonFactory { AndroidStorageFolderProvider(app) } - addSingletonFactory { LocalSourceFileSystem(app, get()) } + addSingletonFactory { LocalSourceFileSystem(get()) } addSingletonFactory { LocalCoverManager(app, get()) } + addSingletonFactory { StorageManager(app, get()) } // Asynchronously init expensive components for a faster cold start ContextCompat.getMainExecutor(app).execute { diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 2ce1a4d55..4df15c79a 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { implementation(platform(kotlinx.coroutines.bom)) implementation(kotlinx.bundles.coroutines) + implementation(libs.unifile) + api(libs.sqldelight.android.paging) testImplementation(libs.bundles.test) diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt new file mode 100644 index 000000000..a1fff4269 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt @@ -0,0 +1,54 @@ +package tachiyomi.domain.storage.service + +import android.content.Context +import androidx.core.net.toUri +import com.hippo.unifile.UniFile +import eu.kanade.tachiyomi.util.storage.DiskUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class StorageManager( + private val context: Context, + storagePreferences: StoragePreferences, +) { + + private val scope = CoroutineScope(Dispatchers.IO) + + private var baseDir: UniFile? = storagePreferences.baseStorageDirectory().get().let(::getBaseDir) + + init { + storagePreferences.baseStorageDirectory().changes() + .onEach { baseDir = getBaseDir(it) } + .launchIn(scope) + } + + private fun getBaseDir(path: String): UniFile? { + val file = UniFile.fromUri(context, path.toUri()) + + return file.takeIf { it?.exists() == true }?.also { parent -> + parent.createDirectory(AUTOMATIC_BACKUPS_PATH) + parent.createDirectory(LOCAL_SOURCE_PATH) + parent.createDirectory(DOWNLOADS_PATH).also { + DiskUtil.createNoMediaFile(it, context) + } + } + } + + fun getAutomaticBackupsDirectory(): UniFile? { + return baseDir?.createDirectory(AUTOMATIC_BACKUPS_PATH) + } + + fun getDownloadsDirectory(): UniFile? { + return baseDir?.createDirectory(DOWNLOADS_PATH) + } + + fun getLocalSourceDirectory(): UniFile? { + return baseDir?.createDirectory(LOCAL_SOURCE_PATH) + } +} + +private const val AUTOMATIC_BACKUPS_PATH = "autobackup" +private const val DOWNLOADS_PATH = "downloads" +private const val LOCAL_SOURCE_PATH = "local" diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt index e930ebaa4..8f7c3fcc6 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -9,9 +9,4 @@ class StoragePreferences( ) { fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path()) - - companion object { - const val BACKUP_DIR = "autobackup" - const val DOWNLOADS_DIR = "downloads" - } } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 1c91597d0..f971caa1d 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -310,8 +310,10 @@ actual class LocalSource( fun getFormat(chapter: SChapter): Format { try { + val (mangaDirName, chapterName) = chapter.url.split(File.separator, limit = 2) return fileSystem.getBaseDirectory() - ?.findFile(chapter.url) + ?.findFile(mangaDirName) + ?.findFile(chapterName) ?.let(Format.Companion::valueOf) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) } catch (e: Format.UnknownFormatException) { diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index a5ce8a513..8642ac21b 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -1,18 +1,14 @@ package tachiyomi.source.local.io -import android.content.Context -import androidx.core.net.toUri import com.hippo.unifile.UniFile -import tachiyomi.core.storage.FolderProvider +import tachiyomi.domain.storage.service.StorageManager actual class LocalSourceFileSystem( - private val context: Context, - private val folderProvider: FolderProvider, + private val storageManager: StorageManager, ) { actual fun getBaseDirectory(): UniFile? { - return UniFile.fromUri(context, folderProvider.path().toUri()) - ?.createDirectory("local") + return storageManager.getLocalSourceDirectory() } actual fun getFilesInBaseDirectory(): List { From bf524595e27e5a42aceb6df0dae2817cefac63c6 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 16:36:42 -0500 Subject: [PATCH 14/22] Show copied to clipboard toast on Samsung devices even if Android 13+ Since OneUI didn't implement the AOSP thing. --- .../java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 14f637366..43bbd9046 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -46,7 +46,7 @@ fun Context.copyToClipboard(label: String, content: String) { // Android 13 and higher shows a visual confirmation of copied contents // https://developer.android.com/about/versions/13/features/copy-paste - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || DeviceUtil.isSamsung) { toast(stringResource(MR.strings.copied_to_clipboard, content.truncateCenter(50))) } } catch (e: Throwable) { From 9ce0bc6b5fbdf8b8008adb1feae21b8346223453 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 18:45:16 -0500 Subject: [PATCH 15/22] Adjust stats overview icons Closes #9865 Still sort of weird, but the icons are now always aligned. --- .../more/stats/StatsScreenContent.kt | 17 +++++++++++------ .../more/stats/components/StatsItem.kt | 7 ++++++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt index f35336d50..64f59280c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/StatsScreenContent.kt @@ -1,8 +1,10 @@ package eu.kanade.presentation.more.stats import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons @@ -12,6 +14,7 @@ import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import eu.kanade.presentation.more.stats.components.StatsItem import eu.kanade.presentation.more.stats.components.StatsOverviewItem @@ -63,22 +66,24 @@ private fun OverviewSection( .toDurationString(context, fallback = none) } StatsSection(MR.strings.label_overview_section) { - Row { + Row( + modifier = Modifier.height(IntrinsicSize.Min), + ) { StatsOverviewItem( title = data.libraryMangaCount.toString(), subtitle = stringResource(MR.strings.in_library), icon = Icons.Outlined.CollectionsBookmark, ) - StatsOverviewItem( - title = data.completedMangaCount.toString(), - subtitle = stringResource(MR.strings.label_completed_titles), - icon = Icons.Outlined.LocalLibrary, - ) StatsOverviewItem( title = readDurationString, subtitle = stringResource(MR.strings.label_read_duration), icon = Icons.Outlined.Schedule, ) + StatsOverviewItem( + title = data.completedMangaCount.toString(), + subtitle = stringResource(MR.strings.label_completed_titles), + icon = Icons.Outlined.LocalLibrary, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt index 32f4fcd28..8002b3d04 100644 --- a/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt +++ b/app/src/main/java/eu/kanade/presentation/more/stats/components/StatsItem.kt @@ -3,6 +3,8 @@ package eu.kanade.presentation.more.stats.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -53,7 +55,9 @@ private fun RowScope.BaseStatsItem( icon: ImageVector? = null, ) { Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -74,6 +78,7 @@ private fun RowScope.BaseStatsItem( textAlign = TextAlign.Center, ) if (icon != null) { + Spacer(modifier = Modifier.weight(1f)) Icon( imageVector = icon, contentDescription = null, From 82bdf634194734851c429d60b68f9ce7c7e51d91 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 26 Nov 2023 22:46:55 -0500 Subject: [PATCH 16/22] Differ extra attempts to load local series' covers until chapter loading --- .../tachiyomi/source/local/LocalSource.kt | 101 ++++++++---------- .../source/local/image/LocalCoverManager.kt | 4 +- .../source/local/io/LocalSourceFileSystem.kt | 15 ++- .../source/local/metadata/EpubFile.kt | 26 ++--- 4 files changed, 61 insertions(+), 85 deletions(-) diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index f971caa1d..3282137ea 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -11,6 +11,8 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.EpubFile +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority @@ -36,8 +38,7 @@ import tachiyomi.source.local.image.LocalCoverManager import tachiyomi.source.local.io.Archive import tachiyomi.source.local.io.Format import tachiyomi.source.local.io.LocalSourceFileSystem -import tachiyomi.source.local.metadata.fillChapterMetadata -import tachiyomi.source.local.metadata.fillMangaMetadata +import tachiyomi.source.local.metadata.fillMetadata import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream @@ -74,21 +75,21 @@ actual class LocalSource( override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS) - override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage { - val baseDirFiles = fileSystem.getFilesInBaseDirectory() - val lastModifiedLimit by lazy { - if (filters === LATEST_FILTERS) { - System.currentTimeMillis() - LATEST_THRESHOLD - } else { - 0L - } + override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext { + val lastModifiedLimit = if (filters === LATEST_FILTERS) { + System.currentTimeMillis() - LATEST_THRESHOLD + } else { + 0L } - var mangaDirs = baseDirFiles + + var mangaDirs = fileSystem.getFilesInBaseDirectory() // Filter out files that are hidden and is not a folder .filter { it.isDirectory && !it.name.orEmpty().startsWith('.') } .distinctBy { it.name } - .filter { // Filter by query or last modified - if (lastModifiedLimit == 0L) { + .filter { + if (lastModifiedLimit == 0L && query.isBlank()) { + true + } else if (lastModifiedLimit == 0L) { it.name.orEmpty().contains(query, ignoreCase = true) } else { it.lastModified() >= lastModifiedLimit @@ -111,59 +112,41 @@ actual class LocalSource( mangaDirs.sortedByDescending(UniFile::lastModified) } } - else -> { /* Do nothing */ } } } - // Transform mangaDirs to list of SManga - val mangas = mangaDirs.map { mangaDir -> - SManga.create().apply { - title = mangaDir.name.orEmpty() - url = mangaDir.name.orEmpty() + val mangas = mangaDirs + .map { mangaDir -> + async { + SManga.create().apply { + title = mangaDir.name.orEmpty() + url = mangaDir.name.orEmpty() - // Try to find the cover - coverManager.find(mangaDir.name.orEmpty()) - ?.takeIf(UniFile::exists) - ?.let { thumbnail_url = it.uri.toString() } - } - } - - // Fetch chapters of all the manga - mangas.forEach { manga -> - val chapters = getChapterList(manga) - if (chapters.isNotEmpty()) { - val chapter = chapters.last() - val format = getFormat(chapter) - - if (format is Format.Epub) { - EpubFile(format.file).use { epub -> - epub.fillMangaMetadata(manga) + // Try to find the cover + coverManager.find(mangaDir.name.orEmpty())?.let { + thumbnail_url = it.filePath + } } } - - // Copy the cover from the first chapter found if not available - if (manga.thumbnail_url == null) { - updateCover(chapter, manga) - } } - } + .awaitAll() - return MangasPage(mangas.toList(), false) + MangasPage(mangas, false) } // Manga details related override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext { coverManager.find(manga.url)?.let { - manga.thumbnail_url = it.uri.toString() + manga.thumbnail_url = it.filePath } // Augment manga details based on metadata files try { - val mangaDir = fileSystem.getMangaDirectory(manga.url) - val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList() + val mangaDir by lazy { fileSystem.getMangaDirectory(manga.url) } + val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url) val comicInfoFile = mangaDirFiles .firstOrNull { it.name == COMIC_INFO_FILE } @@ -215,7 +198,7 @@ actual class LocalSource( setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga) } else { // Avoid re-scanning - File("$folderPath/.noxml").createNewFile() + mangaDir?.createFile(".noxml") } } } @@ -270,18 +253,18 @@ actual class LocalSource( } // Chapters - override suspend fun getChapterList(manga: SManga): List { - return fileSystem.getFilesInMangaDirectory(manga.url) + override suspend fun getChapterList(manga: SManga): List = withIOContext { + val chapters = fileSystem.getFilesInMangaDirectory(manga.url) // Only keep supported formats .filter { it.isDirectory || Archive.isSupported(it) } .map { chapterFile -> SChapter.create().apply { url = "${manga.url}/${chapterFile.name}" name = if (chapterFile.isDirectory) { - chapterFile.name.orEmpty() + chapterFile.name } else { - chapterFile.nameWithoutExtension.orEmpty() - } + chapterFile.nameWithoutExtension + }.orEmpty() date_upload = chapterFile.lastModified() chapter_number = ChapterRecognition .parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble()) @@ -290,7 +273,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { EpubFile(format.file).use { epub -> - epub.fillChapterMetadata(this) + epub.fillMetadata(manga, this) } } } @@ -299,7 +282,15 @@ actual class LocalSource( val c = c2.chapter_number.compareTo(c1.chapter_number) if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c } - .toList() + + // Copy the cover from the first chapter found if not available + if (manga.thumbnail_url.isNullOrBlank()) { + chapters.lastOrNull()?.let { chapter -> + updateCover(chapter, manga) + } + } + + chapters } // Filters @@ -310,7 +301,7 @@ actual class LocalSource( fun getFormat(chapter: SChapter): Format { try { - val (mangaDirName, chapterName) = chapter.url.split(File.separator, limit = 2) + val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2) return fileSystem.getBaseDirectory() ?.findFile(mangaDirName) ?.findFile(chapterName) diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index b19968fb2..d793357f5 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -21,9 +21,7 @@ actual class LocalCoverManager( // Get all file whose names start with "cover" .filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) } // Get the first actual image - .firstOrNull { - ImageUtil.isImage(it.name) { it.openInputStream() } - } + .firstOrNull { ImageUtil.isImage(it.name) { it.openInputStream() } } } actual fun update( diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt index 8642ac21b..ad95b39ce 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/io/LocalSourceFileSystem.kt @@ -16,16 +16,15 @@ actual class LocalSourceFileSystem( } actual fun getMangaDirectory(name: String): UniFile? { - return getFilesInBaseDirectory() - // Get the first mangaDir or null - .firstOrNull { it.isDirectory && it.name == name } + return getBaseDirectory() + ?.findFile(name, true) + ?.takeIf { it.isDirectory } } actual fun getFilesInMangaDirectory(name: String): List { - return getFilesInBaseDirectory() - // Filter out ones that are not related to the manga and is not a directory - .filter { it.isDirectory && it.name == name } - // Get all the files inside the filtered folders - .flatMap { it.listFiles().orEmpty().toList() } + return getBaseDirectory() + ?.findFile(name, true) + ?.takeIf { it.isDirectory } + ?.listFiles().orEmpty().toList() } } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt index d9ce323d5..6bade530b 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/metadata/EpubFile.kt @@ -8,37 +8,25 @@ import java.text.SimpleDateFormat import java.util.Locale /** - * Fills manga metadata using this epub file's metadata. + * Fills manga and chapter metadata using this epub file's metadata. */ -fun EpubFile.fillMangaMetadata(manga: SManga) { - val ref = getPackageHref() - val doc = getPackageDocument(ref) - - val creator = doc.getElementsByTag("dc:creator").first() - val description = doc.getElementsByTag("dc:description").first() - - manga.author = creator?.text() - manga.description = description?.text() -} - -/** - * Fills chapter metadata using this epub file's metadata. - */ -fun EpubFile.fillChapterMetadata(chapter: SChapter) { +fun EpubFile.fillMetadata(manga: SManga, chapter: SChapter) { val ref = getPackageHref() val doc = getPackageDocument(ref) val title = doc.getElementsByTag("dc:title").first() val publisher = doc.getElementsByTag("dc:publisher").first() val creator = doc.getElementsByTag("dc:creator").first() + val description = doc.getElementsByTag("dc:description").first() var date = doc.getElementsByTag("dc:date").first() if (date == null) { date = doc.select("meta[property=dcterms:modified]").first() } - if (title != null) { - chapter.name = title.text() - } + creator?.text()?.let { manga.author = it } + description?.text()?.let { manga.description = it } + + title?.text()?.let { chapter.name = it } if (publisher != null) { chapter.scanlator = publisher.text() From d85a76484c3ad48c6c6dbaa7de1c7f3cf270adf1 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 27 Nov 2023 09:06:38 -0500 Subject: [PATCH 17/22] Revert "Show copied to clipboard toast on Samsung devices even if Android 13+" This reverts commit bf524595e27e5a42aceb6df0dae2817cefac63c6. Apparently it shows a toast, but I don't see it? --- .../java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 43bbd9046..14f637366 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -46,7 +46,7 @@ fun Context.copyToClipboard(label: String, content: String) { // Android 13 and higher shows a visual confirmation of copied contents // https://developer.android.com/about/versions/13/features/copy-paste - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || DeviceUtil.isSamsung) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { toast(stringResource(MR.strings.copied_to_clipboard, content.truncateCenter(50))) } } catch (e: Throwable) { From a74a689c9048cc67f4854678fbfefa361631a5e7 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 27 Nov 2023 22:21:40 -0500 Subject: [PATCH 18/22] Update UniFile Which has more correct nullability for some methods and case insensitivity for listFiles where possible. --- .../more/settings/screen/SettingsDataScreen.kt | 5 +++-- .../eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt | 2 +- .../eu/kanade/tachiyomi/data/backup/BackupCreator.kt | 4 ++-- .../kanade/tachiyomi/data/download/DownloadProvider.kt | 4 ++-- .../eu/kanade/tachiyomi/data/download/Downloader.kt | 10 +++++----- .../kanade/tachiyomi/util/system/ContextExtensions.kt | 2 +- .../main/java/tachiyomi/core/util/system/ImageUtil.kt | 2 +- gradle/libs.versions.toml | 2 +- .../tachiyomi/source/local/image/LocalCoverManager.kt | 2 +- 9 files changed, 17 insertions(+), 16 deletions(-) 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 341cf2bb5..8d84859a3 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 @@ -99,8 +99,9 @@ object SettingsDataScreen : SearchableSettings { context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - storageDirPref.set(file.uri.toString()) + UniFile.fromUri(context, uri)?.let { + storageDirPref.set(it.uri.toString()) + } Injekt.get().invalidateCache() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index 825d66c93..bc37b8c75 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -58,7 +58,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete if (isAutoBackup) { backupPreferences.lastAutoBackupTimestamp().set(Date().time) } else { - notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())) + notifier.showBackupComplete(UniFile.fromUri(context, location.toUri())!!) } Result.success() } catch (e: Exception) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt index a8cd3b773..de907c91e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -95,14 +95,14 @@ class BackupCreator( val dir = UniFile.fromUri(context, uri) // Delete older backups - dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } + dir?.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } .orEmpty() .sortedByDescending { it.name } .drop(MAX_AUTO_BACKUPS - 1) .forEach { it.delete() } // Create new file to place backup - dir.createFile(Backup.getFilename()) + dir?.createFile(Backup.getFilename()) } else { UniFile.fromUri(context, uri) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 77dbfd264..218661aa0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -37,8 +37,8 @@ class DownloadProvider( internal fun getMangaDir(mangaTitle: String, source: Source): UniFile { try { return downloadsDir!! - .createDirectory(getSourceDirName(source)) - .createDirectory(getMangaDirName(mangaTitle)) + .createDirectory(getSourceDirName(source))!! + .createDirectory(getMangaDirName(mangaTitle))!! } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Invalid download directory" } throw Exception(context.stringResource(MR.strings.invalid_location, downloadsDir ?: "")) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 74a90b3ba..df45585ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -335,7 +335,7 @@ class Downloader( } val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator) - val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX) + val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!! try { // If the page list already exists, start from the file @@ -480,7 +480,7 @@ class Downloader( page.progress = 0 return flow { val response = source.getImage(page) - val file = tmpDir.createFile("$filename.tmp") + val file = tmpDir.createFile("$filename.tmp")!! try { response.body.source().saveTo(file.openOutputStream()) val extension = getImageExtension(response, file) @@ -512,7 +512,7 @@ class Downloader( * @param filename the filename of the image. */ private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile { - val tmpFile = tmpDir.createFile("$filename.tmp") + val tmpFile = tmpDir.createFile("$filename.tmp")!! cacheFile.inputStream().use { input -> tmpFile.openOutputStream().use { output -> input.copyTo(output) @@ -603,7 +603,7 @@ class Downloader( dirname: String, tmpDir: UniFile, ) { - val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX") + val zip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!! ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut -> zipOut.setMethod(ZipEntry.STORED) @@ -643,7 +643,7 @@ class Downloader( val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories) // Remove the old file dir.findFile(COMIC_INFO_FILE)?.delete() - dir.createFile(COMIC_INFO_FILE).openOutputStream().use { + dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use { val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) it.write(comicInfoString.toByteArray()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 14f637366..5f91bfbac 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -165,7 +165,7 @@ fun Context.createReaderThemeContext(): Context { * @return document size of [uri] or null if size can't be obtained */ fun Context.getUriSize(uri: Uri): Long? { - return UniFile.fromUri(this, uri).length().takeIf { it >= 0 } + return UniFile.fromUri(this, uri)?.length()?.takeIf { it >= 0 } } /** diff --git a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt index f72a963b7..0aa7f9f59 100644 --- a/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt +++ b/core/src/main/java/tachiyomi/core/util/system/ImageUtil.kt @@ -245,7 +245,7 @@ object ImageUtil { // Remove pre-existing split if exists (this split shouldn't exist under normal circumstances) tmpDir.findFile(splitImageName)?.delete() - val splitFile = tmpDir.createFile(splitImageName) + val splitFile = tmpDir.createFile(splitImageName)!! val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7c72d2304..889ac693d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2" jsoup = "org.jsoup:jsoup:1.16.2" disklrucache = "com.jakewharton:disklrucache:2.0.2" -unifile = "com.github.tachiyomiorg:unifile:17bec43" +unifile = "com.github.tachiyomiorg:unifile:7c257e1c64" junrar = "com.github.junrar:junrar:7.5.5" sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt index d793357f5..0f5a4c343 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/image/LocalCoverManager.kt @@ -34,7 +34,7 @@ actual class LocalCoverManager( return null } - val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME) + val targetFile = find(manga.url) ?: directory.createFile(DEFAULT_COVER_NAME)!! inputStream.use { input -> targetFile.openOutputStream().use { output -> From e41668862f6d3362d7de030692d052ae653364e3 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 28 Nov 2023 08:59:34 -0500 Subject: [PATCH 19/22] Ignore casing when looking for some files/folders --- .../presentation/more/settings/screen/SettingsDataScreen.kt | 1 - .../eu/kanade/tachiyomi/data/download/DownloadProvider.kt | 2 +- .../main/java/eu/kanade/tachiyomi/data/download/Downloader.kt | 2 +- .../androidMain/kotlin/tachiyomi/source/local/LocalSource.kt | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) 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 8d84859a3..26f0b9b04 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 @@ -27,7 +27,6 @@ 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.res.stringResource import androidx.core.net.toUri import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index 218661aa0..e29628878 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -91,7 +91,7 @@ class DownloadProvider( val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList() return mangaDir to chapters.mapNotNull { chapter -> getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence() - .mapNotNull { mangaDir.findFile(it) } + .mapNotNull { mangaDir.findFile(it, true) } .firstOrNull() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index df45585ea..5e58d4f6a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -642,7 +642,7 @@ class Downloader( val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() } val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories) // Remove the old file - dir.findFile(COMIC_INFO_FILE)?.delete() + dir.findFile(COMIC_INFO_FILE, true)?.delete() dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use { val comicInfoString = xml.encodeToString(ComicInfo.serializer(), comicInfo) it.write(comicInfoString.toByteArray()) diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index 3282137ea..b2fa6731c 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -303,8 +303,8 @@ actual class LocalSource( try { val (mangaDirName, chapterName) = chapter.url.split('/', limit = 2) return fileSystem.getBaseDirectory() - ?.findFile(mangaDirName) - ?.findFile(chapterName) + ?.findFile(mangaDirName, true) + ?.findFile(chapterName, true) ?.let(Format.Companion::valueOf) ?: throw Exception(context.stringResource(MR.strings.chapter_not_found)) } catch (e: Format.UnknownFormatException) { From 4fcdde4913df28bbd678ae1be4a2971ed77179d3 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 28 Nov 2023 08:59:45 -0500 Subject: [PATCH 20/22] Remove storage permissions Requires adjusting some file reading to first copy to a temporary file in cache that we have permissions to read from. This is only applicable for things like ZIP files where we need an actual File rather than just some Android content URI shenanigans. --- app/build.gradle.kts | 1 - app/src/main/AndroidManifest.xml | 4 --- .../settings/screen/SettingsDataScreen.kt | 3 -- .../permissions/PermissionRequestHelper.kt | 20 ------------- .../kanade/tachiyomi/ui/browse/BrowseTab.kt | 4 --- .../ui/reader/loader/ChapterLoader.kt | 7 +++-- .../ui/reader/loader/DownloadPageLoader.kt | 3 +- .../ui/reader/loader/EpubPageLoader.kt | 4 +-- .../ui/reader/loader/RarPageLoader.kt | 7 ++--- .../ui/reader/loader/ZipPageLoader.kt | 9 +++--- .../kanade/tachiyomi/util/storage/EpubFile.kt | 6 ++-- .../core/storage/UniFileExtensions.kt | 28 ++++++++++++++++++- gradle/compose.versions.toml | 1 - .../tachiyomi/source/local/LocalSource.kt | 14 +++++----- 14 files changed, 51 insertions(+), 60 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4bba73d1d..0f0917a05 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,6 @@ dependencies { implementation(compose.ui.tooling.preview) implementation(compose.ui.util) implementation(compose.accompanist.webview) - implementation(compose.accompanist.permissions) implementation(compose.accompanist.systemuicontroller) lintChecks(compose.lintchecks) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 442a123f3..5c903960f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,9 +7,6 @@ - - - @@ -39,7 +36,6 @@ android:largeHeap="true" android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config" - android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Tachiyomi"> 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 26f0b9b04..b1bd8879c 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 @@ -35,7 +35,6 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding -import eu.kanade.presentation.permissions.PermissionRequestHelper import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator @@ -71,8 +70,6 @@ object SettingsDataScreen : SearchableSettings { val backupPreferences = Injekt.get() val storagePreferences = Injekt.get() - PermissionRequestHelper.requestStoragePermission() - return listOf( getStorageLocationPref(storagePreferences = storagePreferences), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), diff --git a/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt b/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt deleted file mode 100644 index 7ce28f9da..000000000 --- a/app/src/main/java/eu/kanade/presentation/permissions/PermissionRequestHelper.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.presentation.permissions - -import android.Manifest -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import com.google.accompanist.permissions.rememberPermissionState - -/** - * Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission - */ -object PermissionRequestHelper { - - @Composable - fun requestStoragePermission() { - val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) - LaunchedEffect(Unit) { - permissionState.launchPermissionRequest() - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt index a05cba620..2ce4685bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/BrowseTab.kt @@ -13,7 +13,6 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.TabOptions import eu.kanade.presentation.components.TabbedScreen -import eu.kanade.presentation.permissions.PermissionRequestHelper import eu.kanade.presentation.util.Tab import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel @@ -66,9 +65,6 @@ data class BrowseTab( onChangeSearchQuery = extensionsScreenModel::search, ) - // For local source - PermissionRequestHelper.requestStoragePermission() - LaunchedEffect(Unit) { (context as? MainActivity)?.ready = true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 4989b4704..6a31ed029 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import tachiyomi.core.i18n.stringResource +import tachiyomi.core.storage.toTempFile import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.manga.model.Manga @@ -88,13 +89,13 @@ class ChapterLoader( source is LocalSource -> source.getFormat(chapter.chapter).let { format -> when (format) { is Format.Directory -> DirectoryPageLoader(format.file) - is Format.Zip -> ZipPageLoader(format.file) + is Format.Zip -> ZipPageLoader(format.file.toTempFile(context)) is Format.Rar -> try { - RarPageLoader(format.file) + RarPageLoader(format.file.toTempFile(context)) } catch (e: UnsupportedRarV5Exception) { error(context.stringResource(MR.strings.loader_rar5_error)) } - is Format.Epub -> EpubPageLoader(format.file) + is Format.Epub -> EpubPageLoader(format.file.toTempFile(context)) } } source is HttpSource -> HttpPageLoader(chapter, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt index 4fb5adb6c..3d385551d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/DownloadPageLoader.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage +import tachiyomi.core.storage.toTempFile import tachiyomi.domain.manga.model.Manga import uy.kohesive.injekt.injectLazy @@ -46,7 +47,7 @@ internal class DownloadPageLoader( } private suspend fun getPagesFromArchive(chapterPath: UniFile): List { - val loader = ZipPageLoader(chapterPath).also { zipPageLoader = it } + val loader = ZipPageLoader(chapterPath.toTempFile(context)).also { zipPageLoader = it } return loader.getPages() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt index cd00e3756..324af51bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/EpubPageLoader.kt @@ -1,14 +1,14 @@ package eu.kanade.tachiyomi.ui.reader.loader -import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.storage.EpubFile +import java.io.File /** * Loader used to load a chapter from a .epub file. */ -internal class EpubPageLoader(file: UniFile) : PageLoader() { +internal class EpubPageLoader(file: File) : PageLoader() { private val epub = EpubFile(file) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt index 319179117..056319d4e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/RarPageLoader.kt @@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.loader import com.github.junrar.Archive import com.github.junrar.rarfile.FileHeader -import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.storage.toFile import tachiyomi.core.util.system.ImageUtil +import java.io.File import java.io.InputStream import java.io.PipedInputStream import java.io.PipedOutputStream @@ -15,9 +14,9 @@ import java.io.PipedOutputStream /** * Loader used to load a chapter from a .rar or .cbr file. */ -internal class RarPageLoader(file: UniFile) : PageLoader() { +internal class RarPageLoader(file: File) : PageLoader() { - private val rar = Archive(file.toFile()) + private val rar = Archive(file) override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index b63b32557..e04fe78e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -1,24 +1,23 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.os.Build -import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import tachiyomi.core.storage.toFile import tachiyomi.core.util.system.ImageUtil +import java.io.File import java.nio.charset.StandardCharsets import java.util.zip.ZipFile /** * Loader used to load a chapter from a .zip or .cbz file. */ -internal class ZipPageLoader(file: UniFile) : PageLoader() { +internal class ZipPageLoader(file: File) : PageLoader() { private val zip = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ZipFile(file.toFile(), StandardCharsets.ISO_8859_1) + ZipFile(file, StandardCharsets.ISO_8859_1) } else { - ZipFile(file.toFile()) + ZipFile(file) } override var isLocal: Boolean = true diff --git a/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt index 7650f65b5..a00ee69e7 100644 --- a/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/src/main/java/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,9 +1,7 @@ package eu.kanade.tachiyomi.util.storage -import com.hippo.unifile.UniFile import org.jsoup.Jsoup import org.jsoup.nodes.Document -import tachiyomi.core.storage.toFile import java.io.Closeable import java.io.File import java.io.InputStream @@ -13,12 +11,12 @@ import java.util.zip.ZipFile /** * Wrapper over ZipFile to load files in epub format. */ -class EpubFile(file: UniFile) : Closeable { +class EpubFile(file: File) : Closeable { /** * Zip file of this epub. */ - private val zip = ZipFile(file.toFile()) + private val zip = ZipFile(file) /** * Path separator used by this epub. diff --git a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt index 5343dfa3f..c5c2bbbc8 100644 --- a/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt +++ b/core/src/main/java/tachiyomi/core/storage/UniFileExtensions.kt @@ -1,6 +1,10 @@ package tachiyomi.core.storage +import android.content.Context +import android.os.Build +import android.os.FileUtils import com.hippo.unifile.UniFile +import java.io.BufferedOutputStream import java.io.File val UniFile.extension: String? @@ -9,4 +13,26 @@ val UniFile.extension: String? val UniFile.nameWithoutExtension: String? get() = name?.substringBeforeLast('.') -fun UniFile.toFile(): File? = filePath?.let { File(it) } +fun UniFile.toTempFile(context: Context): File { + val inputStream = context.contentResolver.openInputStream(uri)!! + val tempFile = File.createTempFile( + nameWithoutExtension.orEmpty(), + null, + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + FileUtils.copy(inputStream, tempFile.outputStream()) + } else { + BufferedOutputStream(tempFile.outputStream()).use { tmpOut -> + inputStream.use { input -> + val buffer = ByteArray(8192) + var count: Int + while (input.read(buffer).also { count = it } > 0) { + tmpOut.write(buffer, 0, count) + } + } + } + } + + return tempFile +} diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 39074cb0c..ce7b074bf 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -22,7 +22,6 @@ material-core = { module = "androidx.compose.material:material" } glance = "androidx.glance:glance-appwidget:1.0.0" accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanist" } -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } lintchecks = { module = "com.slack.lint.compose:compose-lint-checks", version = "1.2.0" } \ No newline at end of file diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index b2fa6731c..1305001f5 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -26,7 +26,7 @@ import tachiyomi.core.metadata.comicinfo.getComicInfo import tachiyomi.core.metadata.tachiyomi.MangaDetails import tachiyomi.core.storage.extension import tachiyomi.core.storage.nameWithoutExtension -import tachiyomi.core.storage.toFile +import tachiyomi.core.storage.toTempFile import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.system.ImageUtil import tachiyomi.core.util.system.logcat @@ -213,7 +213,7 @@ actual class LocalSource( for (chapter in chapterArchives) { when (Format.valueOf(chapter)) { is Format.Zip -> { - ZipFile(chapter.toFile()).use { zip: ZipFile -> + ZipFile(chapter.toTempFile(context)).use { zip: ZipFile -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -222,7 +222,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(chapter.toFile()).use { rar -> + JunrarArchive(chapter.toTempFile(context)).use { rar -> rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile -> rar.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folderPath) @@ -272,7 +272,7 @@ actual class LocalSource( val format = Format.valueOf(chapterFile) if (format is Format.Epub) { - EpubFile(format.file).use { epub -> + EpubFile(format.file.toTempFile(context)).use { epub -> epub.fillMetadata(manga, this) } } @@ -331,7 +331,7 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Zip -> { - ZipFile(format.file.toFile()).use { zip -> + ZipFile(format.file.toTempFile(context)).use { zip -> val entry = zip.entries().toList() .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } @@ -340,7 +340,7 @@ actual class LocalSource( } } is Format.Rar -> { - JunrarArchive(format.file.toFile()).use { archive -> + JunrarArchive(format.file.toTempFile(context)).use { archive -> val entry = archive.fileHeaders .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } @@ -349,7 +349,7 @@ actual class LocalSource( } } is Format.Epub -> { - EpubFile(format.file).use { epub -> + EpubFile(format.file.toTempFile(context)).use { epub -> val entry = epub.getImagesFromPages() .firstOrNull() ?.let { epub.getEntry(it) } From e22eebfd02e28eb260273d987d60c216aa4a2100 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 28 Nov 2023 23:02:33 -0500 Subject: [PATCH 21/22] Target SDK 30 Need to convert some services into WorkManager jobs before going to 31 and higher. --- .../ui/base/delegate/SecureActivityDelegate.kt | 3 ++- .../kanade/tachiyomi/ui/reader/ReaderActivity.kt | 5 +++-- .../kanade/tachiyomi/ui/webview/WebViewActivity.kt | 5 +++-- .../tachiyomi/util/system/ActivityExtensions.kt | 14 ++++++++++++++ buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt index 02589cec9..dc017c328 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.ui.security.UnlockActivity import eu.kanade.tachiyomi.util.system.AuthenticatorUtil import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported +import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat import eu.kanade.tachiyomi.util.view.setSecureScreen import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -106,7 +107,7 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser if (activity.isAuthenticationSupported()) { if (!SecureActivityDelegate.requireUnlock) return activity.startActivity(Intent(activity, UnlockActivity::class.java)) - activity.overridePendingTransition(0, 0) + activity.overridePendingTransitionCompat(0, 0) } else { securityPreferences.useAuthenticator().set(false) } 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 f5b647e85..7900b377c 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 @@ -70,6 +70,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.system.hasDisplayCutout import eu.kanade.tachiyomi.util.system.isNightMode +import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent @@ -138,7 +139,7 @@ class ReaderActivity : BaseActivity() { */ override fun onCreate(savedInstanceState: Bundle?) { registerSecureActivity(this) - overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) + overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) super.onCreate(savedInstanceState) @@ -269,7 +270,7 @@ class ReaderActivity : BaseActivity() { override fun finish() { viewModel.onActivityFinish() super.finish() - overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) + overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) } override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 61d09dc79..5afd8364c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.util.system.overridePendingTransitionCompat import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.setComposeContent @@ -35,7 +36,7 @@ class WebViewActivity : BaseActivity() { } override fun onCreate(savedInstanceState: Bundle?) { - overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) + overridePendingTransitionCompat(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) super.onCreate(savedInstanceState) if (!WebViewUtil.supportsWebView(this)) { @@ -77,7 +78,7 @@ class WebViewActivity : BaseActivity() { override fun finish() { super.finish() - overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) + overridePendingTransitionCompat(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit) } private fun shareWebpage(url: String) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt new file mode 100644 index 000000000..c145f0e28 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ActivityExtensions.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.util.system + +import android.app.Activity +import android.os.Build +import androidx.annotation.AnimRes + +fun Activity.overridePendingTransitionCompat(@AnimRes enterAnim: Int, @AnimRes exitAnim: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, enterAnim, exitAnim) + } else { + @Suppress("DEPRECATION") + overridePendingTransition(enterAnim, exitAnim) + } +} diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index f63c07729..2127d0657 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,6 +1,6 @@ object AndroidConfig { const val compileSdk = 34 const val minSdk = 23 - const val targetSdk = 29 + const val targetSdk = 30 const val ndk = "22.1.7171670" } From 8ff2c01bf28d9d645796b7e616d3ab5c6f85a86c Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Wed, 29 Nov 2023 22:43:21 +0700 Subject: [PATCH 22/22] HomeScreen: Add static key for TabNavigator (#10191) Fixes incorrect tab selection after process death --- app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt index 236e80f83..13319c948 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt @@ -66,6 +66,7 @@ object HomeScreen : Screen() { private val showBottomNavEvent = Channel() private const val TabFadeDuration = 200 + private const val TabNavigatorKey = "HomeTabs" private val tabs = listOf( LibraryTab, @@ -80,6 +81,7 @@ object HomeScreen : Screen() { val navigator = LocalNavigator.currentOrThrow TabNavigator( tab = LibraryTab, + key = TabNavigatorKey, ) { tabNavigator -> // Provide usable navigator to content screen CompositionLocalProvider(LocalNavigator provides navigator) {