compose: Replicate epoxy views with composables

Jetpack compose allows creating custom views easily without third-party
libraries. Replicate all epoxy custom views with compose to be able to
replace it easily in future when and where ever required.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-01-19 13:18:19 +07:00
parent 2014073c91
commit 50241ed376
20 changed files with 1653 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Checkbox
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.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import com.aurora.store.BuildConfig
import com.aurora.store.R
/**
* Composable for displaying package details in a list for blacklisting
* @param icon Icon for the package
* @param displayName User-readable name of the package
* @param packageName Name of the package
* @param versionName versionName of the package
* @param versionCode versionCode of the package
* @param isChecked Whether the app is blacklisted
* @param onClick Callback when the composable is clicked
*/
@Composable
fun BlackListComposable(
icon: Bitmap,
displayName: String,
packageName: String,
versionName: String,
versionCode: Long,
isChecked: Boolean = false,
onClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.weight(1F)) {
Image(
bitmap = icon.asImageBitmap(),
contentDescription = null,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)),
) {
Text(
text = displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.version, versionName, versionCode),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
)
}
}
Checkbox(checked = isChecked, onCheckedChange = { onClick() })
}
}
@Preview(showBackground = true)
@Composable
private fun BlackListComposablePreview() {
BlackListComposable(
icon = ColorDrawable(Color.TRANSPARENT).toBitmap(56, 56),
displayName = LocalContext.current.getString(R.string.app_name),
packageName = BuildConfig.APPLICATION_ID,
versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE.toLong(),
isChecked = true
)
}

View File

@@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
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.requiredSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.Category
import com.aurora.store.R
/**
* Composable to show a category in a list
* @param category [Category] details to display
* @param onClick Callback when this composable is clicked
*/
@Composable
fun CategoryComposable(category: Category, onClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(category.imageUrl)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_android),
contentScale = ContentScale.Crop,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
)
Text(
text = category.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Preview(showBackground = true)
@Composable
private fun CategoryComposablePreview() {
CategoryComposable(category = Category(title = "Art & Design"))
}

View File

@@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
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.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display device details for spoofing in a list
* @param userReadableName Name of the device, obtained through `UserReadableName` property
* @param manufacturer Name of the device manufacturer, obtained through `Build.MANUFACTURER` property
* @param androidVersionSdk Android version on the device, obtained through `Build.VERSION.SDK_INT` property
* @param platforms Platforms supported on the device, obtained through `Platforms` property
* @param isChecked If the device is selected
* @param onClick Callback when the composable is clicked
*/
@Composable
fun DeviceComposable(
userReadableName: String,
manufacturer: String,
androidVersionSdk: String,
platforms: String,
isChecked: Boolean = false,
onClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = userReadableName,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = LocalContext.current.getString(
R.string.spoof_property,
manufacturer,
androidVersionSdk
),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = platforms,
style = MaterialTheme.typography.bodySmall
)
}
Checkbox(checked = isChecked, enabled = !isChecked, onCheckedChange = { onClick() })
}
}
@Preview(showBackground = true)
@Composable
private fun DeviceComposablePreview() {
DeviceComposable(
userReadableName = "Google Pixel 7a",
manufacturer = "Google",
androidVersionSdk = "33",
platforms = "arm64-v8a",
isChecked = true
)
}

View File

