feat: add patch bundle info screen (#55)

This commit is contained in:
Tyff 2023-07-24 03:27:07 +12:00 committed by GitHub
parent 1331479072
commit 21d99a1f24
19 changed files with 892 additions and 312 deletions

View File

@ -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)

View File

@ -59,4 +59,5 @@ fun AppTopBar(
containerColor = containerColor
)
)
}
}

View File

@ -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()
}
}
}

View File

@ -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)
)
}
}

View File

@ -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",
)
}

View File

@ -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,
)
}

View File

@ -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))
}
},
)
}
}
}
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -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))
}
)
}
}

View File

@ -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
)
)
}

View File

@ -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<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(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,
)
}
}
}
}
}

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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<Uri?>(null) }
var integrations by rememberSaveable { mutableStateOf<Uri?>(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")
}
}
}
}
}

View File

@ -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)
}
}
)
}

View File

@ -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> = DashboardPage.values()

View File

@ -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

View File

@ -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

View File

@ -10,6 +10,12 @@
<string name="settings">Settings</string>
<string name="select_app">Select an app</string>
<string name="select_patches">Select patches</string>
<string name="import_">Import</string>
<string name="import_bundle">Import Bundle</string>
<string name="bundle_information">Bundle information</string>
<string name="bundle_patches">Bundle patches</string>
<string name="select_version">Select version</string>
<string name="general">General</string>
@ -77,9 +83,10 @@
<string name="help">Help</string>
<string name="back">Back</string>
<string name="add">Add</string>
<string name="delete">Delete</string>
<string name="close">Close</string>
<string name="system">System</string>
<string name="light">Light</string>
<string name="information">Information</string>
<string name="dark">Dark</string>
<string name="appearance">Appearance</string>
<string name="downloaded_apps">Downloaded apps</string>
@ -94,6 +101,10 @@
<string name="storage">Storage</string>
<string name="tab_apps">Apps</string>
<string name="tab_sources">Sources</string>
<string name="delete">Delete</string>
<string name="refresh">Refresh</string>
<string name="remote">Remote</string>
<string name="local">Local</string>
<string name="reload_sources">Reload all sources</string>
<string name="continue_anyways">Continue anyways</string>
<string name="download_another_version">Download another version</string>
@ -102,6 +113,9 @@
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
<string name="source_replace_integrations_fail">Failed to update integrations: %s</string>
<string name="no_patched_apps_found">No patched apps found</string>
<string name="no_patches">No patches available to view</string>
<string name="patches_available">%d Patches available, tap to view</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="unsupported_app">Unsupported app</string>
<string name="unsupported_patches">Unsupported patches</string>
<string name="universal_patches">Universal patches</string>
@ -160,6 +174,14 @@
<string name="submit_feedback_description">Help us improve this application</string>
<string name="developer_options">Developer options</string>
<string name="developer_options_description">Options for debugging issues</string>
<string name="bundle_input_name">Name</string>
<string name="bundle_input_source_url">Source URL</string>
<string name="automatically_update">Automatically update</string>
<string name="automatically_update_description">Automatically update this bundle when ReVanced starts</string>
<string name="bundle_type">Bundle type</string>
<string name="bundle_type_description">Choose the type of bundle you want</string>
<string name="patches_version">Patches version</string>
<string name="integrations_version">Integrations version</string>
<string name="about_revanced_manager">About ReVanced Manager</string>
<string name="revanced_manager_description">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.</string>
<string name="update_notification">A minor update for ReVanced Manager is available. Click here to update and get the latest features and fixes!</string>