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 6723717ade..a1b55ab741 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 @@ -54,6 +58,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.domain.sync.SyncPreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource @@ -70,6 +75,7 @@ object SettingsDataScreen : SearchableSettings { @Composable override fun getPreferences(): List { val backupPreferences = Injekt.get() + val storagePreferences = Injekt.get() PermissionRequestHelper.requestStoragePermission() @@ -77,11 +83,50 @@ object SettingsDataScreen : SearchableSettings { val syncService by syncPreferences.syncService().collectAsState() return listOf( + getStorageLocationPref(storagePreferences = storagePreferences), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), + getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), ) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService) } + @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/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index 4226e77fca..c28d12f263 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,6 +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 uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date @@ -39,8 +40,9 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry() val backupPreferences = Injekt.get() + val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() - ?: backupPreferences.backupsDirectory().get().toUri() + ?: getAutomaticBackupLocation() val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults) try { @@ -73,6 +75,15 @@ 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 + } + } + companion object { fun isManualJobRunning(context: Context): Boolean { return context.workManager.isRunning(TAG_MANUAL) 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 549779dcf3..415900488f 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..eb444398ac 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,23 @@ 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) + 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 b836625902..ce5d7eedde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -11,11 +11,11 @@ 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.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 tachiyomi.domain.sync.SyncPreferences import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar @@ -50,20 +50,17 @@ class PreferenceModule(val app: Application) : InjektModule { TrackPreferences(get()) } addSingletonFactory { - AndroidDownloadFolderProvider(app) + DownloadPreferences(get()) } addSingletonFactory { - DownloadPreferences( - folderProvider = get(), - preferenceStore = get(), - ) + BackupPreferences(get()) } addSingletonFactory { - AndroidBackupFolderProvider(app) + AndroidStorageFolderProvider(app) } addSingletonFactory { - BackupPreferences( - folderProvider = get(), + StoragePreferences( + folderProvider = get(), preferenceStore = get(), ) } diff --git a/core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt b/core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt deleted file mode 100644 index f42fcfbcf0..0000000000 --- a/core/src/main/java/tachiyomi/core/provider/AndroidDownloadFolderProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package tachiyomi.core.provider - -import android.content.Context -import android.os.Environment -import androidx.core.net.toUri -import tachiyomi.core.i18n.stringResource -import tachiyomi.i18n.MR -import java.io.File - -class AndroidDownloadFolderProvider( - val context: Context, -) : FolderProvider { - - override fun directory(): File { - return File( - Environment.getExternalStorageDirectory().absolutePath + File.separator + - context.stringResource(MR.strings.app_name), - "downloads", - ) - } - - override fun path(): String { - return directory().toUri().toString() - } -} diff --git a/core/src/main/java/tachiyomi/core/provider/AndroidBackupFolderProvider.kt b/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt similarity index 91% rename from core/src/main/java/tachiyomi/core/provider/AndroidBackupFolderProvider.kt rename to core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt index afed0d95c7..fc2859868b 100644 --- a/core/src/main/java/tachiyomi/core/provider/AndroidBackupFolderProvider.kt +++ b/core/src/main/java/tachiyomi/core/provider/AndroidStorageFolderProvider.kt @@ -7,7 +7,7 @@ import tachiyomi.core.i18n.stringResource import tachiyomi.i18n.MR import java.io.File -class AndroidBackupFolderProvider( +class AndroidStorageFolderProvider( private val context: Context, ) : FolderProvider { @@ -15,7 +15,6 @@ class AndroidBackupFolderProvider( return File( Environment.getExternalStorageDirectory().absolutePath + File.separator + context.stringResource(MR.strings.app_name), - "backup", ) } diff --git a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt index 6144709de7..ba00faa184 100644 --- a/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt +++ b/domain/src/main/java/tachiyomi/domain/backup/service/BackupPreferences.kt @@ -2,15 +2,11 @@ package tachiyomi.domain.backup.service import tachiyomi.core.preference.Preference import tachiyomi.core.preference.PreferenceStore -import tachiyomi.core.provider.FolderProvider class BackupPreferences( - private val folderProvider: FolderProvider, private val preferenceStore: PreferenceStore, ) { - fun backupsDirectory() = preferenceStore.getString("backup_directory", folderProvider.path()) - fun backupInterval() = preferenceStore.getInt("backup_interval", 12) fun lastAutoBackupTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_auto_backup_timestamp"), 0L) 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..c336825b82 --- /dev/null +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StoragePreferences.kt @@ -0,0 +1,17 @@ +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 BACKUP_DIR = "backup" + 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 6aba4e8119..a2757f5f95 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -431,13 +431,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 @@ -470,11 +468,12 @@ 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 Restore library from backup file - Backup location Automatic backup frequency Create Backup created