package eu.kanade.presentation.more.settings.screen import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MultiChoiceSegmentedButtonRow import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember 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.platform.LocalUriHandler 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.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import logcat.LogPriority import tachiyomi.core.i18n.stringResource import tachiyomi.core.storage.displayablePath import tachiyomi.core.util.lang.launchNonCancellable 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 import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get object SettingsDataScreen : SearchableSettings { val restorePreferenceKeyString = MR.strings.label_backup const val HELP_URL = "https://tachiyomi.org/docs/faq/storage" @ReadOnlyComposable @Composable override fun getTitleRes() = MR.strings.label_data_storage @Composable override fun RowScope.AppBarAction() { val uriHandler = LocalUriHandler.current IconButton(onClick = { uriHandler.openUri(HELP_URL) }) { Icon( imageVector = Icons.AutoMirrored.Outlined.HelpOutline, contentDescription = stringResource(MR.strings.tracking_guide), ) } } @Composable override fun getPreferences(): List { val backupPreferences = Injekt.get() val storagePreferences = Injekt.get() return persistentListOf( getStorageLocationPref(storagePreferences = storagePreferences), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.pref_storage_location_info)), getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), ) } @Composable fun storageLocationPicker( storageDirPref: tachiyomi.core.preference.Preference, ): ManagedActivityResultLauncher { val context = LocalContext.current return 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) UniFile.fromUri(context, uri)?.let { storageDirPref.set(it.uri.toString()) } } } } @Composable fun storageLocationText( storageDirPref: tachiyomi.core.preference.Preference, ): String { val context = LocalContext.current val storageDir by storageDirPref.collectAsState() if (storageDir == storageDirPref.defaultValue()) { return stringResource(MR.strings.no_location_set) } return remember(storageDir) { val file = UniFile.fromUri(context, storageDir.toUri()) file?.displayablePath } ?: stringResource(MR.strings.invalid_location, storageDir) } @Composable private fun getStorageLocationPref( storagePreferences: StoragePreferences, ): Preference.PreferenceItem.TextPreference { val context = LocalContext.current val pickStorageLocation = storageLocationPicker(storagePreferences.baseStorageDirectory()) return Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.pref_storage_location), subtitle = storageLocationText(storagePreferences.baseStorageDirectory()), 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 val navigator = LocalNavigator.currentOrThrow val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState() val chooseBackup = rememberLauncherForActivityResult( object : ActivityResultContracts.GetContent() { override fun createIntent(context: Context, input: String): Intent { val intent = super.createIntent(context, input) return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup)) } }, ) { if (it == null) { context.toast(MR.strings.file_null_uri_error) return@rememberLauncherForActivityResult } navigator.push(RestoreBackupScreen(it.toString())) } return Preference.PreferenceGroup( title = stringResource(MR.strings.label_backup), preferenceItems = persistentListOf( // Manual actions Preference.PreferenceItem.CustomPreference( title = stringResource(restorePreferenceKeyString), ) { BasePreferenceWidget( subcomponent = { MultiChoiceSegmentedButtonRow( modifier = Modifier .fillMaxWidth() .padding(horizontal = PrefsHorizontalPadding), ) { SegmentedButton( checked = false, onCheckedChange = { navigator.push(CreateBackupScreen()) }, shape = SegmentedButtonDefaults.itemShape(0, 2), ) { Text(stringResource(MR.strings.pref_create_backup)) } SegmentedButton( checked = false, onCheckedChange = { if (!BackupRestoreJob.isRunning(context)) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { context.toast(MR.strings.restore_miui_warning) } // no need to catch because it's wrapped with a chooser chooseBackup.launch("*/*") } else { context.toast(MR.strings.restore_in_progress) } }, shape = SegmentedButtonDefaults.itemShape(1, 2), ) { Text(stringResource(MR.strings.pref_restore_backup)) } } }, ) }, // Automatic backups Preference.PreferenceItem.ListPreference( pref = backupPreferences.backupInterval(), title = stringResource(MR.strings.pref_backup_interval), entries = persistentMapOf( 0 to stringResource(MR.strings.off), 6 to stringResource(MR.strings.update_6hour), 12 to stringResource(MR.strings.update_12hour), 24 to stringResource(MR.strings.update_24hour), 48 to stringResource(MR.strings.update_48hour), 168 to stringResource(MR.strings.update_weekly), ), onValueChanged = { BackupCreateJob.setupTask(context, it) true }, ), Preference.PreferenceItem.InfoPreference( stringResource(MR.strings.backup_info) + "\n\n" + stringResource(MR.strings.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)), ), ), ) } @Composable private fun getDataGroup(): Preference.PreferenceGroup { val context = LocalContext.current val scope = rememberCoroutineScope() val libraryPreferences = remember { Injekt.get() } val chapterCache = remember { Injekt.get() } var cacheReadableSizeSema by remember { mutableIntStateOf(0) } val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } return Preference.PreferenceGroup( title = stringResource(MR.strings.pref_storage_usage), preferenceItems = persistentListOf( Preference.PreferenceItem.CustomPreference( title = stringResource(MR.strings.pref_storage_usage), ) { BasePreferenceWidget( subcomponent = { StorageInfo( modifier = Modifier.padding(horizontal = PrefsHorizontalPadding), ) }, ) }, Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.pref_clear_chapter_cache), subtitle = stringResource(MR.strings.used_cache, cacheReadableSize), onClick = { scope.launchNonCancellable { try { val deletedFiles = chapterCache.clear() withUIContext { context.toast(context.stringResource(MR.strings.cache_deleted, deletedFiles)) cacheReadableSizeSema++ } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) withUIContext { context.toast(MR.strings.cache_delete_error) } } } }, ), Preference.PreferenceItem.SwitchPreference( pref = libraryPreferences.autoClearChapterCache(), title = stringResource(MR.strings.pref_auto_clear_chapter_cache), ), ), ) } }