feat: improve UX for failed or missing bundles

This commit is contained in:
Ax333l 2024-07-04 19:36:25 +02:00
parent ec0a077539
commit f99cdfe926
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
7 changed files with 114 additions and 13 deletions

View File

@ -1,21 +1,30 @@
package app.revanced.manager.ui.component.bundle package app.revanced.manager.ui.component.bundle
import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.outlined.Update import androidx.compose.material.icons.outlined.Update
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
@ -26,7 +35,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map import app.revanced.manager.ui.component.ColumnWithScrollbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -35,17 +44,18 @@ fun BundleInformationDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDeleteRequest: () -> Unit, onDeleteRequest: () -> Unit,
bundle: PatchBundleSource, bundle: PatchBundleSource,
onRefreshButton: () -> Unit, onUpdate: () -> Unit,
) { ) {
val composableScope = rememberCoroutineScope() val composableScope = rememberCoroutineScope()
var viewCurrentBundlePatches by remember { mutableStateOf(false) } var viewCurrentBundlePatches by remember { mutableStateOf(false) }
val isLocal = bundle is LocalPatchBundle val isLocal = bundle is LocalPatchBundle
val patchCount by remember(bundle) { val state by bundle.state.collectAsStateWithLifecycle()
bundle.state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
}.collectAsStateWithLifecycle(0)
val props by remember(bundle) { val props by remember(bundle) {
bundle.propsFlow() bundle.propsFlow()
}.collectAsStateWithLifecycle(null) }.collectAsStateWithLifecycle(null)
val patchCount = remember(state) {
state.patchBundleOrNull()?.patches?.size ?: 0
}
if (viewCurrentBundlePatches) { if (viewCurrentBundlePatches) {
BundlePatchesDialog( BundlePatchesDialog(
@ -70,7 +80,7 @@ fun BundleInformationDialog(
BundleTopBar( BundleTopBar(
title = bundleName, title = bundleName,
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { backIcon = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.back)
@ -86,7 +96,7 @@ fun BundleInformationDialog(
} }
} }
if (!isLocal) { if (!isLocal) {
IconButton(onClick = onRefreshButton) { IconButton(onClick = onUpdate) {
Icon( Icon(
Icons.Outlined.Update, Icons.Outlined.Update,
stringResource(R.string.refresh) stringResource(R.string.refresh)
@ -114,7 +124,95 @@ fun BundleInformationDialog(
onPatchesClick = { onPatchesClick = {
viewCurrentBundlePatches = true viewCurrentBundlePatches = true
}, },
extraFields = {
(state as? PatchBundleSource.State.Failed)?.throwable?.let {
var showDialog by rememberSaveable {
mutableStateOf(false)
}
if (showDialog) BundleErrorViewerDialog(
onDismiss = { showDialog = false },
text = remember(it) { it.stackTraceToString() }
)
BundleListItem(
headlineText = stringResource(R.string.bundle_error),
supportingText = stringResource(R.string.bundle_error_description),
trailingContent = {
Icon(
Icons.AutoMirrored.Outlined.ArrowRight,
null
)
},
modifier = Modifier.clickable { showDialog = true }
)
}
if (state is PatchBundleSource.State.Missing && !isLocal) {
BundleListItem(
headlineText = stringResource(R.string.bundle_error),
supportingText = stringResource(R.string.bundle_not_downloaded),
modifier = Modifier.clickable(onClick = onUpdate)
)
}
}
) )
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BundleErrorViewerDialog(onDismiss: () -> Unit, text: String) {
val context = LocalContext.current
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) {
Scaffold(
topBar = {
BundleTopBar(
title = stringResource(R.string.bundle_error),
onBackClick = onDismiss,
backIcon = {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
},
actions = {
IconButton(
onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_TEXT,
text
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
) {
Icon(
Icons.Outlined.Share,
contentDescription = stringResource(R.string.share)
)
}
}
)
}
) { paddingValues ->
ColumnWithScrollbar(
modifier = Modifier.padding(paddingValues)
) {
Text(text, modifier = Modifier.horizontalScroll(rememberScrollState()))
}
}
}
}

View File

@ -57,7 +57,7 @@ fun BundleItem(
onDelete() onDelete()
}, },
bundle = bundle, bundle = bundle,
onRefreshButton = onUpdate, onUpdate = onUpdate,
) )
} }

View File

@ -50,7 +50,7 @@ fun BundlePatchesDialog(
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundle_patches), title = stringResource(R.string.bundle_patches),
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { backIcon = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.back)

View File

@ -19,7 +19,7 @@ fun BundleTopBar(
onBackClick: (() -> Unit)? = null, onBackClick: (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit) = {}, actions: @Composable (RowScope.() -> Unit) = {},
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
onBackIcon: @Composable () -> Unit, backIcon: @Composable () -> Unit,
) { ) {
val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp)
@ -34,7 +34,7 @@ fun BundleTopBar(
navigationIcon = { navigationIcon = {
if (onBackClick != null) { if (onBackClick != null) {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
onBackIcon() backIcon()
} }
} }
}, },

View File

@ -81,7 +81,7 @@ fun ImportBundleDialog(
BundleTopBar( BundleTopBar(
title = stringResource(R.string.import_bundle), title = stringResource(R.string.import_bundle),
onBackClick = onDismissRequest, onBackClick = onDismissRequest,
onBackIcon = { backIcon = {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.close) contentDescription = stringResource(R.string.close)

View File

@ -131,7 +131,7 @@ fun DashboardScreen(
BundleTopBar( BundleTopBar(
title = stringResource(R.string.bundles_selected, vm.selectedSources.size), title = stringResource(R.string.bundles_selected, vm.selectedSources.size),
onBackClick = vm::cancelSourceSelection, onBackClick = vm::cancelSourceSelection,
onBackIcon = { backIcon = {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.back) contentDescription = stringResource(R.string.back)

View File

@ -25,6 +25,8 @@
<string name="bundle_missing">Missing</string> <string name="bundle_missing">Missing</string>
<string name="bundle_error">Error</string> <string name="bundle_error">Error</string>
<string name="bundle_error_description">Bundle could not be loaded. Click to view the error</string>
<string name="bundle_not_downloaded">Bundle has not been downloaded. Click here to download it</string>
<string name="bundle_name_default">Default</string> <string name="bundle_name_default">Default</string>
<string name="bundle_name_fallback">Unnamed</string> <string name="bundle_name_fallback">Unnamed</string>
@ -121,6 +123,7 @@
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="dialog_input_placeholder">Value</string> <string name="dialog_input_placeholder">Value</string>
<string name="reset">Reset</string> <string name="reset">Reset</string>
<string name="share">Share</string>
<string name="patch">Patch</string> <string name="patch">Patch</string>
<string name="select_from_storage">Select from storage</string> <string name="select_from_storage">Select from storage</string>
<string name="select_from_storage_description">Select an APK file from storage using file picker</string> <string name="select_from_storage_description">Select an APK file from storage using file picker</string>