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

View file

@ -1,8 +1,10 @@
package dev.yokai.presentation.settings.screen package dev.yokai.presentation.settings.screen
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.BasePreferenceWidget
import dev.yokai.presentation.component.preference.widget.PrefsHorizontalPadding import dev.yokai.presentation.component.preference.widget.PrefsHorizontalPadding
import dev.yokai.presentation.settings.ComposableSettings import dev.yokai.presentation.settings.ComposableSettings
import dev.yokai.presentation.settings.screen.data.RestoreBackup
import dev.yokai.presentation.settings.screen.data.StorageInfo import dev.yokai.presentation.settings.screen.data.StorageInfo
import eu.kanade.tachiyomi.R 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.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.e
import eu.kanade.tachiyomi.util.system.launchNonCancellable import eu.kanade.tachiyomi.util.system.launchNonCancellable
import eu.kanade.tachiyomi.util.system.toast 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 eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -104,7 +114,35 @@ object SettingsDataScreen : ComposableSettings {
@Composable @Composable
private fun getBackupAndRestoreGroup(preferences: PreferencesHelper): Preference.PreferenceGroup { private fun getBackupAndRestoreGroup(preferences: PreferencesHelper): Preference.PreferenceGroup {
val scope = rememberCoroutineScope()
val context = LocalContext.current 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( return Preference.PreferenceGroup(
title = stringResource(R.string.backup_and_restore), title = stringResource(R.string.backup_and_restore),
@ -123,7 +161,17 @@ object SettingsDataScreen : ComposableSettings {
SegmentedButton( SegmentedButton(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
checked = false, 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), shape = SegmentedButtonDefaults.itemShape(0, 2),
) { ) {
Text(stringResource(R.string.create_backup)) Text(stringResource(R.string.create_backup))
@ -131,7 +179,18 @@ object SettingsDataScreen : ComposableSettings {
SegmentedButton( SegmentedButton(
modifier = Modifier.fillMaxHeight(), modifier = Modifier.fillMaxHeight(),
checked = false, 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), shape = SegmentedButtonDefaults.itemShape(1, 2),
) { ) {
Text(stringResource(R.string.restore_backup)) 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.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.e import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -28,6 +29,7 @@ import kotlinx.parcelize.Parcelize
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.* import java.util.*
import java.util.concurrent.*
/** /**
* The manager of extensions installed as another apk which extend the available sources. It handles * 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) 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]. * 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.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.databinding.MainActivityBinding import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
@ -144,13 +143,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToLong import kotlin.math.roundToLong
@ -706,7 +702,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
splashScreen?.configure() splashScreen?.configure()
getExtensionUpdates(true) lifecycleScope.launchIO {
extensionManager.getExtensionUpdates(true)
}
preferences.extensionUpdatesCount() preferences.extensionUpdatesCount()
.changesIn(lifecycleScope) { .changesIn(lifecycleScope) {
@ -941,7 +939,9 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
checkForAppUpdates() checkForAppUpdates()
getExtensionUpdates(false) lifecycleScope.launchIO {
extensionManager.getExtensionUpdates(false)
}
setExtensionsBadge() setExtensionsBadge()
DownloadJob.callListeners(downloadManager = downloadManager) DownloadJob.callListeners(downloadManager = downloadManager)
showDLQueueTutorial() 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) { fun showNotificationPermissionPrompt(showAnyway: Boolean = false) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
val notificationPermission = Manifest.permission.POST_NOTIFICATIONS val notificationPermission = Manifest.permission.POST_NOTIFICATIONS

View file

@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import dev.yokai.domain.ComposableAlertDialog
import dev.yokai.presentation.settings.ComposableSettings import dev.yokai.presentation.settings.ComposableSettings
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController 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 { abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface {
override fun getTitle(): String? = __getTitle() override fun getTitle(): String? = __getTitle()
@ -14,6 +18,11 @@ abstract class SettingsComposeController: BaseComposeController(), SettingsContr
@Composable @Composable
override fun ScreenContent() { 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.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache 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.main.MainActivity
import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController import eu.kanade.tachiyomi.ui.setting.SettingsLegacyController
import eu.kanade.tachiyomi.ui.setting.bindTo import eu.kanade.tachiyomi.ui.setting.bindTo
@ -55,6 +56,7 @@ class SettingsDataLegacyController : SettingsLegacyController() {
private var backupFlags: BackupOptions = BackupOptions() private var backupFlags: BackupOptions = BackupOptions()
internal val storagePreferences: StoragePreferences by injectLazy() internal val storagePreferences: StoragePreferences by injectLazy()
internal val storageManager: StorageManager by injectLazy() internal val storageManager: StorageManager by injectLazy()
internal val extensionManager: ExtensionManager by injectLazy()
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy() private val chapterCache: ChapterCache by injectLazy()
@ -113,7 +115,9 @@ class SettingsDataLegacyController : SettingsLegacyController() {
} }
if (!BackupRestoreJob.isRunning(context)) { if (!BackupRestoreJob.isRunning(context)) {
(activity as? MainActivity)?.getExtensionUpdates(true) (activity as? AppCompatActivity)?.lifecycleScope?.launchIO {
extensionManager.getExtensionUpdates(true)
}
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
storageManager.getBackupsDirectory()?.let { storageManager.getBackupsDirectory()?.let {