@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
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.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display dispenser URL in a list
* @param url URL of the dispenser
* @param onClick Callback when this URL is clicked
* @param onClear Callback when the clear button is clicked
*/
@Composable
fun DispenserComposable(url: String, onClick: () -> Unit = {}, onClear: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.weight(1F),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
Icon(
painter = painterResource(R.drawable.ic_server),
contentDescription = null
)
Text(
text = url,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
TextButton(onClick = onClear) {
Text(
text = stringResource(R.string.remove),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun DispenserComposablePreview() {
DispenserComposable(url = "https://auroraoss.com/api/auth")
}

View File

@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.room.download.Download
import com.aurora.store.util.CommonUtil.getDownloadSpeedString
import com.aurora.store.util.CommonUtil.getETAString
/**
* Composable to display details of a download in a list
* @param download [Download] to display
* @param onClick Callback when this composable is clicked
* @param onLongClick Callback when this composable is long clicked
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun DownloadComposable(
download: Download,
onClick: () -> Unit = {},
onLongClick: (() -> Unit) = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
)
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(download.iconURL)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_android),
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)),
) {
Text(
text = download.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(download.downloadStatus.localized),
style = MaterialTheme.typography.bodySmall
)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = { download.progress.toFloat() }
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "${download.progress}%",
style = MaterialTheme.typography.bodySmall
)
if (download.isRunning) {
Text(
text = getETAString(LocalContext.current, download.timeRemaining),
style = MaterialTheme.typography.bodySmall
)
Text(
text = getDownloadSpeedString(LocalContext.current, download.speed),
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun DownloadComposablePreview() {
val app = App(
packageName = BuildConfig.APPLICATION_ID,
displayName = LocalContext.current.getString(R.string.app_name)
)
DownloadComposable(download = Download.fromApp(app))
}

View File

@@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import android.text.format.DateUtils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.room.favourite.Favourite
/**
* Composable to display a favourite app in a list
* @param favourite A [Favourite] app to display
* @param onClick Callback when this composable is clicked
* @param onClear Callback when the favourite button is clicked to remove the app from favourites
*/
@Composable
fun FavouriteComposable(
favourite: Favourite,
onClick: () -> Unit = {},
onClear: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(modifier = Modifier.weight(1F)) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(favourite.iconURL)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_android),
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))
) {
Text(
text = favourite.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = favourite.packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = DateUtils.formatDateTime(
LocalContext.current,
favourite.added,
DateUtils.FORMAT_SHOW_DATE
),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
)
}
}
IconButton(onClick = onClear) {
Icon(
painter = painterResource(R.drawable.ic_favorite_checked),
contentDescription = stringResource(R.string.action_favourite)
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun FavouriteComposablePreview() {
val app = App(
packageName = BuildConfig.APPLICATION_ID,
displayName = LocalContext.current.getString(R.string.app_name)
)
FavouriteComposable(favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL))
}

View File

@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
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.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.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display sticky header in list
* @param title Title to display
* @param onClick Callback when this composable is clicked
* @see TextDividerComposable
* @see UpdateHeaderComposable
*/
@Composable
fun HeaderComposable(@StringRes title: Int, onClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(
horizontal = dimensionResource(R.dimen.padding_small),
vertical = dimensionResource(R.dimen.padding_xxsmall)
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1F)
)
Icon(
painter = painterResource(R.drawable.ic_arrow_right),
contentDescription = null
)
}
}
@Preview(showBackground = true)
@Composable
private fun HeaderComposablePreview() {
HeaderComposable(title = R.string.app_name)
}

View File

@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.core.graphics.drawable.toBitmap
import com.aurora.store.BuildConfig
import com.aurora.store.R
/**
* Composable for displaying installed package details in a list
* @param icon Icon for the package
* @param displayName User-readable name of the package
* @param packageName Name of the package
* @param versionName versionName of the package
* @param versionCode versionCode of the package
* @param onClick Callback when the composable is clicked
* @param onLongClick Callback whe composable is long clicked
*/
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun InstalledAppComposable(
icon: Bitmap,
displayName: String,
packageName: String,
versionName: String,
versionCode: Long,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
)
) {
Row {
Image(
bitmap = icon.asImageBitmap(),
contentDescription = null,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)),
) {
Text(
text = displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = packageName,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(R.string.version, versionName, versionCode),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun InstalledAppComposablePreview() {
InstalledAppComposable(
icon = ColorDrawable(Color.TRANSPARENT).toBitmap(56, 56),
displayName = LocalContext.current.getString(R.string.app_name),
packageName = BuildConfig.APPLICATION_ID,
versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE.toLong()
)
}

View File

@@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import com.aurora.store.data.installer.SessionInstaller
import com.aurora.store.data.model.InstallerInfo
/**
* Composable to display installer details in a list
* @param installerInfo A [InstallerInfo] object to display details
* @param isSelected Whether this installer is selected
* @param onClick Callback when this composable is clicked
*/
@Composable
fun InstallerComposable(
installerInfo: InstallerInfo,
isSelected: Boolean = false,
onClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = stringResource(installerInfo.title),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(installerInfo.subtitle),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = stringResource(installerInfo.description),
style = MaterialTheme.typography.bodySmall
)
}
RadioButton(selected = isSelected, onClick = onClick)
}
}
@Preview(showBackground = true)
@Composable
private fun InstallerComposablePreview() {
InstallerComposable(installerInfo = SessionInstaller.installerInfo, isSelected = true)
}

View File

