From 37e7e74c340a32a6f1c843af4365a048c58b3bf5 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Mon, 10 Jun 2024 13:49:22 +0700 Subject: [PATCH] feat: Add restore functionality to composable data settings --- .../settings/SettingsCommonWidget.kt | 9 ++- .../settings/screen/SettingsDataScreen.kt | 63 +++++++++++++++- .../settings/screen/data/AlertDialogs.kt | 75 +++++++++++++++++++ .../tachiyomi/extension/ExtensionManager.kt | 21 ++++++ .../kanade/tachiyomi/ui/main/MainActivity.kt | 31 ++------ .../ui/setting/SettingsComposeController.kt | 11 ++- .../legacy/SettingsDataLegacyController.kt | 6 +- 7 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/dev/yokai/presentation/settings/screen/data/AlertDialogs.kt 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 e9d232748e..4fd49f0976 100644 --- a/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt +++ b/app/src/main/java/dev/yokai/presentation/settings/SettingsCommonWidget.kt @@ -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, diff --git a/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt b/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt index 817ecccb3c..a66882f244 100644 --- a/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/dev/yokai/presentation/settings/screen/SettingsDataScreen.kt @@ -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() } + + 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)) diff --git a/app/src/main/java/dev/yokai/presentation/settings/screen/data/AlertDialogs.kt b/app/src/main/java/dev/yokai/presentation/settings/screen/data/AlertDialogs.kt new file mode 100644 index 0000000000..0b08dfcbcc --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/settings/screen/data/AlertDialogs.kt @@ -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) { + 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 = {}, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 2c3b9e4fed..bf1a537fe1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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]. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index cf14832e22..0511ddaad1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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() { splashScreen?.configure() - getExtensionUpdates(true) + lifecycleScope.launchIO { + extensionManager.getExtensionUpdates(true) + } preferences.extensionUpdatesCount() .changesIn(lifecycleScope) { @@ -941,7 +939,9 @@ open class MainActivity : BaseActivity() { 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() { } } - 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 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 3d445a174e..95ad8ef61e 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 @@ -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() + } } } 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 index 44a091bf40..024f123e67 100644 --- 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 @@ -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 {