diff --git a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt index 7002886aa2..e7c6bcc970 100644 --- a/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/sources/RemoteSource.kt @@ -10,7 +10,6 @@ import java.io.File @Stable class RemoteSource(name: String, id: Int, directory: File) : Source(name, id, directory) { private val api: ManagerAPI = get() - suspend fun downloadLatest() = withContext(Dispatchers.IO) { api.downloadBundle(patchesJar, integrations).also { (patchesVer, integrationsVer) -> saveVersion(patchesVer, integrationsVer) diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt b/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt index ffb466dec5..98872a9ec6 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppScaffold.kt @@ -59,4 +59,5 @@ fun AppTopBar( containerColor = containerColor ) ) -} \ No newline at end of file +} + diff --git a/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt new file mode 100644 index 0000000000..4c23afd477 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/NotificationCard.kt @@ -0,0 +1,57 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun NotificationCard( + color: Color, + icon: ImageVector, + text: String, + content: @Composable () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(28.dp)) + .background(color) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + 16.dp, + Alignment.Start + ) + ) { + Icon( + imageVector = icon, + contentDescription = null, + ) + Text( + modifier = Modifier.width(220.dp), + text = text, + style = MaterialTheme.typography.bodyMedium + ) + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt b/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt new file mode 100644 index 0000000000..c66457cfe2 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SourceItem.kt @@ -0,0 +1,96 @@ +package app.revanced.manager.ui.component + + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.sources.RemoteSource +import app.revanced.manager.domain.sources.Source +import app.revanced.manager.ui.component.bundle.BundleInformationDialog +import app.revanced.manager.ui.viewmodel.SourcesViewModel +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@Composable +fun SourceItem( + source: Source, onDelete: () -> Unit, + coroutineScope: CoroutineScope, +) { + var viewBundleDialogPage by rememberSaveable { mutableStateOf(false) } + + val bundle by source.bundle.collectAsStateWithLifecycle() + val patchCount = bundle.patches.size + val padding = PaddingValues(16.dp, 0.dp) + + val androidContext = LocalContext.current + + if (viewBundleDialogPage) { + BundleInformationDialog( + onDismissRequest = { viewBundleDialogPage = false }, + onDeleteRequest = { + viewBundleDialogPage = false + onDelete() + }, + source = source, + patchCount = patchCount, + onRefreshButton = { + coroutineScope.launch { + uiSafe( + androidContext, + R.string.source_download_fail, + SourcesViewModel.failLogMsg + ) { + if (source is RemoteSource) source.update() + } + } + }, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .height(64.dp) + .fillMaxWidth() + .clickable { + viewBundleDialogPage = true + } + ) { + Text( + text = source.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(padding) + ) + + Spacer( + modifier = Modifier.weight(1f) + ) + + Text( + text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(padding) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt new file mode 100644 index 0000000000..0241c5e9e4 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoContent.kt @@ -0,0 +1,87 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowRight +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@Composable +fun BundleInfoContent( + switchChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + patchInfoText: String, + patchCount: Int, + onArrowClick: () -> Unit, + isLocal: Boolean, + tonalButtonOnClick: () -> Unit = {}, + tonalButtonContent: @Composable RowScope.() -> Unit, +) { + if(!isLocal) { + BundleInfoListItem( + headlineText = stringResource(R.string.automatically_update), + supportingText = stringResource(R.string.automatically_update_description), + trailingContent = { + Switch( + checked = switchChecked, + onCheckedChange = onCheckedChange + ) + } + ) + } + + BundleInfoListItem( + headlineText = stringResource(R.string.bundle_type), + supportingText = stringResource(R.string.bundle_type_description) + ) { + FilledTonalButton( + onClick = tonalButtonOnClick, + content = tonalButtonContent, + ) + } + + Text( + text = stringResource(R.string.information), + modifier = Modifier.padding( + horizontal = 16.dp, + vertical = 12.dp + ), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + + BundleInfoListItem( + headlineText = stringResource(R.string.patches), + supportingText = patchInfoText, + trailingContent = { + if (patchCount > 0) { + IconButton(onClick = onArrowClick) { + Icon( + Icons.Outlined.ArrowRight, + stringResource(R.string.patches) + ) + } + } + } + ) + + BundleInfoListItem( + headlineText = stringResource(R.string.patches_version), + supportingText = "1.0.0", + ) + + BundleInfoListItem( + headlineText = stringResource(R.string.integrations_version), + supportingText = "1.0.0", + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt new file mode 100644 index 0000000000..1867570076 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInfoListItem.kt @@ -0,0 +1,30 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable + +@Composable +fun BundleInfoListItem( + headlineText: String, + supportingText: String = "", + trailingContent: @Composable (() -> Unit)? = null, +) { + ListItem( + headlineContent = { + Text( + text = headlineText, + style = MaterialTheme.typography.titleLarge + ) + }, + supportingContent = { + Text( + text = supportingText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline + ) + }, + trailingContent = trailingContent, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt new file mode 100644 index 0000000000..ead9650dad --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -0,0 +1,143 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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.domain.sources.LocalSource +import app.revanced.manager.domain.sources.RemoteSource +import app.revanced.manager.domain.sources.Source + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundleInformationDialog( + onDismissRequest: () -> Unit, + onDeleteRequest: () -> Unit, + source: Source, + remoteName: String = "", + patchCount: Int = 0, + onRefreshButton: () -> Unit, +) { + var checked by remember { mutableStateOf(true) } + var viewCurrentBundlePatches by remember { mutableStateOf(false) } + + val isLocal = source is LocalSource + + val patchInfoText = if (patchCount == 0) stringResource(R.string.no_patches) + else stringResource(R.string.patches_available, patchCount) + + if (viewCurrentBundlePatches) { + BundlePatchesDialog( + onDismissRequest = { + viewCurrentBundlePatches = false + }, + source = source, + ) + } + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.bundle_information), + onBackClick = onDismissRequest, + onBackIcon = { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + }, + actions = { + IconButton(onClick = onDeleteRequest) { + Icon( + Icons.Outlined.DeleteOutline, + stringResource(R.string.delete) + ) + } + if(!isLocal) { + IconButton(onClick = onRefreshButton) { + Icon( + Icons.Outlined.Refresh, + stringResource(R.string.refresh) + ) + } + } + } + ) + }, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Column( + modifier = Modifier.padding( + start = 24.dp, + top = 16.dp, + end = 24.dp, + ) + ) { + BundleTextContent( + name = source.name, + isLocal = isLocal, + remoteUrl = remoteName, + ) + } + + Column( + Modifier.padding( + start = 8.dp, + top = 8.dp, + end = 4.dp, + ) + ) { + BundleInfoContent( + switchChecked = checked, + onCheckedChange = { checked = it }, + patchInfoText = patchInfoText, + patchCount = patchCount, + isLocal = isLocal, + onArrowClick = { + viewCurrentBundlePatches = true + }, + tonalButtonContent = { + when(source) { + is RemoteSource -> Text(stringResource(R.string.remote)) + is LocalSource -> Text(stringResource(R.string.local)) + } + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt new file mode 100644 index 0000000000..3dd1fb30db --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -0,0 +1,112 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Lightbulb +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import app.revanced.manager.R +import app.revanced.manager.domain.sources.Source +import app.revanced.manager.ui.component.NotificationCard + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundlePatchesDialog( + onDismissRequest: () -> Unit, + source: Source, +) { + var informationCardVisible by remember { mutableStateOf(true) } + val bundle by source.bundle.collectAsStateWithLifecycle() + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.bundle_patches), + onBackClick = onDismissRequest, + onBackIcon = { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + }, + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .padding(16.dp) + ) { + item { + AnimatedVisibility(visible = informationCardVisible) { + NotificationCard( + color = MaterialTheme.colorScheme.secondaryContainer, + icon = Icons.Outlined.Lightbulb, + text = stringResource(R.string.tap_on_patches) + ) { + IconButton(onClick = { informationCardVisible = false }) { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.close), + ) + } + } + } + } + + items(bundle.patches.size) { bundleIndex -> + val patch = bundle.patches[bundleIndex] + ListItem( + headlineContent = { + Text( + text = patch.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + }, + supportingContent = { + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + ) + Divider() + } + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt new file mode 100644 index 0000000000..26940e7a8e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTextContent.kt @@ -0,0 +1,43 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@Composable +fun BundleTextContent( + name: String, + onNameChange: (String) -> Unit = {}, + isLocal: Boolean, + remoteUrl: String, + onRemoteUrlChange: (String) -> Unit = {}, +) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = name, + onValueChange = onNameChange, + label = { + Text(stringResource(R.string.bundle_input_name)) + } + ) + if (!isLocal) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = remoteUrl, + onValueChange = onRemoteUrlChange, + label = { + Text(stringResource(R.string.bundle_input_source_url)) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt new file mode 100644 index 0000000000..7f98f01434 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleTopBar.kt @@ -0,0 +1,46 @@ +package app.revanced.manager.ui.component.bundle + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BundleTopBar( + title: String, + onBackClick: (() -> Unit)? = null, + actions: @Composable (RowScope.() -> Unit) = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, + onBackIcon: @Composable () -> Unit, +) { + val containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.0.dp) + + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + }, + scrollBehavior = scrollBehavior, + navigationIcon = { + if (onBackClick != null) { + IconButton(onClick = onBackClick) { + onBackIcon() + } + } + }, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = containerColor + ) + ) +} \ 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 new file mode 100644 index 0000000000..5dc6232864 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/ImportBundleDialog.kt @@ -0,0 +1,214 @@ +package app.revanced.manager.ui.component.bundle + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Topic +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import app.revanced.manager.R +import app.revanced.manager.util.APK_MIMETYPE +import app.revanced.manager.util.JAR_MIMETYPE +import app.revanced.manager.util.parseUrlOrNull +import io.ktor.http.Url + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ImportBundleDialog( + onDismissRequest: () -> Unit, + onRemoteSubmit: (String, Url) -> Unit, + onLocalSubmit: (String, Uri, Uri?) -> Unit, + patchCount: Int = 0, +) { + var name by rememberSaveable { mutableStateOf("") } + var remoteUrl by rememberSaveable { mutableStateOf("") } + var checked by remember { mutableStateOf(true) } + var isLocal by rememberSaveable { mutableStateOf(false) } + var patchBundle by rememberSaveable { mutableStateOf(null) } + var integrations by rememberSaveable { mutableStateOf(null) } + + val patchBundleText = patchBundle?.toString().orEmpty() + val integrationText = integrations?.toString().orEmpty() + + val inputsAreValid by remember { + derivedStateOf { + val nameSize = name.length + nameSize in 4..19 && if (isLocal) patchBundle != null else { + remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null + } + } + } + + val patchActivityLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { patchBundle = it } + } + + val integrationsActivityLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { integrations = it } + } + + val onPatchLauncherClick = { + patchActivityLauncher.launch(JAR_MIMETYPE) + } + + val onIntegrationLauncherClick = { + integrationsActivityLauncher.launch(APK_MIMETYPE) + } + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + dismissOnBackPress = true + ) + ) { + Scaffold( + topBar = { + BundleTopBar( + title = stringResource(R.string.import_bundle), + onBackClick = onDismissRequest, + onBackIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.close) + ) + }, + actions = { + Text( + text = stringResource(R.string.import_), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(end = 16.dp) + .clickable { + if (inputsAreValid) { + if (isLocal) { + onLocalSubmit(name, patchBundle!!, integrations) + } else { + onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!) + } + } + } + ) + } + ) + }, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Column( + modifier = Modifier.padding( + start = 24.dp, + top = 16.dp, + end = 24.dp, + ) + ) { + BundleTextContent( + name = name, + onNameChange = { name = it }, + isLocal = isLocal, + remoteUrl = remoteUrl, + onRemoteUrlChange = { remoteUrl = it }, + ) + + if(isLocal) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = patchBundleText, + onValueChange = {}, + label = { + Text("Patches Source File") + }, + trailingIcon = { + IconButton( + onClick = onPatchLauncherClick + ) { + Icon( + imageVector = Icons.Default.Topic, + contentDescription = null + ) + } + } + ) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + value = integrationText, + onValueChange = {}, + label = { + Text("Integrations Source File") + }, + trailingIcon = { + IconButton(onClick = onIntegrationLauncherClick) { + Icon( + imageVector = Icons.Default.Topic, + contentDescription = null + ) + } + } + ) + } + } + + Column( + Modifier.padding( + start = 8.dp, + top = 8.dp, + end = 4.dp, + ) + ) { + BundleInfoContent( + switchChecked = checked, + onCheckedChange = { checked = it }, + patchInfoText = stringResource(R.string.no_patches), + patchCount = patchCount, + onArrowClick = {}, + tonalButtonContent = { + if (isLocal) { + Text(stringResource(R.string.local)) + } else { + Text(stringResource(R.string.remote)) + } + }, + tonalButtonOnClick = { isLocal = !isLocal }, + isLocal = isLocal, + ) + } + } + } + } +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/SourceSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt similarity index 97% rename from app/src/main/java/app/revanced/manager/ui/component/sources/SourceSelector.kt rename to app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt index 2fd0e527df..ce190c8239 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/SourceSelector.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/SourceSelector.kt @@ -1,4 +1,4 @@ -package app.revanced.manager.ui.component.sources +package app.revanced.manager.ui.component.bundle import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt deleted file mode 100644 index de90fc7fce..0000000000 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.revanced.manager.ui.component.sources - -import android.net.Uri -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import app.revanced.manager.ui.component.ContentSelector -import app.revanced.manager.util.APK_MIMETYPE -import app.revanced.manager.util.JAR_MIMETYPE - -@Composable -fun LocalBundleSelectors(onPatchesSelection: (Uri) -> Unit, onIntegrationsSelection: (Uri) -> Unit) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - ContentSelector( - mime = JAR_MIMETYPE, - onSelect = onPatchesSelection - ) { - Text("Patches") - } - - ContentSelector( - mime = APK_MIMETYPE, - onSelect = onIntegrationsSelection - ) { - Text("Integrations") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/NewSourceDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/NewSourceDialog.kt deleted file mode 100644 index 170598243c..0000000000 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/NewSourceDialog.kt +++ /dev/null @@ -1,101 +0,0 @@ -package app.revanced.manager.ui.component.sources - -import android.net.Uri -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import app.revanced.manager.R -import app.revanced.manager.util.parseUrlOrNull -import io.ktor.http.* - -@Composable -fun NewSourceDialog( - onDismissRequest: () -> Unit, - onRemoteSubmit: (String, Url) -> Unit, - onLocalSubmit: (String, Uri, Uri?) -> Unit -) { - Dialog( - onDismissRequest = onDismissRequest, - properties = DialogProperties( - usePlatformDefaultWidth = false, - dismissOnBackPress = true - ) - ) { - Surface(modifier = Modifier.fillMaxSize()) { - Column { - IconButton(onClick = onDismissRequest) { - Icon(Icons.Filled.Cancel, stringResource(R.string.cancel)) - } - var isLocal by rememberSaveable { mutableStateOf(false) } - var patchBundle by rememberSaveable { mutableStateOf(null) } - var integrations by rememberSaveable { mutableStateOf(null) } - var remoteUrl by rememberSaveable { mutableStateOf("") } - - var name by rememberSaveable { mutableStateOf("") } - - val inputsAreValid by remember { - derivedStateOf { - val nameSize = name.length - - nameSize in 4..19 && if (isLocal) patchBundle != null else { - remoteUrl.isNotEmpty() && remoteUrl.parseUrlOrNull() != null - } - } - } - - LaunchedEffect(isLocal) { - integrations = null - patchBundle = null - remoteUrl = "" - } - - Text(text = if (isLocal) "Local" else "Remote") - Switch(checked = isLocal, onCheckedChange = { isLocal = it }) - - TextField( - value = name, - onValueChange = { name = it }, - label = { - Text("Name") - } - ) - - if (isLocal) { - LocalBundleSelectors( - onPatchesSelection = { patchBundle = it }, - onIntegrationsSelection = { integrations = it }, - ) - } else { - TextField( - value = remoteUrl, - onValueChange = { remoteUrl = it }, - label = { - Text("API Url") - } - ) - } - - Button( - onClick = { - if (isLocal) { - onLocalSubmit(name, patchBundle!!, integrations) - } else { - onRemoteSubmit(name, remoteUrl.parseUrlOrNull()!!) - } - }, - enabled = inputsAreValid - ) { - Text("Save") - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/SourceItem.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/SourceItem.kt deleted file mode 100644 index 9d4be93296..0000000000 --- a/app/src/main/java/app/revanced/manager/ui/component/sources/SourceItem.kt +++ /dev/null @@ -1,155 +0,0 @@ -package app.revanced.manager.ui.component.sources - -import android.net.Uri -import androidx.annotation.StringRes -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import app.revanced.manager.R -import app.revanced.manager.domain.sources.LocalSource -import app.revanced.manager.domain.sources.RemoteSource -import app.revanced.manager.domain.sources.Source -import app.revanced.manager.ui.viewmodel.SourcesViewModel -import app.revanced.manager.util.uiSafe -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.io.InputStream - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SourceItem(source: Source, onDelete: () -> Unit, coroutineScope: CoroutineScope) { - val composableScope = rememberCoroutineScope() - var sheetActive by rememberSaveable { mutableStateOf(false) } - - val bundle by source.bundle.collectAsStateWithLifecycle() - val patchCount = bundle.patches.size - val padding = PaddingValues(16.dp, 0.dp) - - if (sheetActive) { - val modalSheetState = rememberModalBottomSheetState( - confirmValueChange = { it != SheetValue.PartiallyExpanded }, - skipPartiallyExpanded = true - ) - - ModalBottomSheet( - sheetState = modalSheetState, - onDismissRequest = { sheetActive = false } - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = source.name, - style = MaterialTheme.typography.titleLarge - ) - - when (source) { - is RemoteSource -> RemoteSourceItem(source, coroutineScope) - is LocalSource -> LocalSourceItem(source, coroutineScope) - } - - Button( - onClick = { - composableScope.launch { - modalSheetState.hide() - sheetActive = false - onDelete() - } - } - ) { - Text("Delete this source") - } - } - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .height(64.dp) - .fillMaxWidth() - .clickable { - sheetActive = true - } - ) { - Text( - text = source.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(padding) - ) - - Spacer( - modifier = Modifier.weight(1f) - ) - - Text( - text = pluralStringResource(R.plurals.patches_count, patchCount, patchCount), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(padding) - ) - } -} - -@Composable -private fun RemoteSourceItem(source: RemoteSource, coroutineScope: CoroutineScope) { - val androidContext = LocalContext.current - Text(text = "(api url here)") - - Button(onClick = { - coroutineScope.launch { - uiSafe(androidContext, R.string.source_download_fail, SourcesViewModel.failLogMsg) { - source.update() - } - } - }) { - Text(text = "Check for updates") - } -} - -@Composable -private fun LocalSourceItem(source: LocalSource, coroutineScope: CoroutineScope) { - val androidContext = LocalContext.current - val resolver = remember { androidContext.contentResolver!! } - - fun loadAndReplace( - uri: Uri, - @StringRes toastMsg: Int, - errorLogMsg: String, - callback: suspend (InputStream) -> Unit - ) = coroutineScope.launch { - uiSafe(androidContext, toastMsg, errorLogMsg) { - resolver.openInputStream(uri)!!.use { - callback(it) - } - } - } - - LocalBundleSelectors( - onPatchesSelection = { uri -> - loadAndReplace(uri, R.string.source_replace_fail, "Failed to replace patch bundle") { - source.replace(it, null) - } - }, - onIntegrationsSelection = { uri -> - loadAndReplace( - uri, - R.string.source_replace_integrations_fail, - "Failed to replace integrations" - ) { - source.replace(null, it) - } - } - ) -} \ 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 c0667643f4..af70853af7 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 @@ -12,7 +12,16 @@ import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.HelpOutline 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.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -35,7 +44,7 @@ enum class DashboardPage( @Composable fun DashboardScreen( onAppSelectorClick: () -> Unit, - onSettingsClick: () -> Unit + onSettingsClick: () -> Unit, ) { val pages: Array = DashboardPage.values() diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt index bc8f7649a1..28e822c3a6 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SourcesScreen.kt @@ -1,35 +1,44 @@ package app.revanced.manager.ui.screen -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +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.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope import app.revanced.manager.R -import app.revanced.manager.ui.component.sources.NewSourceDialog -import app.revanced.manager.ui.component.sources.SourceItem +import app.revanced.manager.ui.component.bundle.ImportBundleDialog +import app.revanced.manager.ui.component.SourceItem import app.revanced.manager.ui.viewmodel.SourcesViewModel import org.koin.androidx.compose.getViewModel @Composable -fun SourcesScreen(vm: SourcesViewModel = getViewModel()) { +fun SourcesScreen( + vm: SourcesViewModel = getViewModel(), +) { var showNewSourceDialog by rememberSaveable { mutableStateOf(false) } val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList()) - if (showNewSourceDialog) NewSourceDialog( - onDismissRequest = { showNewSourceDialog = false }, - onLocalSubmit = { name, patches, integrations -> - showNewSourceDialog = false - vm.addLocal(name, patches, integrations) - }, - onRemoteSubmit = { name, url -> - showNewSourceDialog = false - vm.addRemote(name, url) - } - ) + if (showNewSourceDialog) { + ImportBundleDialog( + onDismissRequest = { showNewSourceDialog = false }, + onLocalSubmit = { name, patches, integrations -> + showNewSourceDialog = false + vm.addLocal(name, patches, integrations) + }, + onRemoteSubmit = { name, url -> + showNewSourceDialog = false + vm.addRemote(name, url) + }, + ) + } Column( modifier = Modifier diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index e51b270e3e..17dee23d86 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -31,7 +31,7 @@ import app.revanced.manager.ui.viewmodel.ImportExportViewModel import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.GroupHeader import app.revanced.manager.ui.component.PasswordField -import app.revanced.manager.ui.component.sources.SourceSelector +import app.revanced.manager.ui.component.bundle.SourceSelector import app.revanced.manager.util.toast import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 73bf0a7483..41e6cbb47a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,12 @@ Settings Select an app Select patches + + Import + Import Bundle + Bundle information + Bundle patches + Select version General @@ -77,9 +83,10 @@ Help Back Add - Delete + Close System Light + Information Dark Appearance Downloaded apps @@ -94,6 +101,10 @@ Storage Apps Sources + Delete + Refresh + Remote + Local Reload all sources Continue anyways Download another version @@ -102,6 +113,9 @@ Failed to load updated patch bundle: %s Failed to update integrations: %s No patched apps found + No patches available to view + %d Patches available, tap to view + Tap on the patches to get more information about them Unsupported app Unsupported patches Universal patches @@ -160,6 +174,14 @@ Help us improve this application Developer options Options for debugging issues + Name + Source URL + Automatically update + Automatically update this bundle when ReVanced starts + Bundle type + Choose the type of bundle you want + Patches version + Integrations version 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. A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!