mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
feat: Composable Data and storage Settings
This commit is contained in:
parent
b2b93d6d50
commit
9d0cefa11f
18 changed files with 748 additions and 429 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Preference>.findHighlightedIndex(highlightKey: String): Int {
|
||||
return flatMap {
|
||||
if (it is Preference.PreferenceGroup) {
|
||||
buildList<String?> {
|
||||
add(null) // Header
|
||||
addAll(it.preferenceItems.map { groupItem -> groupItem.title })
|
||||
add(null) // Spacer
|
||||
}
|
||||
} else {
|
||||
listOf(it.title)
|
||||
}
|
||||
}.indexOfFirst { it == highlightKey }
|
||||
}
|
||||
|
|
|
@ -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<LibraryPreferences>() }
|
||||
|
||||
val coverCache = remember { Injekt.get<CoverCache>() }
|
||||
val chapterCache = remember { Injekt.get<ChapterCache>() }
|
||||
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),
|
||||
),
|
||||
*/
|
||||
),
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 <P : Preference> 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) }
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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<KClass<out SettingsLegacyController>> = listOf(
|
||||
private val settingControllersList: List<KClass<out SettingsControllerInterface>> = 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<KClass<out SettingsComposeController>> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
22
app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt
Normal file
22
app/src/main/java/eu/kanade/tachiyomi/util/TimeUtil.kt
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -806,6 +806,7 @@
|
|||
<string name="data_and_storage">Data and storage</string>
|
||||
<string name="storage_location">Storage location</string>
|
||||
<string name="storage_usage">Storage usage</string>
|
||||
<string name="available_disk_space_info">Available: %1$s / Total: %2$s</string>
|
||||
|
||||
<!-- Backup -->
|
||||
<string name="backup">Backup</string>
|
||||
|
@ -847,6 +848,7 @@
|
|||
</plurals>
|
||||
<string name="not_logged_into_">Not logged into %1$s</string>
|
||||
<string name="backup_private_pref">Include sensitive settings (e.g. tracker login tokens)</string>
|
||||
<string name="last_auto_backup_info">Last automatically backed up: %s</string>
|
||||
|
||||
<!-- Advanced section -->
|
||||
<string name="clear_chapter_cache">Clear chapter cache</string>
|
||||
|
@ -1195,6 +1197,7 @@
|
|||
<string name="recently_installed">Recently installed</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="never">Never</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="newest">Newest</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="no_animation">No animation</string>
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue