From 695813ef7d922bc3d9ad5bccef8a466075caa57b Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 26 Oct 2023 13:43:42 -0400 Subject: [PATCH] Add unified storage location setting Currently only using it as a replacement for the downloads location. --- .../settings/screen/SettingsDataScreen.kt | 45 ++++++++++++ .../settings/screen/SettingsDownloadScreen.kt | 70 ------------------- .../tachiyomi/data/backup/BackupCreator.kt | 4 +- .../tachiyomi/data/download/DownloadCache.kt | 10 +-- .../data/download/DownloadProvider.kt | 24 ++++--- .../kanade/tachiyomi/di/PreferenceModule.kt | 20 +++--- ...der.kt => AndroidStorageFolderProvider.kt} | 5 +- .../download/service/DownloadPreferences.kt | 7 -- .../storage/service/StoragePreferences.kt | 16 +++++ .../commonMain/resources/MR/base/strings.xml | 4 +- 10 files changed, 99 insertions(+), 106 deletions(-) rename core/src/main/java/tachiyomi/core/provider/{AndroidDownloadFolderProvider.kt => AndroidStorageFolderProvider.kt} (86%) create mode 100644 domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.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 6727743228..461ec4eebd 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 @@ -1,5 +1,6 @@ package eu.kanade.presentation.more.settings.screen +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri @@ -26,8 +27,11 @@ 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 +import com.hippo.unifile.UniFile import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget @@ -49,6 +53,7 @@ import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState @@ -64,15 +69,55 @@ object SettingsDataScreen : SearchableSettings { @Composable override fun getPreferences(): List { 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)), + getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), ) } + @Composable + private fun getStorageLocationPref( + storagePreferences: StoragePreferences, + ): Preference.PreferenceItem.TextPreference { + val context = LocalContext.current + val storageDirPref = storagePreferences.baseStorageDirectory() + val storageDir by storageDirPref.collectAsState() + val pickStorageLocation = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocumentTree(), + ) { uri -> + if (uri != null) { + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + context.contentResolver.takePersistableUriPermission(uri, flags) + + val file = UniFile.fromUri(context, uri) + storageDirPref.set(file.uri.toString()) + } + } + + return Preference.PreferenceItem.TextPreference( + title = stringResource(MR.strings.pref_storage_location), + subtitle = remember(storageDir) { + (UniFile.fromUri(context, storageDir.toUri())?.filePath) + } ?: stringResource(MR.strings.invalid_location, storageDir), + onClick = { + try { + pickStorageLocation.launch(null) + } catch (e: ActivityNotFoundException) { + context.toast(MR.strings.file_picker_error) + } + }, + ) + } + @Composable private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { val context = LocalContext.current diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt index 9fb1e829b7..c9dd746f1e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDownloadScreen.kt @@ -1,9 +1,5 @@ package eu.kanade.presentation.more.settings.screen -import android.content.Intent -import android.os.Environment -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.collectAsState @@ -12,10 +8,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.util.fastMap -import androidx.core.net.toUri -import com.hippo.unifile.UniFile import eu.kanade.presentation.category.visualName import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.widget.TriStateListDialog @@ -29,7 +22,6 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.File object SettingsDownloadScreen : SearchableSettings { @@ -44,7 +36,6 @@ object SettingsDownloadScreen : SearchableSettings { val downloadPreferences = remember { Injekt.get() } return listOf( - getDownloadLocationPreference(downloadPreferences = downloadPreferences), Preference.PreferenceItem.SwitchPreference( pref = downloadPreferences.downloadOnlyOverWifi(), title = stringResource(MR.strings.connected_to_wifi), @@ -70,67 +61,6 @@ object SettingsDownloadScreen : SearchableSettings { ) } - @Composable - private fun getDownloadLocationPreference( - downloadPreferences: DownloadPreferences, - ): Preference.PreferenceItem.ListPreference { - val context = LocalContext.current - val currentDirPref = downloadPreferences.downloadsDirectory() - val currentDir by currentDirPref.collectAsState() - - val pickLocation = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocumentTree(), - ) { uri -> - if (uri != null) { - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - context.contentResolver.takePersistableUriPermission(uri, flags) - - val file = UniFile.fromUri(context, uri) - currentDirPref.set(file.uri.toString()) - } - } - - val defaultDirPair = rememberDefaultDownloadDir() - val customDirEntryKey = currentDir.takeIf { it != defaultDirPair.first } ?: "custom" - - return Preference.PreferenceItem.ListPreference( - pref = currentDirPref, - title = stringResource(MR.strings.pref_download_directory), - subtitleProvider = { value, _ -> - remember(value) { - UniFile.fromUri(context, value.toUri())?.filePath - } ?: stringResource(MR.strings.invalid_location, value) - }, - entries = mapOf( - defaultDirPair, - customDirEntryKey to stringResource(MR.strings.custom_dir), - ), - onValueChanged = { - val default = it == defaultDirPair.first - if (!default) { - pickLocation.launch(null) - } - default // Don't update when non-default chosen - }, - ) - } - - @Composable - private fun rememberDefaultDownloadDir(): Pair { - val appName = stringResource(MR.strings.app_name) - return remember { - val file = UniFile.fromFile( - File( - "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}$appName", - "downloads", - ), - )!! - file.uri.toString() to file.filePath!! - } - } - @Composable private fun getDeleteChaptersGroup( downloadPreferences: DownloadPreferences, 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 2aedf41dcd..619e30e01e 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 @@ -92,8 +92,8 @@ class BackupCreator( file = ( if (isAutoBackup) { // Get dir of file and create - var dir = UniFile.fromUri(context, uri) - dir = dir.createDirectory("automatic") + val dir = UniFile.fromUri(context, uri) + .createDirectory("automatic") // Delete older backups dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } 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 ff11da580f..407d2d505c 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 @@ -44,9 +44,9 @@ import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.source.service.SourceManager +import tachiyomi.domain.storage.service.StoragePreferences import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -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 downloadPreferences: DownloadPreferences = Injekt.get(), + private val storagePreferences: StoragePreferences = Injekt.get(), ) { private val scope = CoroutineScope(Dispatchers.IO) @@ -98,7 +98,7 @@ class DownloadCache( private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) init { - downloadPreferences.downloadsDirectory().changes() + storagePreferences.baseStorageDirectory().changes() .onEach { rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) invalidateCache() @@ -297,8 +297,8 @@ class DownloadCache( * Returns the downloads directory from the user's preferences. */ private fun getDirectoryFromPreference(): UniFile { - val dir = downloadPreferences.downloadsDirectory().get() - return UniFile.fromUri(context, dir.toUri()) + return UniFile.fromUri(context, storagePreferences.baseStorageDirectory().get().toUri()) + .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 cdf86836a4..cbb4d9dd35 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 @@ -12,8 +12,8 @@ import logcat.LogPriority import tachiyomi.core.i18n.stringResource import tachiyomi.core.util.system.logcat import tachiyomi.domain.chapter.model.Chapter -import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.manga.model.Manga +import tachiyomi.domain.storage.service.StoragePreferences import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -26,7 +26,7 @@ import uy.kohesive.injekt.api.get */ class DownloadProvider( private val context: Context, - downloadPreferences: DownloadPreferences = Injekt.get(), + private val storagePreferences: StoragePreferences = Injekt.get(), ) { private val scope = MainScope() @@ -34,18 +34,24 @@ class DownloadProvider( /** * The root directory for downloads. */ - private var downloadsDir = downloadPreferences.downloadsDirectory().get().let { - val dir = UniFile.fromUri(context, it.toUri()) - DiskUtil.createNoMediaFile(dir, context) - dir - } + private var downloadsDir = setDownloadsLocation() init { - downloadPreferences.downloadsDirectory().changes() - .onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) } + 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) + logcat { "downloadsDir: ${dir.filePath}" } + dir + } + } + /** * Returns the download directory for a manga. For internal use only. * 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 c7051cd474..48dfc91a95 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -12,10 +12,11 @@ import eu.kanade.tachiyomi.util.system.isDevFlavor import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.provider.AndroidBackupFolderProvider -import tachiyomi.core.provider.AndroidDownloadFolderProvider +import tachiyomi.core.provider.AndroidStorageFolderProvider import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.library.service.LibraryPreferences +import tachiyomi.domain.storage.service.StoragePreferences import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingletonFactory @@ -49,13 +50,7 @@ class PreferenceModule(val app: Application) : InjektModule { TrackPreferences(get()) } addSingletonFactory { - AndroidDownloadFolderProvider(app) - } - addSingletonFactory { - DownloadPreferences( - folderProvider = get(), - preferenceStore = get(), - ) + DownloadPreferences(get()) } addSingletonFactory { AndroidBackupFolderProvider(app) @@ -66,6 +61,15 @@ class PreferenceModule(val app: Application) : InjektModule { preferenceStore = get(), ) } + addSingletonFactory { + AndroidStorageFolderProvider(app) + } + addSingletonFactory { + StoragePreferences( + folderProvider = get(), + preferenceStore = get(), + ) + } addSingletonFactory { UiPreferences(get()) } diff --git a/core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt b/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt similarity index 86% rename from core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt rename to core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt index f42fcfbcf0..fc2859868b 100644 --- a/core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt +++ b/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt @@ -7,15 +7,14 @@ import tachiyomi.core.i18n.stringResource import tachiyomi.i18n.MR import java.io.File -class AndroidDownloadFolderProvider( - val context: Context, +class AndroidStorageFolderProvider( + private val context: Context, ) : FolderProvider { override fun directory(): File { return File( Environment.getExternalStorageDirectory().absolutePath + File.separator + context.stringResource(MR.strings.app_name), - "downloads", ) } diff --git a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt index 51a07dc8e5..84dfaecfeb 100644 --- a/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/download/service/DownloadPreferences.kt @@ -1,18 +1,11 @@ package tachiyomi.domain.download.service import tachiyomi.core.preference.PreferenceStore -import tachiyomi.core.provider.FolderProvider class DownloadPreferences( - private val folderProvider: FolderProvider, private val preferenceStore: PreferenceStore, ) { - fun downloadsDirectory() = preferenceStore.getString( - "download_directory", - folderProvider.path(), - ) - fun downloadOnlyOverWifi() = preferenceStore.getBoolean( "pref_download_only_over_wifi_key", true, diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt new file mode 100644 index 0000000000..1746a37238 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -0,0 +1,16 @@ +package tachiyomi.domain.storage.service + +import tachiyomi.core.preference.PreferenceStore +import tachiyomi.core.provider.FolderProvider + +class StoragePreferences( + private val folderProvider: FolderProvider, + private val preferenceStore: PreferenceStore, +) { + + fun baseStorageDirectory() = preferenceStore.getString("storage_dir", folderProvider.path()) + + companion object { + const val DOWNLOADS_DIR = "downloads" + } +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index c423f80405..1074a85572 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -426,13 +426,11 @@ Lowest - Download location Delete chapters After manually marked as read After reading automatically delete Allow deleting bookmarked chapters Excluded categories - Custom location Invalid location: %s Disabled Last read chapter @@ -465,6 +463,8 @@ Hide entries already in library + Storage location + Used for automatic backups, chapter downloads, and local source. Create backup Can be used to restore current library Restore backup