Add unified storage location setting

Currently only using it as a replacement for the downloads location.
This commit is contained in:
arkon 2023-10-26 13:43:42 -04:00
parent e3b70ca08d
commit 695813ef7d
10 changed files with 99 additions and 106 deletions

View file

@ -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<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>()
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

View file

@ -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<DownloadPreferences>() }
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<String> {
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<String, String> {
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,

View file

@ -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) }

View file

@ -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)
}
/**

View file

@ -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.
*

View file

@ -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<AndroidDownloadFolderProvider>(),
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<AndroidStorageFolderProvider>(),
preferenceStore = get(),
)
}
addSingletonFactory {
UiPreferences(get())
}

View file

@ -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",
)
}

View file

@ -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,

View file

@ -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"
}
}

View file

@ -426,13 +426,11 @@
<string name="pref_lowest">Lowest</string>
<!-- Downloads section -->
<string name="pref_download_directory">Download location</string>
<string name="pref_category_delete_chapters">Delete chapters</string>
<string name="pref_remove_after_marked_as_read">After manually marked as read</string>
<string name="pref_remove_after_read">After reading automatically delete</string>
<string name="pref_remove_bookmarked_chapters">Allow deleting bookmarked chapters</string>
<string name="pref_remove_exclude_categories">Excluded categories</string>
<string name="custom_dir">Custom location</string>
<string name="invalid_location">Invalid location: %s</string>
<string name="disabled">Disabled</string>
<string name="last_read_chapter">Last read chapter</string>
@ -465,6 +463,8 @@
<string name="pref_hide_in_library_items">Hide entries already in library</string>
<!-- Data and storage section -->
<string name="pref_storage_location">Storage location</string>
<string name="pref_storage_location_info">Used for automatic backups, chapter downloads, and local source.</string>
<string name="pref_create_backup">Create backup</string>
<string name="pref_create_backup_summ">Can be used to restore current library</string>
<string name="pref_restore_backup">Restore backup</string>