feat: Add restore functionality to composable data settings

This commit is contained in:
Ahmad Ansori Palembani 2024-06-10 13:49:22 +07:00
parent 981c92043f
commit 37e7e74c34
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
7 changed files with 185 additions and 31 deletions

View file

@ -23,7 +23,9 @@ import dev.yokai.presentation.component.preference.PreferenceItem
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.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.currentOrThrow
import kotlinx.coroutines.delay
import uy.kohesive.injekt.injectLazy
import kotlin.time.Duration.Companion.seconds
@ -38,10 +40,11 @@ fun SettingsScaffold(
val preferences: PreferencesHelper by injectLazy()
val useLargeAppBar by preferences.useLargeToolbar().collectAsState()
val listState = rememberLazyListState()
val onBackPress = LocalBackPress.current
val onBackPress = LocalBackPress.currentOrThrow
val alertDialog = LocalAlertDialog.currentOrThrow
YokaiScaffold(
onNavigationIconClicked = onBackPress ?: {},
onNavigationIconClicked = onBackPress,
title = title,
appBarType = appBarType ?: if (useLargeAppBar) AppBarType.LARGE else AppBarType.SMALL,
actions = appBarActions,
@ -50,6 +53,8 @@ fun SettingsScaffold(
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
),
) { innerPadding ->
alertDialog.content?.let { it() }
PreferenceScreen(
items = itemsProvider(),
listState = listState,

View file

@ -1,8 +1,10 @@
package dev.yokai.presentation.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@ -32,12 +34,19 @@ 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.RestoreBackup
import dev.yokai.presentation.settings.screen.data.StorageInfo
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.restore.BackupRestoreJob
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.extension.ExtensionManager
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.currentOrThrow
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.launchNonCancellable
import eu.kanade.tachiyomi.util.system.toast
@ -45,6 +54,7 @@ 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 kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -104,7 +114,35 @@ object SettingsDataScreen : ComposableSettings {
@Composable
private fun getBackupAndRestoreGroup(preferences: PreferencesHelper): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current
val alertDialog = LocalAlertDialog.currentOrThrow
val extensionManager = remember { Injekt.get<ExtensionManager>() }
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
intent.addCategory(Intent.CATEGORY_OPENABLE)
return Intent.createChooser(intent, context.getString(R.string.select_backup_file))
}
},
) {
if (it == null) {
context.toast(R.string.invalid_location_generic)
return@rememberLauncherForActivityResult
}
val results = try {
Pair(BackupFileValidator().validate(context, it), null)
} catch (e: Exception) {
Pair(null, e)
}
alertDialog.content = {
RestoreBackup(context = context, uri = it, pair = results)
}
}
return Preference.PreferenceGroup(
title = stringResource(R.string.backup_and_restore),
@ -123,7 +161,17 @@ object SettingsDataScreen : ComposableSettings {
SegmentedButton(
modifier = Modifier.fillMaxHeight(),
checked = false,
onCheckedChange = {},
onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
context.toast("Not yet available")
} else {
context.toast(R.string.backup_in_progress)
}
},
shape = SegmentedButtonDefaults.itemShape(0, 2),
) {
Text(stringResource(R.string.create_backup))
@ -131,7 +179,18 @@ object SettingsDataScreen : ComposableSettings {
SegmentedButton(
modifier = Modifier.fillMaxHeight(),
checked = false,
onCheckedChange = {},
onCheckedChange = {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
scope.launch { extensionManager.getExtensionUpdates(true) }
chooseBackup.launch("*/*")
} else {
context.toast(R.string.restore_in_progress)
}
},
shape = SegmentedButtonDefaults.itemShape(1, 2),
) {
Text(stringResource(R.string.restore_backup))

View file

@ -0,0 +1,75 @@
package dev.yokai.presentation.settings.screen.data
import android.content.Context
import android.net.Uri
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupFileValidator.Results
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.util.system.toast
@Composable
fun RestoreBackup(context: Context, uri: Uri, pair: Pair<Results?, Exception?>) {
val (results, e) = pair
if (results != null) {
var message = stringResource(R.string.restore_content_full)
if (results.missingSources.isNotEmpty()) {
message += "\n\n${stringResource(R.string.restore_missing_sources)}\n${
results.missingSources.joinToString(
"\n",
) { "- $it" }
}"
}
if (results.missingTrackers.isNotEmpty()) {
message += "\n\n${stringResource(R.string.restore_missing_trackers)}\n${
results.missingTrackers.joinToString(
"\n",
) { "- $it" }
}"
}
AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(
onClick = {
context.toast(R.string.restoring_backup)
BackupRestoreJob.start(context, uri)
},
) {
Text(text = stringResource(R.string.restore))
}
},
dismissButton = {
TextButton(onClick = {}) {
Text(text = stringResource(android.R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.restore_backup)) },
text = { Text(text = message) },
)
} else {
AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(onClick = {}) {
Text(text = stringResource(android.R.string.cancel))
}
},
title = { Text(text = stringResource(R.string.invalid_backup_file)) },
text = { e?.message?.let { Text(text = it) } }
)
}
}
@Composable
private fun CreateBackup(context: Context) {
AlertDialog(
onDismissRequest = {},
confirmButton = {},
)
}