@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import com.aurora.store.data.model.Link
/**
* Composable to show link details in a list
* @param link [Link] to show details
* @param onClick Callback when the composable is clicked
*/
@Composable
fun LinkComposable(link: Link, onClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
Icon(
painter = painterResource(link.icon),
contentDescription = null
)
VerticalDivider(
modifier = Modifier
.padding(horizontal = dimensionResource(R.dimen.margin_large))
.width(dimensionResource(R.dimen.padding_xxsmall))
.height(dimensionResource(R.dimen.height_header))
)
Column {
Text(
text = link.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = link.subtitle,
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun LinkComposablePreview() {
LinkComposable(
link = Link(
id = 0,
title = LocalContext.current.getString(R.string.title_about),
subtitle = LocalContext.current.getString(R.string.about_aurora_store_subtitle),
url = "https://auroraoss.com/",
icon = R.drawable.ic_menu_about
)
)
}

View File

@@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
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.res.dimensionResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import java.util.Locale
/**
* Composable to display locale details in a list
* @param displayName Display name of the locale
* @param displayLanguage Display name of the language in the locale
* @param isChecked Whether the locale is checked/selected
* @param onClick Callback when the composable is clicked
*/
@Composable
fun LocaleComposable(
displayName: String,
displayLanguage: String,
isChecked: Boolean = false,
onClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_small)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = displayName,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = displayLanguage,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Checkbox(checked = isChecked, enabled = !isChecked, onCheckedChange = { onClick() })
}
}
@Preview(showBackground = true)
@Composable
private fun LocaleComposablePreview() {
LocaleComposable(
displayName = Locale.JAPANESE.displayName,
displayLanguage = Locale.JAPAN.getDisplayLanguage(Locale.JAPAN),
isChecked = true
)
}

View File

@@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import com.aurora.store.data.model.Permission
import com.aurora.store.data.model.PermissionType
/**
* Composable to display permission details in a list
* @param permission [Permission] to display
* @param isGranted If the permission has been granted
* @param onAction Callback when the user clicks the action button
*/
@Composable
fun PermissionComposable(
permission: Permission,
isGranted: Boolean = false,
onAction: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = permission.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = permission.subtitle,
style = MaterialTheme.typography.bodySmall
)
}
TextButton(onClick = onAction, enabled = !isGranted) {
Text(
text = if (isGranted) {
stringResource(R.string.action_granted)
} else {
stringResource(R.string.action_grant)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun PermissionComposablePreview() {
PermissionComposable(
permission = Permission(
PermissionType.STORAGE_MANAGER,
LocalContext.current.getString(R.string.onboarding_permission_esm),
LocalContext.current.getString(R.string.onboarding_permission_esa_desc),
false
)
)
}

View File

@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.foundation.clickable
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.requiredSize
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.store.R
/**
* Composable for displaying search suggestions in a list
* @param searchSuggestEntry A [SearchSuggestEntry] to display search suggestion
* @param onClick Callback when this composable is clicked
* @param onAction Callback when action button is clicked
*/
@Composable
fun SearchSuggestionComposable(
searchSuggestEntry: SearchSuggestEntry,
onClick: () -> Unit = {},
onAction: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_medium)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
modifier = Modifier
.weight(1F)
.clickable { onClick() },
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(
if (searchSuggestEntry.hasImageContainer()) {
searchSuggestEntry.imageContainer.imageUrl
} else {
R.drawable.ic_search_suggestion
}
)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_search_suggestion),
contentScale = ContentScale.Crop,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_default))
)
Text(
text = searchSuggestEntry.title,
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
IconButton(onClick = onAction) {
Icon(
painter = painterResource(R.drawable.ic_search_append),
contentDescription = null
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SearchSuggestionComposablePreview() {
SearchSuggestionComposable(
searchSuggestEntry = SearchSuggestEntry.getDefaultInstance()
)
}

View File

@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display a sticky header in a list
* @param title Title to display
* @see HeaderComposable
* @see UpdateHeaderComposable
*/
@Composable
fun TextDividerComposable(@StringRes title: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small))
) {
Text(
text = stringResource(title).uppercase(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Preview(showBackground = true)
@Composable
private fun TextDividerComposablePreview() {
TextDividerComposable(title = R.string.item_optional)
}

View File

@@ -0,0 +1,72 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.annotation.StringRes
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.material3.Button
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.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display sticky header in list
* @param title Title to display
* @param actionTitle Title for the action button
* @param onAction Callback when action button is clicked
* @see TextDividerComposable
* @see HeaderComposable
*/
@Composable
fun UpdateHeaderComposable(
@StringRes title: Int,
@StringRes actionTitle: Int,
onAction: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.bodyLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Button(onClick = onAction) {
Text(
text = stringResource(actionTitle),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun UpdateHeaderComposablePreview() {
UpdateHeaderComposable(
title = R.string.updates_available,
actionTitle = R.string.action_update_all
)
}

View File

@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.util.CommonUtil.addSiPrefix
/**
* Composable for displaying minimal app details in a horizontal-scrollable list
* @param app [App] to display
* @param onClick Callback when the composable is clicked
* @see AppListComposable
*/
@Composable
fun AppComposable(app: App, onClick: () -> Unit = {}) {
Column(
modifier = Modifier
.width(dimensionResource(R.dimen.icon_size_cluster))
.clickable { onClick() }
.padding(dimensionResource(R.dimen.padding_xsmall))
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(app.iconArtwork.url)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_android),
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_cluster))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Text(
text = app.displayName,
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = if (app.size > 0) addSiPrefix(app.size) else app.downloadString,
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
)
}
}
@Preview(showBackground = true)
@Composable
private fun AppComposablePreview() {
AppComposable(
app = App(
packageName = BuildConfig.APPLICATION_ID,
displayName = LocalContext.current.getString(R.string.app_name),
size = 7431013
)
)
}

View File

@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.PackageUtil
/**
* Composable for displaying minimal app details in a vertical-scrollable list
* @param app [App] to display
* @param onClick Callback when the composable is clicked
* @see AppComposable
*/
@Composable
fun AppListComposable(app: App, onClick: () -> Unit = {}) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_small)
)
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(app.iconArtwork.url)
.crossfade(true)
.build(),
contentDescription = null,
placeholder = painterResource(R.drawable.ic_android),
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
)
Column(
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small)),
) {
Text(
text = app.displayName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(text = app.developerName, style = MaterialTheme.typography.bodySmall)
Text(
text = buildExtras(app).joinToString(separator = ""),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
)
}
}
}
@Preview(showBackground = true)
@Composable
fun AppListComposablePreview() {
AppListComposable(
app = App(
packageName = BuildConfig.APPLICATION_ID,
displayName = LocalContext.current.getString(R.string.app_name),
developerName = "Rahul Kumar Patel",
isFree = true,
containsAds = false,
size = 7431013
)
)
}
@Composable
private fun buildExtras(app: App): List<String> {
return mutableListOf<String>().apply {
add(if (app.size > 0) CommonUtil.addSiPrefix(app.size) else app.downloadString)
add("${app.labeledRating}")
if (app.isFree) {
add(stringResource(R.string.details_free))
} else {
add(stringResource(R.string.details_paid))
}
if (app.containsAds) {
add(stringResource(R.string.details_contains_ads))
} else {
add(stringResource(R.string.details_no_ads))
}
if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) {
add(stringResource(R.string.details_gsf_dependent))
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to display an indeterminate circular progress indicator
*/
@Composable
fun AppProgressComposable() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_small)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_small))
)
}
}
@Preview(showBackground = true)
@Composable
private fun AppProgressComposablePreview() {
AppProgressComposable()
}

View File

@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to show error message when no apps are available for a request
* @param message Message for error
* @see NoAppAltComposable
*/
@Composable
fun NoAppAltComposable(@StringRes message: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.padding_xsmall))
) {
Text(
text = stringResource(message),
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Preview(showBackground = true)
@Composable
private fun NoAppAltComposablePreview() {
NoAppAltComposable(message = R.string.details_no_dependencies)
}

View File

@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables.app
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* Composable to show error message when no apps are available for a request
* @param icon Drawable for error
* @param message Message for error
* @param actionMessage Message to show on action button; defaults to null with button not visible
* @param onAction Callback when action button is clicked
* @see NoAppAltComposable
*/
@Composable
fun NoAppComposable(
@DrawableRes icon: Int,
@StringRes message: Int,
@StringRes actionMessage: Int? = null,
onAction: () -> Unit = {}
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(R.dimen.margin_small),
Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = icon),
contentDescription = null,
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size))
)
Text(text = stringResource(message))
if (actionMessage != null) {
Button(onClick = onAction) {
Text(
text = stringResource(actionMessage),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun NoAppComposablePreview() {
NoAppComposable(
icon = R.drawable.ic_updates,
message = R.string.details_no_updates,
actionMessage = R.string.check_updates
)
}