diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index dbf2f100..96d4e50b 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -3,6 +3,7 @@ package app.revanced.manager.domain.manager import android.content.Context import app.revanced.manager.domain.manager.base.BasePreferencesManager import app.revanced.manager.ui.theme.Theme +import app.revanced.manager.util.isDebuggable class PreferencesManager( context: Context @@ -28,4 +29,6 @@ class PreferencesManager( val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) + + val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable) } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index b6fa2d33..24b756d2 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -1,5 +1,6 @@ package app.revanced.manager.ui.screen +import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -7,55 +8,79 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.model.navigation.Settings +import org.koin.compose.koinInject -private val settingsSections = listOf( - Triple( - R.string.general, - R.string.general_description, - Icons.Outlined.Settings - ) to Settings.General, - Triple( - R.string.updates, - R.string.updates_description, - Icons.Outlined.Update - ) to Settings.Updates, - Triple( - R.string.downloads, - R.string.downloads_description, - Icons.Outlined.Download - ) to Settings.Downloads, - Triple( - R.string.import_export, - R.string.import_export_description, - Icons.Outlined.SwapVert - ) to Settings.ImportExport, - Triple( - R.string.advanced, - R.string.advanced_description, - Icons.Outlined.Tune - ) to Settings.Advanced, - Triple( - R.string.developer_options, - R.string.developer_options_description, - Icons.Outlined.Code - ) to Settings.Developer, - Triple( - R.string.about, - R.string.app_name, - Icons.Outlined.Info - ) to Settings.About, +private data class Section( + @StringRes val name: Int, + @StringRes val description: Int, + val image: ImageVector, + val destination: Settings.Destination, ) @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> Unit) { + val prefs: PreferencesManager = koinInject() + val showDeveloperSettings by prefs.showDeveloperSettings.getAsState() + + val settingsSections = remember(showDeveloperSettings) { + listOfNotNull( + Section( + R.string.general, + R.string.general_description, + Icons.Outlined.Settings, + Settings.General + ), + Section( + R.string.updates, + R.string.updates_description, + Icons.Outlined.Update, + Settings.Updates + ), + Section( + R.string.downloads, + R.string.downloads_description, + Icons.Outlined.Download, + Settings.Downloads + ), + Section( + R.string.import_export, + R.string.import_export_description, + Icons.Outlined.SwapVert, + Settings.ImportExport + ), + Section( + R.string.advanced, + R.string.advanced_description, + Icons.Outlined.Tune, + Settings.Advanced + ), + Section( + R.string.about, + R.string.app_name, + Icons.Outlined.Info, + Settings.About + ), + Section( + R.string.developer_options, + R.string.developer_options_description, + Icons.Outlined.Code, + Settings.Developer + ).takeIf { showDeveloperSettings } + ) + } + Scaffold( topBar = { AppTopBar( @@ -69,12 +94,12 @@ fun SettingsScreen(onBackClick: () -> Unit, navigate: (Settings.Destination) -> .padding(paddingValues) .fillMaxSize() ) { - settingsSections.forEach { (titleDescIcon, destination) -> + settingsSections.forEach { (name, description, icon, destination) -> SettingsListItem( modifier = Modifier.clickable { navigate(destination) }, - headlineContent = stringResource(titleDescIcon.first), - supportingContent = stringResource(titleDescIcon.second), - leadingContent = { Icon(titleDescIcon.third, null) } + headlineContent = stringResource(name), + supportingContent = stringResource(description), + leadingContent = { Icon(icon, null) } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt index 63a9e9c3..80fc6636 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt @@ -23,16 +23,26 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import app.revanced.manager.BuildConfig import app.revanced.manager.R @@ -42,6 +52,7 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.model.navigation.Settings import app.revanced.manager.ui.viewmodel.AboutViewModel +import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.DEVELOPER_OPTIONS_TAPS import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon import app.revanced.manager.util.openUrl import app.revanced.manager.util.toast @@ -109,7 +120,8 @@ fun AboutSettingsScreen( } val listItems = listOfNotNull( - Triple(stringResource(R.string.submit_feedback), + Triple( + stringResource(R.string.submit_feedback), stringResource(R.string.submit_feedback_description), third = { context.openUrl("https://github.com/ReVanced/revanced-manager/issues/new/choose") @@ -134,6 +146,35 @@ fun AboutSettingsScreen( ) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackbarHostState = remember { SnackbarHostState() } + + val showDeveloperSettings by viewModel.showDeveloperSettings.getAsState() + var developerTaps by rememberSaveable { mutableIntStateOf(0) } + LaunchedEffect(developerTaps) { + if (developerTaps == 0) return@LaunchedEffect + if (showDeveloperSettings) { + snackbarHostState.showSnackbar(context.getString(R.string.developer_options_already_enabled)) + developerTaps = 0 + return@LaunchedEffect + } + + val remaining = DEVELOPER_OPTIONS_TAPS - developerTaps + if (remaining > 0) { + snackbarHostState.showSnackbar( + context.getString( + R.string.developer_options_taps, + remaining + ), + duration = SnackbarDuration.Long + ) + } else if (remaining == 0) { + viewModel.showDeveloperSettings.update(true) + snackbarHostState.showSnackbar(context.getString(R.string.developer_options_enabled)) + } + + // Reset the counter + developerTaps = 0 + } Scaffold( topBar = { @@ -143,6 +184,9 @@ fun AboutSettingsScreen( onBackClick = onBackClick ) }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> ColumnWithScrollbar( @@ -153,9 +197,11 @@ fun AboutSettingsScreen( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Image( - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier + .padding(top = 16.dp) + .clickable { developerTaps += 1 }, painter = icon, - contentDescription = null + contentDescription = stringResource(R.string.app_name) ) Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -163,7 +209,11 @@ fun AboutSettingsScreen( ) { Text( stringResource(R.string.app_name), - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.semantics { + // Icon already has this information for the purpose of being clickable. + hideFromAccessibility() + } ) Text( text = stringResource(R.string.version) + " " + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")", diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt index 6139ced7..a6164ec8 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperSettingsScreen.kt @@ -12,11 +12,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import app.revanced.manager.R +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -25,6 +28,7 @@ fun DeveloperSettingsScreen( vm: DeveloperOptionsViewModel = koinViewModel() ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val prefs: PreferencesManager = koinInject() Scaffold( topBar = { @@ -37,6 +41,13 @@ fun DeveloperSettingsScreen( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { + GroupHeader(stringResource(R.string.manager)) + BooleanItem( + preference = prefs.showDeveloperSettings, + headline = R.string.developer_options, + description = R.string.developer_options_description, + ) + GroupHeader(stringResource(R.string.patch_bundles_section)) SettingsListItem( headlineContent = stringResource(R.string.patch_bundles_force_download), diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt index c919152a..037cb9d6 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AboutViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.data.platform.NetworkInfo +import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.network.api.ReVancedAPI import app.revanced.manager.network.dto.ReVancedDonationLink import app.revanced.manager.network.dto.ReVancedSocial @@ -27,6 +28,7 @@ import kotlinx.coroutines.withContext class AboutViewModel( private val reVancedAPI: ReVancedAPI, private val network: NetworkInfo, + prefs: PreferencesManager, ) : ViewModel() { var socials by mutableStateOf(emptyList()) private set @@ -37,6 +39,8 @@ class AboutViewModel( val isConnected: Boolean get() = network.isConnected() + val showDeveloperSettings = prefs.showDeveloperSettings + init { viewModelScope.launch { if (!isConnected) { @@ -53,6 +57,8 @@ class AboutViewModel( } companion object { + const val DEVELOPER_OPTIONS_TAPS = 5 + private val socialIcons = mapOf( "Discord" to FontAwesomeIcons.Brands.Discord, "GitHub" to FontAwesomeIcons.Brands.Github, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc28a580..b4205802 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -348,6 +348,9 @@ About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. + %d taps remaining + Developer options enabled + Developer options are already enabled An update is available Current version: %s New version: %s