View file

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
@ -28,6 +29,7 @@ import kotlinx.parcelize.Parcelize
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.*
/**
* The manager of extensions installed as another apk which extend the available sources. It handles
@ -127,6 +129,25 @@ class ExtensionManager(
return ExtensionLoader.isExtensionInstalledByApp(context, extension.pkgName)
}
suspend fun getExtensionUpdates(force: Boolean) {
if ((force && availableExtensionsFlow.value.isEmpty()) ||
Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6)
) {
withIOContext {
try {
findAvailableExtensions()
val pendingUpdates = ExtensionApi().checkForUpdates(
context,
availableExtensionsFlow.value.takeIf { it.isNotEmpty() },
)
preferences.extensionUpdatesCount().set(pendingUpdates.size)
preferences.lastExtCheck().set(Date().time)
} catch (_: Exception) {
}
}
}
}
/**
* Finds the available extensions in the [api] and updates [availableExtensionsFlow].
*/

View file

@ -89,7 +89,6 @@ import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
@ -144,13 +143,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToLong
@ -706,7 +702,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
splashScreen?.configure()
getExtensionUpdates(true)
lifecycleScope.launchIO {
extensionManager.getExtensionUpdates(true)
}
preferences.extensionUpdatesCount()
.changesIn(lifecycleScope) {
@ -941,7 +939,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
override fun onResume() {
super.onResume()
checkForAppUpdates()
getExtensionUpdates(false)
lifecycleScope.launchIO {
extensionManager.getExtensionUpdates(false)
}
setExtensionsBadge()
DownloadJob.callListeners(downloadManager = downloadManager)
showDLQueueTutorial()
@ -1016,25 +1016,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
}
}
fun getExtensionUpdates(force: Boolean) {
if ((force && extensionManager.availableExtensionsFlow.value.isEmpty()) ||
Date().time >= preferences.lastExtCheck().get() + TimeUnit.HOURS.toMillis(6)
) {
lifecycleScope.launch(Dispatchers.IO) {
try {
extensionManager.findAvailableExtensions()
val pendingUpdates = ExtensionApi().checkForUpdates(
this@MainActivity,
extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() },
)
preferences.extensionUpdatesCount().set(pendingUpdates.size)
preferences.lastExtCheck().set(Date().time)
} catch (_: Exception) {
}
}
}
}
fun showNotificationPermissionPrompt(showAnyway: Boolean = false) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val notificationPermission = Manifest.permission.POST_NOTIFICATIONS

View file

@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import dev.yokai.domain.ComposableAlertDialog
import dev.yokai.presentation.settings.ComposableSettings
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface {
override fun getTitle(): String? = __getTitle()
@ -14,6 +18,11 @@ abstract class SettingsComposeController: BaseComposeController(), SettingsContr
@Composable
override fun ScreenContent() {
getComposableSettings().Content()
CompositionLocalProvider(
LocalAlertDialog provides ComposableAlertDialog(null),
LocalBackPress provides router::handleBack,
) {
getComposableSettings().Content()
}
}
}

View file

@ -24,6 +24,7 @@ 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.extension.ExtensionManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.bindTo
@ -55,6 +56,7 @@ class SettingsDataLegacyController : SettingsLegacyController() {
private var backupFlags: BackupOptions = BackupOptions()
internal val storagePreferences: StoragePreferences by injectLazy()
internal val storageManager: StorageManager by injectLazy()
internal val extensionManager: ExtensionManager by injectLazy()
private val coverCache: CoverCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy()
@ -113,7 +115,9 @@ class SettingsDataLegacyController : SettingsLegacyController() {
}
if (!BackupRestoreJob.isRunning(context)) {
(activity as? MainActivity)?.getExtensionUpdates(true)
(activity as? AppCompatActivity)?.lifecycleScope?.launchIO {
extensionManager.getExtensionUpdates(true)
}
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
storageManager.getBackupsDirectory()?.let {