From 9d0cefa11f434f94df8811594c09d6560a14ce52 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 10 Jun 2024 12:23:53 +0700 Subject: [PATCH] feat: Composable Data and storage Settings --- app/build.gradle.kts | 3 + .../component/preference/Preference.kt | 4 +- .../settings/ComposableSettings.kt | 8 + .../settings/SettingsCommonWidget.kt | 30 +- .../{ => screen}/SettingsDataScreen.kt | 109 ++++- .../settings/screen/data/StorageInfo.kt | 74 ++++ .../tachiyomi/data/cache/ChapterCache.kt | 12 +- .../ui/setting/LongClickablePreference.kt | 24 ++ .../tachiyomi/ui/setting/PreferenceDSL.kt | 9 + .../ui/setting/SettingsComposeController.kt | 1 + .../controllers/SettingsDataController.kt | 407 +----------------- .../controllers/SettingsMainController.kt | 12 +- .../legacy/SettingsDataLegacyController.kt | 400 +++++++++++++++++ .../ui/setting/search/SettingsSearchHelper.kt | 36 +- .../java/eu/kanade/tachiyomi/util/TimeUtil.kt | 22 + .../kanade/tachiyomi/util/storage/DiskUtil.kt | 22 +- app/src/main/res/values/strings.xml | 3 + gradle/libs.versions.toml | 1 + 18 files changed, 748 insertions(+), 429 deletions(-) rename app/src/main/java/dev/yokai/presentation/settings/{ => screen}/SettingsDataScreen.kt (53%) create mode 100644 app/src/main/java/dev/yokai/presentation/settings/screen/data/StorageInfo.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/LongClickablePreference.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/legacy/SettingsDataLegacyController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b5295a59ed..ea8c32635e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -148,6 +148,7 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true } kotlinOptions { jvmTarget = "17" @@ -295,6 +296,8 @@ dependencies { implementation(kotlinx.immutable) + "coreLibraryDesugaring"(libs.desugar) + // Tests testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.test.runtime) diff --git a/app/src/main/java/dev/yokai/presentation/component/preference/Preference.kt b/app/src/main/java/dev/yokai/presentation/component/preference/Preference.kt index f997787b73..710bae4d1a 100644 --- a/app/src/main/java/dev/yokai/presentation/component/preference/Preference.kt +++ b/app/src/main/java/dev/yokai/presentation/component/preference/Preference.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import com.google.common.collect.ImmutableList -import com.google.common.collect.ImmutableMap import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackService +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap import eu.kanade.tachiyomi.core.preference.Preference as PreferenceData sealed class Preference { diff --git a/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt b/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt index d267cefe44..6bcbd2da33 100644 --- a/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt +++ b/app/src/main/java/dev/yokai/presentation/settings/ComposableSettings.kt @@ -27,4 +27,12 @@ interface ComposableSettings { appBarActions = { AppBarAction() }, ) } + + companion object { + // HACK: for the background blipping thingy. + // The title of the target PreferenceItem + // Set before showing the destination screen and reset after + // See BasePreferenceWidget.highlightBackground + var highlightKey: String? = null + } } diff --git a/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt b/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt index 689959337c..e9d232748e 100644 --- a/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt +++ b/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -23,7 +24,9 @@ import dev.yokai.presentation.component.preference.widget.PreferenceGroupHeader import eu.kanade.tachiyomi.core.preference.collectAsState import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.util.compose.LocalBackPress +import kotlinx.coroutines.delay import uy.kohesive.injekt.injectLazy +import kotlin.time.Duration.Companion.seconds @Composable fun SettingsScaffold( @@ -62,12 +65,23 @@ fun PreferenceScreen( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { + val highlightKey = ComposableSettings.highlightKey + if (highlightKey != null) { + LaunchedEffect(Unit) { + val i = items.findHighlightedIndex(highlightKey) + if (i >= 0) { + delay(0.5.seconds) + listState.animateScrollToItem(i) + } + ComposableSettings.highlightKey = null + } + } + LazyColumn( modifier = modifier, contentPadding = contentPadding, state = listState ) { - val highlightKey = null as String? // TODO items.fastForEachIndexed { i, preference -> when (preference) { is Preference.PreferenceGroup -> { @@ -94,3 +108,17 @@ fun PreferenceScreen( } } } + +private fun List.findHighlightedIndex(highlightKey: String): Int { + return flatMap { + if (it is Preference.PreferenceGroup) { + buildList { + add(null) // Header + addAll(it.preferenceItems.map { groupItem -> groupItem.title }) + add(null) // Spacer + } + } else { + listOf(it.title) + } + }.indexOfFirst { it == highlightKey } +} diff --git a/app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt b/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt similarity index 53% rename from app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt rename to app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt index 213b4dd400..817ecccb3c 100644 --- a/app/src/main/java/dev/yokai/presentation/settings/SettingsDataScreen.kt +++ b/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt @@ -1,4 +1,4 @@ -package dev.yokai.presentation.settings +package dev.yokai.presentation.settings.screen import android.content.ActivityNotFoundException import android.content.Intent @@ -16,20 +16,37 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +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.res.stringResource -import com.google.common.collect.ImmutableList +import co.touchlab.kermit.Logger import com.hippo.unifile.UniFile import dev.yokai.domain.storage.StoragePreferences import dev.yokai.presentation.component.preference.Preference import dev.yokai.presentation.component.preference.storageLocationText import dev.yokai.presentation.component.preference.widget.BasePreferenceWidget import dev.yokai.presentation.component.preference.widget.PrefsHorizontalPadding +import dev.yokai.presentation.settings.ComposableSettings +import dev.yokai.presentation.settings.screen.data.StorageInfo import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.util.system.e +import eu.kanade.tachiyomi.util.system.launchNonCancellable import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission +import eu.kanade.tachiyomi.util.system.withUIContext +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy object SettingsDataScreen : ComposableSettings { @@ -41,9 +58,10 @@ object SettingsDataScreen : ComposableSettings { val storagePreferences: StoragePreferences by injectLazy() val preferences: PreferencesHelper by injectLazy() - return ImmutableList.of( + return persistentListOf( getStorageLocationPreference(storagePreferences = storagePreferences), getBackupAndRestoreGroup(preferences = preferences), + getDataGroup(), ) } @@ -86,9 +104,11 @@ object SettingsDataScreen : ComposableSettings { @Composable private fun getBackupAndRestoreGroup(preferences: PreferencesHelper): Preference.PreferenceGroup { + val context = LocalContext.current + return Preference.PreferenceGroup( title = stringResource(R.string.backup_and_restore), - preferenceItems = ImmutableList.of( + preferenceItems = persistentListOf( Preference.PreferenceItem.CustomPreference( title = stringResource(R.string.backup_and_restore), ) { @@ -120,6 +140,87 @@ object SettingsDataScreen : ComposableSettings { }, ) }, + + // Automatic backups + Preference.PreferenceItem.ListPreference( + pref = preferences.backupInterval(), + title = stringResource(R.string.backup_frequency), + entries = persistentMapOf( + 0 to stringResource(R.string.manual), + 6 to stringResource(R.string.every_6_hours), + 12 to stringResource(R.string.every_12_hours), + 24 to stringResource(R.string.daily), + 48 to stringResource(R.string.every_2_days), + 168 to stringResource(R.string.weekly), + ), + onValueChanged = { + BackupCreatorJob.setupTask(context, it) + true + }, + ), + Preference.PreferenceItem.InfoPreference( + stringResource(R.string.backup_info) + /*+ "\n\n" + stringResource(R.string.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup))*/, + ), + ), + ) + } + + @Composable + private fun getDataGroup(): Preference.PreferenceGroup { + val context = LocalContext.current + val scope = rememberCoroutineScope() + // TODO + // val libraryPreferences = remember { Injekt.get() } + + val coverCache = 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(R.string.storage_usage), + preferenceItems = persistentListOf( + Preference.PreferenceItem.CustomPreference( + title = stringResource(R.string.storage_usage), + ) { + BasePreferenceWidget( + subcomponent = { + StorageInfo( + modifier = Modifier.padding(horizontal = PrefsHorizontalPadding), + ) + }, + ) + }, + + Preference.PreferenceItem.TextPreference( + title = stringResource(R.string.clear_chapter_cache), + subtitle = stringResource(R.string.used_, cacheReadableSize), + onClick = { + scope.launchNonCancellable { + try { + val deletedFiles = chapterCache.clear() + withUIContext { + context.toast(context.resources?.getQuantityString( + R.plurals.cache_cleared, + deletedFiles, + deletedFiles, + )) + cacheReadableSizeSema++ + } + } catch (e: Throwable) { + Logger.e(e) + withUIContext { context.toast(R.string.cache_delete_error) } + } + } + }, + ), + /* + Preference.PreferenceItem.SwitchPreference( + pref = libraryPreferences.autoClearChapterCache(), + title = stringResource(MR.strings.pref_auto_clear_chapter_cache), + ), + */ ), ) } diff --git a/app/src/main/java/dev/yokai/presentation/settings/screen/data/StorageInfo.kt b/app/src/main/java/dev/yokai/presentation/settings/screen/data/StorageInfo.kt new file mode 100644 index 0000000000..ed35836d03 --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/settings/screen/data/StorageInfo.kt @@ -0,0 +1,74 @@ +package dev.yokai.presentation.settings.screen.data + +import android.text.format.Formatter +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.yokai.presentation.core.util.secondaryItemAlpha +import dev.yokai.presentation.theme.Size +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.storage.DiskUtil +import java.io.File + +@Composable +fun StorageInfo( + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val storages = remember { DiskUtil.getExternalStorages(context) } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(Size.small), + ) { + storages.forEach { + StorageInfo(it) + } + } +} + +@Composable +private fun StorageInfo( + file: File, +) { + val context = LocalContext.current + + val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) } + val availableText = remember(available) { Formatter.formatFileSize(context, available) } + val total = remember(file) { DiskUtil.getTotalStorageSpace(file) } + val totalText = remember(total) { Formatter.formatFileSize(context, total) } + + Column( + verticalArrangement = Arrangement.spacedBy(Size.tiny), + ) { + Text( + text = file.absolutePath, + style = MaterialTheme.typography.headlineMedium, + ) + + LinearProgressIndicator( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .height(12.dp), + progress = { (1 - (available / total.toFloat())) }, + ) + + Text( + text = stringResource(R.string.available_disk_space_info, availableText, totalText), + modifier = Modifier.secondaryItemAlpha(), + style = MaterialTheme.typography.bodySmall, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index e5917688d6..6f96d61cba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -14,7 +14,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.Response @@ -227,6 +226,17 @@ class ChapterCache(private val context: Context) { } } + fun clear(): Int { + val files = cacheDir.listFiles() ?: return 0 + var deletedFiles = 0 + files.forEach { file -> + if (removeFileFromCache(file.name)) { + deletedFiles++ + } + } + return deletedFiles + } + private fun getKey(chapter: Chapter): String { return "${chapter.manga_id}${chapter.url}" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/LongClickablePreference.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/LongClickablePreference.kt new file mode 100644 index 0000000000..3758858ea7 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/LongClickablePreference.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.ui.setting + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder + +class LongClickablePreference(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) { + var onPreferenceLongClickListener: ((Preference) -> Boolean)? = null + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.itemView.setOnLongClickListener { + performLongClick() + } + } + + private fun performLongClick(): Boolean { + if (!isEnabled || !isSelectable) { + return false + } + return onPreferenceLongClickListener?.invoke(this) ?: false + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt index 3af1c97e05..152cc1541a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/PreferenceDSL.kt @@ -37,6 +37,10 @@ inline fun PreferenceManager.newScreen(block: (@DSL PreferenceScreen).() -> Unit return createPreferenceScreen(context).also { it.block() } } +inline fun PreferenceGroup.preferenceLongClickable(block: (@DSL LongClickablePreference).() -> Unit): Preference { + return initThenAdd(LongClickablePreference(context), block) +} + inline fun PreferenceGroup.preference(block: (@DSL Preference).() -> Unit): Preference { return initThenAdd(Preference(context), block) } @@ -147,6 +151,10 @@ inline fun

PreferenceGroup.addThenInit(p: P, block: P.() -> Uni } } +inline fun LongClickablePreference.onLongClick(crossinline block: () -> Unit) { + onPreferenceLongClickListener = { block(); true } +} + inline fun Preference.onClick(crossinline block: () -> Unit) { setOnPreferenceClickListener { block(); true } } @@ -248,3 +256,4 @@ var Preference.summaryRes: Int var Preference.iconTint: Int get() = 0 // set only set(value) { icon?.setTint(value) } + diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt index d219c86e2b..3d445a174e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsComposeController.kt @@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface { override fun getTitle(): String? = __getTitle() + override fun getSearchTitle(): String? = __getTitle() fun setTitle() = __setTitle() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsDataController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsDataController.kt index bfc2f53fe1..8450c664bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsDataController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsDataController.kt @@ -1,406 +1,9 @@ package eu.kanade.tachiyomi.ui.setting.controllers -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.net.Uri -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.net.toUri -import androidx.lifecycle.lifecycleScope -import androidx.preference.PreferenceScreen -import com.hippo.unifile.UniFile -import dev.yokai.domain.storage.StorageManager -import dev.yokai.domain.storage.StoragePreferences -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupFileValidator -import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob -import eu.kanade.tachiyomi.data.backup.create.BackupOptions -import eu.kanade.tachiyomi.data.backup.models.Backup -import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob -import eu.kanade.tachiyomi.data.cache.ChapterCache -import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController -import eu.kanade.tachiyomi.ui.setting.bindTo -import eu.kanade.tachiyomi.ui.setting.infoPreference -import eu.kanade.tachiyomi.ui.setting.intListPreference -import eu.kanade.tachiyomi.ui.setting.onChange -import eu.kanade.tachiyomi.ui.setting.onClick -import eu.kanade.tachiyomi.ui.setting.preference -import eu.kanade.tachiyomi.ui.setting.preferenceCategory -import eu.kanade.tachiyomi.ui.setting.summaryRes -import eu.kanade.tachiyomi.ui.setting.titleRes -import eu.kanade.tachiyomi.util.system.DeviceUtil -import eu.kanade.tachiyomi.util.system.disableItems -import eu.kanade.tachiyomi.util.system.launchIO -import eu.kanade.tachiyomi.util.system.materialAlertDialog -import eu.kanade.tachiyomi.util.system.openInBrowser -import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission -import eu.kanade.tachiyomi.util.system.withUIContext -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import uy.kohesive.injekt.injectLazy +import dev.yokai.presentation.settings.ComposableSettings +import dev.yokai.presentation.settings.screen.SettingsDataScreen +import eu.kanade.tachiyomi.ui.setting.SettingsComposeController -class SettingsDataController : SettingsLegacyController() { - - /** - * Flags containing information of what to backup. - */ - private var backupFlags: BackupOptions = BackupOptions() - internal val storagePreferences: StoragePreferences by injectLazy() - internal val storageManager: StorageManager by injectLazy() - - private val coverCache: CoverCache by injectLazy() - private val chapterCache: ChapterCache by injectLazy() - - override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { - titleRes = R.string.data_and_storage - - preference { - key = "pref_storage_location" - bindTo(storagePreferences.baseStorageDirectory()) - titleRes = R.string.storage_location - - onClick { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - startActivityForResult(intent, CODE_DATA_DIR) - } catch (e: ActivityNotFoundException) { - activity?.toast(R.string.file_picker_error) - } - } - - storagePreferences.baseStorageDirectory().changes() - .onEach { path -> - summary = UniFile.fromUri(context, path.toUri())?.let { dir -> - dir.filePath ?: context.getString(R.string.invalid_location, dir.uri) - } ?: context.getString(R.string.invalid_location_generic) - } - .launchIn(viewScope) - } - - preference { - key = "pref_create_backup" - titleRes = R.string.create_backup - summaryRes = R.string.can_be_used_to_restore - - onClick { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) - } - - if (!BackupCreatorJob.isManualJobRunning(context)) { - showBackupCreateDialog() - } else { - context.toast(R.string.backup_in_progress) - } - } - } - preference { - key = "pref_restore_backup" - titleRes = R.string.restore_backup - summaryRes = R.string.restore_from_backup_file - - onClick { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) - } - - if (!BackupRestoreJob.isRunning(context)) { - (activity as? MainActivity)?.getExtensionUpdates(true) - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - storageManager.getBackupsDirectory()?.let { - intent.setDataAndType(it.uri, "*/*") - } - val title = resources?.getString(R.string.select_backup_file) - val chooser = Intent.createChooser(intent, title) - startActivityForResult(chooser, CODE_BACKUP_RESTORE) - } else { - context.toast(R.string.restore_in_progress) - } - } - } - - preferenceCategory { - titleRes = R.string.automatic_backups - - intListPreference(activity) { - bindTo(preferences.backupInterval()) - titleRes = R.string.backup_frequency - entriesRes = arrayOf( - R.string.manual, - R.string.every_6_hours, - R.string.every_12_hours, - R.string.daily, - R.string.every_2_days, - R.string.weekly, - ) - entryValues = listOf(0, 6, 12, 24, 48, 168) - - onChange { newValue -> - val interval = newValue as Int - BackupCreatorJob.setupTask(context, interval) - true - } - } - intListPreference(activity) { - bindTo(preferences.numberOfBackups()) - titleRes = R.string.max_auto_backups - entries = (1..5).map(Int::toString) - entryRange = 1..5 - - visibleIf(preferences.backupInterval()) { it > 0 } - } - } - - infoPreference(R.string.backup_info) - - preferenceCategory { - titleRes = R.string.storage_usage - - preference { - key = CLEAR_CACHE_KEY - titleRes = R.string.clear_chapter_cache - summary = context.getString(R.string.used_, chapterCache.readableSize) - - onClick { clearChapterCache() } - } - - preference { - key = "clear_cached_not_library" - titleRes = R.string.clear_cached_covers_non_library - summary = context.getString( - R.string.delete_all_covers__not_in_library_used_, - coverCache.getOnlineCoverCacheSize(), - ) - - onClick { - context.toast(R.string.starting_cleanup) - (activity as? AppCompatActivity)?.lifecycleScope?.launchIO { - coverCache.deleteAllCachedCovers() - } - } - } - - preference { - key = "clean_cached_covers" - titleRes = R.string.clean_up_cached_covers - summary = context.getString( - R.string.delete_old_covers_in_library_used_, - coverCache.getChapterCacheSize(), - ) - - onClick { - context.toast(R.string.starting_cleanup) - (activity as? AppCompatActivity)?.lifecycleScope?.launchIO { - coverCache.deleteOldCovers() - } - } - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.settings_backup, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.action_backup_help -> activity?.openInBrowser(HELP_URL) - } - return super.onOptionsItemSelected(item) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (data != null && resultCode == Activity.RESULT_OK) { - val activity = activity ?: return - val uri = data.data - - if (uri == null) { - activity.toast(R.string.backup_restore_invalid_uri) - return - } - - when (requestCode) { - CODE_DATA_DIR -> { - // Get UriPermission so it's possible to write files - val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - activity.tryTakePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(activity, uri)!! - storagePreferences.baseStorageDirectory().set(file.uri.toString()) - } - - CODE_BACKUP_CREATE -> { - doBackup(backupFlags, uri, true) - } - - CODE_BACKUP_RESTORE -> { - (activity as? MainActivity)?.showNotificationPermissionPrompt(true) - showBackupRestoreDialog(uri) - } - } - } - } - - private fun doBackup(options: BackupOptions, uri: Uri, requirePersist: Boolean = false) { - val activity = activity ?: return - - val actualUri = - if (requirePersist) { - val intentFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - activity.tryTakePersistableUriPermission(uri, intentFlags) - uri - } else { - UniFile.fromUri(activity, uri)?.createFile(Backup.getBackupFilename())?.uri - } ?: return - activity.toast(R.string.creating_backup) - BackupCreatorJob.startNow(activity, actualUri, options) - } - - private fun createBackup(options: BackupOptions, picker: Boolean = false) { - backupFlags = options - - val dir = storageManager.getBackupsDirectory() - if (dir == null) { - activity?.toast(R.string.invalid_location_generic) - return - } - - if (!picker) { - doBackup(backupFlags, dir.uri) - return - } - - try { - // Use Android's built-in file creator - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType("application/*") - .putExtra(Intent.EXTRA_TITLE, Backup.getBackupFilename()) - - startActivityForResult(intent, CODE_BACKUP_CREATE) - } catch (e: ActivityNotFoundException) { - activity?.toast(R.string.file_picker_error) - } - } - - private fun showBackupCreateDialog() { - val activity = activity ?: return - val options = BackupOptions.getOptions().map { activity.getString(it) } - - activity.materialAlertDialog() - .setTitle(R.string.what_should_backup) - .setMultiChoiceItems( - options.toTypedArray(), - BackupOptions().asBooleanArray(), - ) { dialog, position, _ -> - if (position == 0) { - val listView = (dialog as AlertDialog).listView - listView.setItemChecked(position, true) - } - } - .setPositiveButton(R.string.create) { dialog, _ -> - val listView = (dialog as AlertDialog).listView - val booleanArrayList = arrayListOf(true) - // TODO: Allow library_entries to be disabled - for (i in 1 until listView.count) { // skip 0, since 0 is always enabled - booleanArrayList.add(listView.isItemChecked(i)) - } - createBackup(BackupOptions.fromBooleanArray(booleanArrayList.toBooleanArray())) - } - .setNegativeButton(android.R.string.cancel, null) - .show().apply { - disableItems(arrayOf(options.first())) - } - } - - private fun showBackupRestoreDialog(uri: Uri) { - val activity = activity ?: return - - try { - val results = BackupFileValidator().validate(activity, uri) - - var message = activity.getString(R.string.restore_content_full) - if (results.missingSources.isNotEmpty()) { - message += "\n\n${activity.getString(R.string.restore_missing_sources)}\n${ - results.missingSources.joinToString( - "\n", - ) { "- $it" } - }" - } - if (results.missingTrackers.isNotEmpty()) { - message += "\n\n${activity.getString(R.string.restore_missing_trackers)}\n${ - results.missingTrackers.joinToString( - "\n", - ) { "- $it" } - }" - } - - activity.materialAlertDialog() - .setTitle(R.string.restore_backup) - .setMessage(message) - .setPositiveButton(R.string.restore) { _, _ -> - val context = applicationContext - if (context != null) { - activity.toast(R.string.restoring_backup) - BackupRestoreJob.start(context, uri) - } - }.show() - } catch (e: Exception) { - activity.materialAlertDialog() - .setTitle(R.string.invalid_backup_file) - .setMessage(e.message) - .setPositiveButton(android.R.string.cancel, null) - .show() - } - } - - private fun clearChapterCache() { - if (activity == null) return - viewScope.launchIO { - val files = chapterCache.cacheDir.listFiles() ?: return@launchIO - var deletedFiles = 0 - try { - files.forEach { file -> - if (chapterCache.removeFileFromCache(file.name)) { - deletedFiles++ - } - } - withUIContext { - activity?.toast( - resources?.getQuantityString( - R.plurals.cache_cleared, - deletedFiles, - deletedFiles, - ), - ) - findPreference(CLEAR_CACHE_KEY)?.summary = - resources?.getString(R.string.used_, chapterCache.readableSize) - } - } catch (_: Exception) { - withUIContext { - activity?.toast(R.string.cache_delete_error) - } - } - } - } +class SettingsDataController : SettingsComposeController() { + override fun getComposableSettings(): ComposableSettings = SettingsDataScreen } - -private const val CLEAR_CACHE_KEY = "pref_clear_cache_key" - -private const val CODE_DATA_DIR = 104 -private const val CODE_BACKUP_CREATE = 504 -private const val CODE_BACKUP_RESTORE = 505 - -private const val HELP_URL = "https://tachiyomi.org/docs/guides/backups" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsMainController.kt index bd004d7244..c1f317fdba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsMainController.kt @@ -13,13 +13,17 @@ import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.main.FloatingSearchInterface import eu.kanade.tachiyomi.ui.more.AboutController import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController +import eu.kanade.tachiyomi.ui.setting.controllers.legacy.SettingsDataLegacyController import eu.kanade.tachiyomi.ui.setting.iconRes import eu.kanade.tachiyomi.ui.setting.iconTint import eu.kanade.tachiyomi.ui.setting.onClick +import eu.kanade.tachiyomi.ui.setting.onLongClick import eu.kanade.tachiyomi.ui.setting.preference +import eu.kanade.tachiyomi.ui.setting.preferenceLongClickable import eu.kanade.tachiyomi.ui.setting.search.SettingsSearchController import eu.kanade.tachiyomi.ui.setting.titleRes import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.fadeTransactionHandler import eu.kanade.tachiyomi.util.view.openInBrowser @@ -78,11 +82,15 @@ class SettingsMainController : SettingsLegacyController(), FloatingSearchInterfa titleRes = R.string.tracking onClick { navigateTo(SettingsTrackingController()) } } - preference { + preferenceLongClickable { iconRes = R.drawable.ic_storage_24dp iconTint = tintColor titleRes = R.string.data_and_storage - onClick { navigateTo(SettingsDataController()) } + onClick { navigateTo(SettingsDataLegacyController()) } + onLongClick { + navigateTo(SettingsDataController()) + context.toast("You're entering beta version of 'Data and storage'") + } } preference { iconRes = R.drawable.ic_security_24dp diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/legacy/SettingsDataLegacyController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/legacy/SettingsDataLegacyController.kt new file mode 100644 index 0000000000..44a091bf40 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/legacy/SettingsDataLegacyController.kt @@ -0,0 +1,400 @@ +package eu.kanade.tachiyomi.ui.setting.controllers.legacy + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceScreen +import com.hippo.unifile.UniFile +import dev.yokai.domain.storage.StorageManager +import dev.yokai.domain.storage.StoragePreferences +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.BackupFileValidator +import eu.kanade.tachiyomi.data.backup.create.BackupCreatorJob +import eu.kanade.tachiyomi.data.backup.create.BackupOptions +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob +import eu.kanade.tachiyomi.data.cache.ChapterCache +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController +import eu.kanade.tachiyomi.ui.setting.bindTo +import eu.kanade.tachiyomi.ui.setting.infoPreference +import eu.kanade.tachiyomi.ui.setting.intListPreference +import eu.kanade.tachiyomi.ui.setting.onChange +import eu.kanade.tachiyomi.ui.setting.onClick +import eu.kanade.tachiyomi.ui.setting.preference +import eu.kanade.tachiyomi.ui.setting.preferenceCategory +import eu.kanade.tachiyomi.ui.setting.summaryRes +import eu.kanade.tachiyomi.ui.setting.titleRes +import eu.kanade.tachiyomi.util.system.DeviceUtil +import eu.kanade.tachiyomi.util.system.disableItems +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.materialAlertDialog +import eu.kanade.tachiyomi.util.system.openInBrowser +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.tryTakePersistableUriPermission +import eu.kanade.tachiyomi.util.system.withUIContext +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import uy.kohesive.injekt.injectLazy + +class SettingsDataLegacyController : SettingsLegacyController() { + + /** + * Flags containing information of what to backup. + */ + private var backupFlags: BackupOptions = BackupOptions() + internal val storagePreferences: StoragePreferences by injectLazy() + internal val storageManager: StorageManager by injectLazy() + + private val coverCache: CoverCache by injectLazy() + private val chapterCache: ChapterCache by injectLazy() + + override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply { + titleRes = R.string.data_and_storage + + preference { + key = "pref_storage_location" + bindTo(storagePreferences.baseStorageDirectory()) + titleRes = R.string.storage_location + + onClick { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + startActivityForResult(intent, CODE_DATA_DIR) + } catch (e: ActivityNotFoundException) { + activity?.toast(R.string.file_picker_error) + } + } + + storagePreferences.baseStorageDirectory().changes() + .onEach { path -> + summary = UniFile.fromUri(context, path.toUri())?.let { dir -> + dir.filePath ?: context.getString(R.string.invalid_location, dir.uri) + } ?: context.getString(R.string.invalid_location_generic) + } + .launchIn(viewScope) + } + + preference { + key = "pref_create_backup" + titleRes = R.string.create_backup + summaryRes = R.string.can_be_used_to_restore + + onClick { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) + } + + if (!BackupCreatorJob.isManualJobRunning(context)) { + showBackupCreateDialog() + } else { + context.toast(R.string.backup_in_progress) + } + } + } + preference { + key = "pref_restore_backup" + titleRes = R.string.restore_backup + summaryRes = R.string.restore_from_backup_file + + onClick { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) + } + + if (!BackupRestoreJob.isRunning(context)) { + (activity as? MainActivity)?.getExtensionUpdates(true) + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + storageManager.getBackupsDirectory()?.let { + intent.setDataAndType(it.uri, "*/*") + } + val title = resources?.getString(R.string.select_backup_file) + val chooser = Intent.createChooser(intent, title) + startActivityForResult(chooser, CODE_BACKUP_RESTORE) + } else { + context.toast(R.string.restore_in_progress) + } + } + } + + preferenceCategory { + titleRes = R.string.automatic_backups + + intListPreference(activity) { + bindTo(preferences.backupInterval()) + titleRes = R.string.backup_frequency + entriesRes = arrayOf( + R.string.manual, + R.string.every_6_hours, + R.string.every_12_hours, + R.string.daily, + R.string.every_2_days, + R.string.weekly, + ) + entryValues = listOf(0, 6, 12, 24, 48, 168) + + onChange { newValue -> + val interval = newValue as Int + BackupCreatorJob.setupTask(context, interval) + true + } + } + intListPreference(activity) { + bindTo(preferences.numberOfBackups()) + titleRes = R.string.max_auto_backups + entries = (1..5).map(Int::toString) + entryRange = 1..5 + + visibleIf(preferences.backupInterval()) { it > 0 } + } + } + + infoPreference(R.string.backup_info) + + preferenceCategory { + titleRes = R.string.storage_usage + + preference { + key = CLEAR_CACHE_KEY + titleRes = R.string.clear_chapter_cache + summary = context.getString(R.string.used_, chapterCache.readableSize) + + onClick { clearChapterCache() } + } + + preference { + key = "clear_cached_not_library" + titleRes = R.string.clear_cached_covers_non_library + summary = context.getString( + R.string.delete_all_covers__not_in_library_used_, + coverCache.getOnlineCoverCacheSize(), + ) + + onClick { + context.toast(R.string.starting_cleanup) + (activity as? AppCompatActivity)?.lifecycleScope?.launchIO { + coverCache.deleteAllCachedCovers() + } + } + } + + preference { + key = "clean_cached_covers" + titleRes = R.string.clean_up_cached_covers + summary = context.getString( + R.string.delete_old_covers_in_library_used_, + coverCache.getChapterCacheSize(), + ) + + onClick { + context.toast(R.string.starting_cleanup) + (activity as? AppCompatActivity)?.lifecycleScope?.launchIO { + coverCache.deleteOldCovers() + } + } + } + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.settings_backup, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_backup_help -> activity?.openInBrowser(HELP_URL) + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (data != null && resultCode == Activity.RESULT_OK) { + val activity = activity ?: return + val uri = data.data + + if (uri == null) { + activity.toast(R.string.backup_restore_invalid_uri) + return + } + + when (requestCode) { + CODE_DATA_DIR -> { + // Get UriPermission so it's possible to write files + val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + activity.tryTakePersistableUriPermission(uri, flags) + val file = UniFile.fromUri(activity, uri)!! + storagePreferences.baseStorageDirectory().set(file.uri.toString()) + } + + CODE_BACKUP_CREATE -> { + doBackup(backupFlags, uri, true) + } + + CODE_BACKUP_RESTORE -> { + (activity as? MainActivity)?.showNotificationPermissionPrompt(true) + showBackupRestoreDialog(uri) + } + } + } + } + + private fun doBackup(options: BackupOptions, uri: Uri, requirePersist: Boolean = false) { + val activity = activity ?: return + + val actualUri = + if (requirePersist) { + val intentFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + activity.tryTakePersistableUriPermission(uri, intentFlags) + uri + } else { + UniFile.fromUri(activity, uri)?.createFile(Backup.getBackupFilename())?.uri + } ?: return + activity.toast(R.string.creating_backup) + BackupCreatorJob.startNow(activity, actualUri, options) + } + + private fun createBackup(options: BackupOptions, picker: Boolean = false) { + backupFlags = options + + val dir = storageManager.getBackupsDirectory() + if (dir == null) { + activity?.toast(R.string.invalid_location_generic) + return + } + + if (!picker) { + doBackup(backupFlags, dir.uri) + return + } + + try { + // Use Android's built-in file creator + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("application/*") + .putExtra(Intent.EXTRA_TITLE, Backup.getBackupFilename()) + + startActivityForResult(intent, CODE_BACKUP_CREATE) + } catch (e: ActivityNotFoundException) { + activity?.toast(R.string.file_picker_error) + } + } + + private fun showBackupCreateDialog() { + val activity = activity ?: return + val options = BackupOptions.getOptions().map { activity.getString(it) } + + activity.materialAlertDialog() + .setTitle(R.string.what_should_backup) + .setMultiChoiceItems( + options.toTypedArray(), + BackupOptions().asBooleanArray(), + ) { dialog, position, _ -> + if (position == 0) { + val listView = (dialog as AlertDialog).listView + listView.setItemChecked(position, true) + } + } + .setPositiveButton(R.string.create) { dialog, _ -> + val listView = (dialog as AlertDialog).listView + val booleanArrayList = arrayListOf(true) + // TODO: Allow library_entries to be disabled + for (i in 1 until listView.count) { // skip 0, since 0 is always enabled + booleanArrayList.add(listView.isItemChecked(i)) + } + createBackup(BackupOptions.fromBooleanArray(booleanArrayList.toBooleanArray())) + } + .setNegativeButton(android.R.string.cancel, null) + .show().apply { + disableItems(arrayOf(options.first())) + } + } + + private fun showBackupRestoreDialog(uri: Uri) { + val activity = activity ?: return + + try { + val results = BackupFileValidator().validate(activity, uri) + + var message = activity.getString(R.string.restore_content_full) + if (results.missingSources.isNotEmpty()) { + message += "\n\n${activity.getString(R.string.restore_missing_sources)}\n${ + results.missingSources.joinToString( + "\n", + ) { "- $it" } + }" + } + if (results.missingTrackers.isNotEmpty()) { + message += "\n\n${activity.getString(R.string.restore_missing_trackers)}\n${ + results.missingTrackers.joinToString( + "\n", + ) { "- $it" } + }" + } + + activity.materialAlertDialog() + .setTitle(R.string.restore_backup) + .setMessage(message) + .setPositiveButton(R.string.restore) { _, _ -> + val context = applicationContext + if (context != null) { + activity.toast(R.string.restoring_backup) + BackupRestoreJob.start(context, uri) + } + }.show() + } catch (e: Exception) { + activity.materialAlertDialog() + .setTitle(R.string.invalid_backup_file) + .setMessage(e.message) + .setPositiveButton(android.R.string.cancel, null) + .show() + } + } + + private fun clearChapterCache() { + if (activity == null) return + viewScope.launchIO { + try { + val deletedFiles = chapterCache.clear() + withUIContext { + activity?.toast( + resources?.getQuantityString( + R.plurals.cache_cleared, + deletedFiles, + deletedFiles, + ), + ) + findPreference(CLEAR_CACHE_KEY)?.summary = + resources?.getString(R.string.used_, chapterCache.readableSize) + } + } catch (_: Exception) { + withUIContext { + activity?.toast(R.string.cache_delete_error) + } + } + } + } +} + +private const val CLEAR_CACHE_KEY = "pref_clear_cache_key" + +private const val CODE_DATA_DIR = 104 +private const val CODE_BACKUP_CREATE = 504 +private const val CODE_BACKUP_RESTORE = 505 + +private const val HELP_URL = "https://tachiyomi.org/docs/guides/backups" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt index a26a3c7d36..bc3ba652f2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchHelper.kt @@ -9,17 +9,17 @@ import androidx.preference.PreferenceGroup import androidx.preference.PreferenceManager import eu.kanade.tachiyomi.ui.setting.SettingsComposeController import eu.kanade.tachiyomi.ui.setting.SettingsControllerInterface +import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsAdvancedController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsAppearanceController -import eu.kanade.tachiyomi.ui.setting.controllers.SettingsDataController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsBrowseController -import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsDownloadController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsGeneralController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsLibraryController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsReaderController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsSecurityController import eu.kanade.tachiyomi.ui.setting.controllers.SettingsTrackingController +import eu.kanade.tachiyomi.ui.setting.controllers.legacy.SettingsDataLegacyController import eu.kanade.tachiyomi.util.system.isLTR import eu.kanade.tachiyomi.util.system.launchNow import kotlin.reflect.KClass @@ -31,9 +31,10 @@ object SettingsSearchHelper { /** * All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable. */ - private val settingLegacyControllersList: List> = listOf( + private val settingControllersList: List> = listOf( SettingsAdvancedController::class, - SettingsDataController::class, + SettingsDataLegacyController::class, + // SettingsDataController::class, // compose SettingsBrowseController::class, SettingsDownloadController::class, SettingsGeneralController::class, @@ -44,9 +45,6 @@ object SettingsSearchHelper { SettingsTrackingController::class, ) - private val settingComposeControllersList: List> = listOf( - ) - /** * Must be called to populate `prefSearchResultList` */ @@ -56,14 +54,22 @@ object SettingsSearchHelper { prefSearchResultList.clear() launchNow { - settingLegacyControllersList.forEach { kClass -> - val ctrl = kClass.createInstance() - val settingsPrefScreen = ctrl.setupPreferenceScreen(preferenceManager.createPreferenceScreen(context)) - val prefCount = settingsPrefScreen.preferenceCount - for (i in 0 until prefCount) { - val rootPref = settingsPrefScreen.getPreference(i) - if (rootPref.title == null) continue // no title, not a preference. (note: only info notes appear to not have titles) - getLegacySettingSearchResult(ctrl, rootPref, "${settingsPrefScreen.title}") + settingControllersList.forEach { kClass -> + when (val ctrl = kClass.createInstance()) { + is SettingsLegacyController -> { + val settingsPrefScreen = + ctrl.setupPreferenceScreen(preferenceManager.createPreferenceScreen(context)) + val prefCount = settingsPrefScreen.preferenceCount + for (i in 0 until prefCount) { + val rootPref = settingsPrefScreen.getPreference(i) + if (rootPref.title == null) continue // no title, not a preference. (note: only info notes appear to not have titles) + getLegacySettingSearchResult(ctrl, rootPref, "${settingsPrefScreen.title}") + } + } + is SettingsComposeController -> { + // TODO: Impossible to achieve, require search to be composable + // ctrl.getComposableSettings().getPreferences() + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt new file mode 100644 index 0000000000..fb8f512c93 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.util + +import android.text.format.DateUtils +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.stringResource +import eu.kanade.tachiyomi.R +import java.time.Instant +import kotlin.time.Duration.Companion.minutes + +@Composable +@ReadOnlyComposable +fun relativeTimeSpanString(epochMillis: Long): String { + val now = Instant.now().toEpochMilli() + return when { + epochMillis <= 0L -> stringResource(R.string.never) + now - epochMillis < 1.minutes.inWholeMilliseconds -> stringResource( + R.string.just_now, + ) + else -> DateUtils.getRelativeTimeSpanString(epochMillis, now, DateUtils.MINUTE_IN_MILLIS).toString() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index fedf2f7a6f..016418593f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -29,12 +29,30 @@ object DiskUtil { return size } + fun getTotalStorageSpace(file: UniFile): Long = getTotalStorageSpace(file.uri.path) + fun getTotalStorageSpace(file: File): Long = getTotalStorageSpace(file.absolutePath) + + /** + * Gets the total space for the disk that a file path points to, in bytes. + */ + fun getTotalStorageSpace(path: String?): Long { + return try { + val stat = StatFs(path) + stat.blockCountLong * stat.blockSizeLong + } catch (_: Exception) { + -1L + } + } + + fun getAvailableStorageSpace(file: UniFile): Long = getAvailableStorageSpace(file.uri.path) + fun getAvailableStorageSpace(file: File): Long = getAvailableStorageSpace(file.absolutePath) + /** * Gets the available space for the disk that a file path points to, in bytes. */ - fun getAvailableStorageSpace(f: UniFile): Long { + fun getAvailableStorageSpace(path: String?): Long { return try { - val stat = StatFs(f.uri.path) + val stat = StatFs(path) stat.availableBlocksLong * stat.blockSizeLong } catch (_: Exception) { -1L diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8e2738aa6..060454eb11 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -806,6 +806,7 @@ Data and storage Storage location Storage usage + Available: %1$s / Total: %2$s Backup @@ -847,6 +848,7 @@ Not logged into %1$s Include sensitive settings (e.g. tracker login tokens) + Last automatically backed up: %s Clear chapter cache @@ -1195,6 +1197,7 @@ Recently installed Language Never + Just now Newest Next No animation diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7110733878..afc9f26393 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ compose-theme-adapter3 = { module = "com.google.accompanist:accompanist-themeada conductor = { module = "com.bluelinelabs:conductor", version = "4.0.0-preview-4" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version = "3.0.0" } conscrypt = { module = "org.conscrypt:conscrypt-android", version = "2.5.2" } +desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } directionalviewpager = { module = "com.github.tachiyomiorg:DirectionalViewPager", version = "1.0.0" } disklrucache = { module = "com.jakewharton:disklrucache", version = "2.0.2" } fastadapter-extensions-binding = { module = "com.mikepenz:fastadapter-extensions-binding", version.ref = "fast_adapter" }