diff --git a/app/src/main/java/com/aurora/store/compose/composables/BlackListComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/BlackListComposable.kt new file mode 100644 index 000000000..ebb403ea8 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/BlackListComposable.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/CategoryComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/CategoryComposable.kt new file mode 100644 index 000000000..2484a46f1 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/CategoryComposable.kt @@ -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")) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/DeviceComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/DeviceComposable.kt new file mode 100644 index 000000000..953158b04 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/DeviceComposable.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/DispenserComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/DispenserComposable.kt new file mode 100644 index 000000000..e6ec43419 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/DispenserComposable.kt @@ -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") +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt new file mode 100644 index 000000000..6d015496b --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/DownloadComposable.kt @@ -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)) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/FavouriteComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/FavouriteComposable.kt new file mode 100644 index 000000000..4ae0bf272 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/FavouriteComposable.kt @@ -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)) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/HeaderComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/HeaderComposable.kt new file mode 100644 index 000000000..191f0900d --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/HeaderComposable.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt new file mode 100644 index 000000000..7b0f1ad31 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt @@ -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() + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/InstallerComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/InstallerComposable.kt new file mode 100644 index 000000000..68bc1b838 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/InstallerComposable.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/LinkComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/LinkComposable.kt new file mode 100644 index 000000000..d533bd769 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/LinkComposable.kt @@ -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 + ) + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/LocaleComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/LocaleComposable.kt new file mode 100644 index 000000000..b4a9e7d9a --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/LocaleComposable.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt new file mode 100644 index 000000000..f72b1d3e6 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/PermissionComposable.kt @@ -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 + ) + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/SearchSuggestionComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/SearchSuggestionComposable.kt new file mode 100644 index 000000000..f5af98822 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/SearchSuggestionComposable.kt @@ -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() + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt new file mode 100644 index 000000000..9296eddba --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/TextDividerComposable.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/UpdateHeaderComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/UpdateHeaderComposable.kt new file mode 100644 index 000000000..e5f5a0567 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/UpdateHeaderComposable.kt @@ -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 + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/AppComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/AppComposable.kt new file mode 100644 index 000000000..40d0f6b1a --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/app/AppComposable.kt @@ -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 + ) + ) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt new file mode 100644 index 000000000..86738d3f9 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/app/AppListComposable.kt @@ -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 { + return mutableListOf().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)) + } + } +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/AppProgressComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/AppProgressComposable.kt new file mode 100644 index 000000000..f162c7064 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/app/AppProgressComposable.kt @@ -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() +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/NoAppAltComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/NoAppAltComposable.kt new file mode 100644 index 000000000..9578f8357 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/app/NoAppAltComposable.kt @@ -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) +} diff --git a/app/src/main/java/com/aurora/store/compose/composables/app/NoAppComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/app/NoAppComposable.kt new file mode 100644 index 000000000..f86c3ac36 --- /dev/null +++ b/app/src/main/java/com/aurora/store/compose/composables/app/NoAppComposable.kt @@ -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 + ) +}