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 package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -26,8 +27,11 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
import com.hippo.unifile.UniFile
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget 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.core.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
@ -64,15 +69,55 @@ object SettingsDataScreen : SearchableSettings {
@Composable @Composable
override fun getPreferences(): List<Preference> { override fun getPreferences(): List<Preference> {
val backupPreferences = Injekt.get<BackupPreferences>() val backupPreferences = Injekt.get<BackupPreferences>()
val storagePreferences = Injekt.get<StoragePreferences>()
PermissionRequestHelper.requestStoragePermission() PermissionRequestHelper.requestStoragePermission()
return listOf( return listOf(
getStorageLocationPref(storagePreferences = storagePreferences),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)),
getBackupAndRestoreGroup(backupPreferences = backupPreferences), getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(), 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 @Composable
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val context = LocalContext.current val context = LocalContext.current

View file

@ -1,9 +1,5 @@
package eu.kanade.presentation.more.settings.screen 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.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -12,10 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastMap 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.category.visualName
import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.widget.TriStateListDialog 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 tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
object SettingsDownloadScreen : SearchableSettings { object SettingsDownloadScreen : SearchableSettings {
@ -44,7 +36,6 @@ object SettingsDownloadScreen : SearchableSettings {
val downloadPreferences = remember { Injekt.get<DownloadPreferences>() } val downloadPreferences = remember { Injekt.get<DownloadPreferences>() }
return listOf( return listOf(
getDownloadLocationPreference(downloadPreferences = downloadPreferences),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = downloadPreferences.downloadOnlyOverWifi(), pref = downloadPreferences.downloadOnlyOverWifi(),
title = stringResource(MR.strings.connected_to_wifi), 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 @Composable
private fun getDeleteChaptersGroup( private fun getDeleteChaptersGroup(
downloadPreferences: DownloadPreferences, downloadPreferences: DownloadPreferences,

View file

@ -92,8 +92,8 @@ class BackupCreator(
file = ( file = (
if (isAutoBackup) { if (isAutoBackup) {
// Get dir of file and create // Get dir of file and create
var dir = UniFile.fromUri(context, uri) val dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic") .createDirectory("automatic")
// Delete older backups // Delete older backups
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) } 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.lang.launchNonCancellable
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -64,7 +64,7 @@ class DownloadCache(
private val provider: DownloadProvider = Injekt.get(), private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = 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) private val scope = CoroutineScope(Dispatchers.IO)
@ -98,7 +98,7 @@ class DownloadCache(
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
init { init {
downloadPreferences.downloadsDirectory().changes() storagePreferences.baseStorageDirectory().changes()
.onEach { .onEach {
rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
invalidateCache() invalidateCache()
@ -297,8 +297,8 @@ class DownloadCache(
* Returns the downloads directory from the user's preferences. * Returns the downloads directory from the user's preferences.
*/ */
private fun getDirectoryFromPreference(): UniFile { private fun getDirectoryFromPreference(): UniFile {
val dir = downloadPreferences.downloadsDirectory().get() return UniFile.fromUri(context, storagePreferences.baseStorageDirectory().get().toUri())
return UniFile.fromUri(context, dir.toUri()) .createDirectory(StoragePreferences.DOWNLOADS_DIR)
} }
/** /**

View file

@ -12,8 +12,8 @@ import logcat.LogPriority
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -26,7 +26,7 @@ import uy.kohesive.injekt.api.get
*/ */
class DownloadProvider( class DownloadProvider(
private val context: Context, private val context: Context,
downloadPreferences: DownloadPreferences = Injekt.get(), private val storagePreferences: StoragePreferences = Injekt.get(),
) { ) {
private val scope = MainScope() private val scope = MainScope()
@ -34,18 +34,24 @@ class DownloadProvider(
/** /**
* The root directory for downloads. * The root directory for downloads.
*/ */
private var downloadsDir = downloadPreferences.downloadsDirectory().get().let { private var downloadsDir = setDownloadsLocation()
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
init { init {
downloadPreferences.downloadsDirectory().changes() storagePreferences.baseStorageDirectory().changes()
.onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) } .onEach { downloadsDir = setDownloadsLocation() }
.launchIn(scope) .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. * 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.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.AndroidBackupFolderProvider import tachiyomi.core.provider.AndroidBackupFolderProvider
import tachiyomi.core.provider.AndroidDownloadFolderProvider import tachiyomi.core.provider.AndroidStorageFolderProvider
import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory import uy.kohesive.injekt.api.addSingletonFactory
@ -49,13 +50,7 @@ class PreferenceModule(val app: Application) : InjektModule {
TrackPreferences(get()) TrackPreferences(get())
} }
addSingletonFactory { addSingletonFactory {
AndroidDownloadFolderProvider(app) DownloadPreferences(get())
}
addSingletonFactory {
DownloadPreferences(
folderProvider = get<AndroidDownloadFolderProvider>(),
preferenceStore = get(),
)
} }
addSingletonFactory { addSingletonFactory {
AndroidBackupFolderProvider(app) AndroidBackupFolderProvider(app)
@ -66,6 +61,15 @@ class PreferenceModule(val app: Application) : InjektModule {
preferenceStore = get(), preferenceStore = get(),
) )
} }
addSingletonFactory {
AndroidStorageFolderProvider(app)
}
addSingletonFactory {
StoragePreferences(
folderProvider = get<AndroidStorageFolderProvider>(),
preferenceStore = get(),
)
}
addSingletonFactory { addSingletonFactory {
UiPreferences(get()) UiPreferences(get())
} }

View file

@ -7,15 +7,14 @@ import tachiyomi.core.i18n.stringResource
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import java.io.File import java.io.File
class AndroidDownloadFolderProvider( class AndroidStorageFolderProvider(
val context: Context, private val context: Context,
) : FolderProvider { ) : FolderProvider {
override fun directory(): File { override fun directory(): File {
return File( return File(
Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.getExternalStorageDirectory().absolutePath + File.separator +
context.stringResource(MR.strings.app_name), context.stringResource(MR.strings.app_name),
"downloads",
) )
} }

View file

@ -1,18 +1,11 @@
package tachiyomi.domain.download.service package tachiyomi.domain.download.service
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.provider.FolderProvider
class DownloadPreferences( class DownloadPreferences(
private val folderProvider: FolderProvider,
private val preferenceStore: PreferenceStore, private val preferenceStore: PreferenceStore,
) { ) {
fun downloadsDirectory() = preferenceStore.getString(
"download_directory",
folderProvider.path(),
)
fun downloadOnlyOverWifi() = preferenceStore.getBoolean( fun downloadOnlyOverWifi() = preferenceStore.getBoolean(
"pref_download_only_over_wifi_key", "pref_download_only_over_wifi_key",
true, 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> <string name="pref_lowest">Lowest</string>
<!-- Downloads section --> <!-- Downloads section -->
<string name="pref_download_directory">Download location</string>
<string name="pref_category_delete_chapters">Delete chapters</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_marked_as_read">After manually marked as read</string>
<string name="pref_remove_after_read">After reading automatically delete</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_bookmarked_chapters">Allow deleting bookmarked chapters</string>
<string name="pref_remove_exclude_categories">Excluded categories</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="invalid_location">Invalid location: %s</string>
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="last_read_chapter">Last read chapter</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> <string name="pref_hide_in_library_items">Hide entries already in library</string>
<!-- Data and storage section --> <!-- 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">Create backup</string>
<string name="pref_create_backup_summ">Can be used to restore current library</string> <string name="pref_create_backup_summ">Can be used to restore current library</string>
<string name="pref_restore_backup">Restore backup</string> <string name="pref_restore_backup">Restore backup</string>