diff --git a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt new file mode 100644 index 00000000..0ca22d44 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt @@ -0,0 +1,150 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) +@Composable +fun AlertDialogExtended( + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, + confirmButton: @Composable () -> Unit, + dismissButton: @Composable (() -> Unit)? = null, + tertiaryButton: @Composable (() -> Unit)? = null, + icon: @Composable (() -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + text: @Composable (() -> Unit)? = null, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = AlertDialogDefaults.containerColor, + iconContentColor: Color = AlertDialogDefaults.iconContentColor, + titleContentColor: Color = AlertDialogDefaults.titleContentColor, + textContentColor: Color = AlertDialogDefaults.textContentColor, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, + textHorizontalPadding: PaddingValues = PaddingValues(horizontal = 24.dp) +) { + AlertDialog(onDismissRequest = onDismissRequest) { + Surface( + modifier = modifier, + shape = shape, + color = containerColor, + tonalElevation = tonalElevation, + ) { + Column(modifier = Modifier.padding(vertical = 24.dp)) { + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + icon?.let { + ContentStyle(color = iconContentColor) { + Box( + Modifier + .padding(bottom = 16.dp) + .align(Alignment.CenterHorizontally) + ) { + icon() + } + } + } + title?.let { + ContentStyle( + color = titleContentColor, + textStyle = MaterialTheme.typography.headlineSmall + ) { + Box( + // Align the title to the center when an icon is present. + Modifier + .padding(bottom = 16.dp) + .align( + if (icon == null) { + Alignment.Start + } else { + Alignment.CenterHorizontally + } + ) + ) { + title() + } + } + } + } + text?.let { + ContentStyle( + color = textContentColor, + textStyle = MaterialTheme.typography.bodyMedium + ) { + Box( + Modifier + .weight(weight = 1f, fill = false) + .padding(bottom = 24.dp) + .padding(textHorizontalPadding) + .align(Alignment.Start) + ) { + text() + } + } + } + Box( + modifier = Modifier + .padding(horizontal = 24.dp) + ) { + ContentStyle( + color = MaterialTheme.colorScheme.primary, + textStyle = MaterialTheme.typography.labelLarge + ) { + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + 12.dp, + if (tertiaryButton != null) Alignment.Start else Alignment.End + ), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + tertiaryButton?.let { + it() + Spacer(modifier = Modifier.weight(1f)) + } + dismissButton?.invoke() + confirmButton() + } + } + } + } + } + } +} + +@Composable +private fun ContentStyle( + color: Color = LocalContentColor.current, + textStyle: TextStyle = LocalTextStyle.current, + content: @Composable () -> Unit +) { + CompositionLocalProvider(LocalContentColor provides color) { + ProvideTextStyle(textStyle) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt index d5bc9c03..b6bb3e3a 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import app.revanced.manager.R +import app.revanced.manager.ui.model.BundleType import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.JAR_MIMETYPE @@ -35,18 +36,19 @@ import app.revanced.manager.util.JAR_MIMETYPE fun ImportBundleDialog( onDismissRequest: () -> Unit, onRemoteSubmit: (String, String, Boolean) -> Unit, - onLocalSubmit: (String, Uri, Uri?) -> Unit + onLocalSubmit: (String, Uri, Uri?) -> Unit, + bundleType: BundleType ) { var name by rememberSaveable { mutableStateOf("") } var remoteUrl by rememberSaveable { mutableStateOf("") } var autoUpdate by rememberSaveable { mutableStateOf(true) } - var isLocal by rememberSaveable { mutableStateOf(false) } + var bundleType by rememberSaveable { mutableStateOf(bundleType) } var patchBundle by rememberSaveable { mutableStateOf(null) } var integrations by rememberSaveable { mutableStateOf(null) } val inputsAreValid by remember { derivedStateOf { - name.isNotEmpty() && if (isLocal) patchBundle != null else remoteUrl.isNotEmpty() + name.isNotEmpty() && if (bundleType == BundleType.Local) patchBundle != null else remoteUrl.isNotEmpty() } } @@ -88,14 +90,13 @@ fun ImportBundleDialog( TextButton( enabled = inputsAreValid, onClick = { - if (isLocal) { - onLocalSubmit(name, patchBundle!!, integrations) - } else { - onRemoteSubmit( + when (bundleType) { + BundleType.Local -> onLocalSubmit( name, - remoteUrl, - autoUpdate + patchBundle!!, + integrations ) + BundleType.Remote -> onRemoteSubmit(name, remoteUrl, autoUpdate) } }, modifier = Modifier.padding(end = 16.dp) @@ -111,17 +112,22 @@ fun ImportBundleDialog( isDefault = false, name = name, onNameChange = { name = it }, - remoteUrl = remoteUrl.takeUnless { isLocal }, + remoteUrl = remoteUrl.takeUnless { bundleType == BundleType.Local }, onRemoteUrlChange = { remoteUrl = it }, patchCount = 0, version = null, autoUpdate = autoUpdate, onAutoUpdateChange = { autoUpdate = it }, onPatchesClick = {}, - onBundleTypeClick = { isLocal = !isLocal }, + onBundleTypeClick = { + bundleType = when (bundleType) { + BundleType.Local -> BundleType.Remote + BundleType.Remote -> BundleType.Local + } + }, ) { - if (!isLocal) return@BaseBundleDialog - + if (bundleType == BundleType.Remote) return@BaseBundleDialog + BundleListItem( headlineText = stringResource(R.string.patch_bundle_field), supportingText = stringResource(if (patchBundle != null) R.string.file_field_set else R.string.file_field_not_set), @@ -160,4 +166,4 @@ fun ImportBundleDialog( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt new file mode 100644 index 00000000..6f741129 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleTypeSelectorDialog.kt @@ -0,0 +1,95 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.ListItem +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import app.revanced.manager.ui.component.AlertDialogExtended +import app.revanced.manager.ui.model.BundleType + +@Composable +fun ImportBundleTypeSelectorDialog( + onDismiss: () -> Unit, + onConfirm: (BundleType) -> Unit, +) { + var bundleType: BundleType by rememberSaveable { mutableStateOf(BundleType.Remote) } + + AlertDialogExtended( + onDismissRequest = onDismiss, + confirmButton = { + TextButton( + onClick = { onConfirm(bundleType) } + ) { + Text(stringResource(R.string.select)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + title = { + Text(stringResource(R.string.select_bundle_type_dialog_title)) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResource(R.string.select_bundle_type_dialog_description) + ) + Column { + ListItem( + modifier = Modifier.clickable( + role = Role.RadioButton, + onClick = { bundleType = BundleType.Remote } + ), + headlineContent = { Text(stringResource(R.string.remote)) }, + overlineContent = { Text(stringResource(R.string.recommended)) }, + supportingContent = { Text(stringResource(R.string.remote_bundle_description)) }, + leadingContent = { + RadioButton( + selected = bundleType == BundleType.Remote, + onClick = null + ) + } + ) + Divider(modifier = Modifier.padding(horizontal = 16.dp)) + ListItem( + modifier = Modifier.clickable( + role = Role.RadioButton, + onClick = { bundleType = BundleType.Local } + ), + headlineContent = { Text(stringResource(R.string.local)) }, + supportingContent = { Text(stringResource(R.string.local_bundle_description)) }, + overlineContent = { }, // we're using this parameter to force the 3-line ListItem state + leadingContent = { + RadioButton( + selected = bundleType == BundleType.Local, + onClick = null + ) + } + ) + } + } + }, + textHorizontalPadding = PaddingValues(0.dp) + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt index fd048e4b..91f2e9d7 100644 --- a/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt +++ b/app/src/main/java/app/revanced/manager/ui/model/BundleInfo.kt @@ -79,4 +79,9 @@ data class BundleInfo( } } } +} + +enum class BundleType { + Local, + Remote } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 8bd2cbaa..dfe82a74 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -15,7 +15,16 @@ import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Source -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -32,12 +41,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R -import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.data.room.apps.installed.InstalledApp +import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleTopBar import app.revanced.manager.ui.component.bundle.ImportBundleDialog +import app.revanced.manager.ui.component.bundle.ImportBundleTypeSelectorDialog +import app.revanced.manager.ui.model.BundleType import app.revanced.manager.ui.viewmodel.DashboardViewModel import app.revanced.manager.util.toast import kotlinx.coroutines.launch @@ -59,7 +70,8 @@ fun DashboardScreen( onSettingsClick: () -> Unit, onAppClick: (InstalledApp) -> Unit ) { - var showImportBundleDialog by rememberSaveable { mutableStateOf(false) } + var showBundleTypeSelectorDialog by rememberSaveable { mutableStateOf(false) } + var selectedBundleType: BundleType? by rememberSaveable { mutableStateOf(null) } val bundlesSelectable by remember { derivedStateOf { vm.selectedSources.size > 0 } } val pages: Array = DashboardPage.values() @@ -78,9 +90,19 @@ fun DashboardScreen( if (pagerState.currentPage != DashboardPage.BUNDLES.ordinal) vm.cancelSourceSelection() } - if (showImportBundleDialog) { + if (showBundleTypeSelectorDialog) { + ImportBundleTypeSelectorDialog( + onDismiss = { showBundleTypeSelectorDialog = false }, + onConfirm = { + selectedBundleType = it + showBundleTypeSelectorDialog = false + } + ) + } + + selectedBundleType?.let { fun dismiss() { - showImportBundleDialog = false + selectedBundleType = null } ImportBundleDialog( @@ -93,6 +115,7 @@ fun DashboardScreen( dismiss() vm.createRemoteSource(name, url, autoUpdate) }, + bundleType = it ) } @@ -165,7 +188,7 @@ fun DashboardScreen( } DashboardPage.BUNDLES.ordinal -> { - showImportBundleDialog = true + showBundleTypeSelectorDialog = true } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1cdfaf2..d7c418ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -315,4 +315,10 @@ You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue? Download update? No contributors found + Select + Select bundle type + Select the type that is right for you. + Import local files from your storage, does not automatically update + Import remote files from a URL, can automatically update + Recommended \ No newline at end of file