compose: Initial migration of AppDetails* logic to compose [2/*]

Deprecate setting to hide similar and related apps as they will be always
listed in the suggestions pane on widescreen devices

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-04-28 14:16:26 +08:00
parent f6f858e9f5
commit 097f5a7375
111 changed files with 1108 additions and 2571 deletions

View File

@@ -194,6 +194,9 @@ dependencies {
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.adaptive.core)
implementation(libs.androidx.adaptive.navigation)
implementation(libs.androidx.adaptive.layout)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)

View File

@@ -30,7 +30,6 @@ import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
@@ -57,7 +56,6 @@ class OnboardingFragment : BaseFlavouredOnboardingFragment() {
save(PREFERENCE_THEME_STYLE, 0)
save(PREFERENCE_DEFAULT_SELECTED_TAB, 0)
save(PREFERENCE_FOR_YOU, true)
save(PREFERENCE_SIMILAR, true)
/*Installer*/
save(PREFERENCE_AUTO_DELETE, true)

View File

@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.extensions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.window.core.layout.WindowWidthSizeClass
/**
* Returns navigation icon for adaptive screens such as extra pane
*/
val WindowAdaptiveInfo.adaptiveNavigationIcon: ImageVector
get() = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> Icons.AutoMirrored.Filled.ArrowBack
else -> Icons.Default.Close
}

View File

@@ -32,6 +32,7 @@ import com.aurora.store.compose.composables.preview.AppPreviewProvider
/**
* Composable to show some information
* @param modifier Modifier to change the composable
* @param title Title of the information
* @param description Information to show
* @param icon Optional icon representing the information
@@ -39,13 +40,14 @@ import com.aurora.store.compose.composables.preview.AppPreviewProvider
*/
@Composable
fun InfoComposable(
modifier: Modifier = Modifier,
title: AnnotatedString,
description: AnnotatedString? = null,
@DrawableRes icon: Int? = null,
onClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.clickable(onClick = { if (onClick != null) onClick() }, enabled = onClick != null)
.padding(

View File

@@ -14,6 +14,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
@@ -21,6 +22,7 @@ import com.aurora.store.R
/**
* A top app bar composable to be used with Scaffold in different Screen
* @param title Title of the screen
* @param navigationIcon Icon for the navigation button
* @param onNavigateUp Action when user clicks the navigation icon
* @param actions Actions to display on the top app bar (for e.g. menu)
*/
@@ -28,17 +30,17 @@ import com.aurora.store.R
@OptIn(ExperimentalMaterial3Api::class)
fun TopAppBarComposable(
title: String? = null,
onNavigateUp: () -> Unit,
navigationIcon: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
onNavigateUp: (() -> Unit)? = null,
actions: @Composable (RowScope.() -> Unit) = {}
) {
TopAppBar(
title = { if (title != null) Text(text = title) },
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
if (onNavigateUp != null) {
IconButton(onClick = onNavigateUp) {
Icon(imageVector = navigationIcon, contentDescription = null)
}
}
},
actions = actions

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.app
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.LocalAsyncImagePreviewHandler
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
/**
* Composable to show icon for an app that can be animated to also show install progress
* @param modifier Modifier to alter the composable
* @param iconUrl URL of the app icon
* @param progress Progress to show, for e.g. download or install
* @param inProgress Whether to show indeterminate or determinate progress bar
*/
@Composable
fun AnimatedAppIconComposable(
modifier: Modifier = Modifier,
iconUrl: String,
progress: Float = 0F,
inProgress: Boolean = false
) {
val animatedScale by animateFloatAsState(
targetValue = if (inProgress) 0.75F else 1F,
animationSpec = tween(durationMillis = 300)
)
val clip = when {
inProgress -> CircleShape
else -> RoundedCornerShape(dimensionResource(R.dimen.radius_medium))
}
Box(modifier = modifier, contentAlignment = Alignment.Center) {
if (inProgress) {
val indicatorModifier = Modifier.fillMaxSize()
if (progress > 0) {
CircularProgressIndicator(
modifier = indicatorModifier,
progress = { progress / 100 }
)
} else {
CircularProgressIndicator(modifier = indicatorModifier)
}
}
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(iconUrl)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.graphicsLayer(scaleX = animatedScale, scaleY = animatedScale)
.clip(clip)
)
}
}
private class ProgressProvider: PreviewParameterProvider<Float> {
override val values: Sequence<Float>
get() = sequenceOf(0F, 50F)
}
@Preview(showBackground = true)
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AnimatedAppIconPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = app.iconArtwork.url
)
}
}
@Preview(showBackground = true)
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AnimatedAppIconPreview(@PreviewParameter(ProgressProvider::class) progress: Float) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = "",
inProgress = true,
progress = progress
)
}
}

View File

@@ -1,47 +0,0 @@
/*
* 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

@@ -31,7 +31,6 @@ import com.aurora.store.R
* @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(

View File

@@ -11,13 +11,9 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import com.aurora.extensions.parentViewModel
import com.aurora.store.compose.ui.commons.BlacklistScreen
import com.aurora.store.compose.ui.details.AppDetailsScreen
import com.aurora.store.compose.ui.details.DetailsExodusScreen
import com.aurora.store.compose.ui.details.DetailsMoreScreen
import com.aurora.store.compose.ui.details.DetailsReviewScreen
import com.aurora.store.compose.ui.details.DetailsScreenshotScreen
import com.aurora.store.compose.ui.dev.DevProfileScreen
/**
* Navigation graph for compose screens
@@ -45,46 +41,21 @@ fun NavGraph(navHostController: NavHostController, startDestination: Screen) {
val appDetails = backstackEntry.toRoute<Screen.AppDetails>()
AppDetailsScreen(
packageName = appDetails.packageName,
viewModel = backstackEntry.parentViewModel(navHostController),
onNavigateUp = { onNavigateUp() },
onNavigateToDetailsMore = { navHostController.navigate(Screen.DetailsMore) },
onNavigateToDetailsReview = { navHostController.navigate(Screen.DetailsReview) },
onNavigateToDetailsExodus = { navHostController.navigate(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { index ->
navHostController.navigate(
Screen.DetailsScreenshot(index)
)
onNavigateToAppDetails = { packageName ->
navHostController.navigate(Screen.AppDetails(packageName))
}
)
}
composable<Screen.DetailsExodus> { backstackEntry ->
DetailsExodusScreen(
composable<Screen.DevProfile> { backstackEntry ->
val devProfile = backstackEntry.toRoute<Screen.DevProfile>()
DevProfileScreen(
developerId = devProfile.developerId,
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsMore> { backstackEntry ->
DetailsMoreScreen(
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsScreenshot> { backstackEntry ->
val screenshotDetails = backstackEntry.toRoute<Screen.DetailsScreenshot>()
DetailsScreenshotScreen(
index = screenshotDetails.index,
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
)
}
composable<Screen.DetailsReview> { backstackEntry ->
DetailsReviewScreen(
onNavigateUp = { onNavigateUp() },
viewModel = backstackEntry.parentViewModel(navHostController)
onNavigateToAppDetails = { packageName ->
navHostController.navigate(Screen.AppDetails(packageName))
}
)
}
}

View File

@@ -31,18 +31,33 @@ sealed class Screen(
@Serializable
data object Blacklist : Screen(label = R.string.title_blacklist_manager)
@Serializable
data class DevProfile(val developerId: String): Screen()
@Serializable
data class AppDetails(val packageName: String) : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsMore : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data class DetailsScreenshot(val index: Int) : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsExodus : Screen()
/**
* Child screen of [AppDetails]; Avoid navigating to this screen directly.
*/
@Serializable
data object DetailsReview : Screen()
}

View File

@@ -18,6 +18,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@@ -25,36 +28,44 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.NavigableSupportingPaneScaffold
import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.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.AnnotatedString
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.util.fastForEach
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.AsyncImage
import coil3.compose.LocalAsyncImagePreviewHandler
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.extensions.bodyVerySmall
import com.aurora.extensions.browse
import com.aurora.extensions.copyToClipBoard
@@ -68,6 +79,8 @@ import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AnimatedAppIconComposable
import com.aurora.store.compose.composables.app.AppListComposable
import com.aurora.store.compose.composables.app.AppProgressComposable
import com.aurora.store.compose.composables.app.AppTagComposable
import com.aurora.store.compose.composables.app.NoAppComposable
@@ -75,6 +88,8 @@ import com.aurora.store.compose.composables.details.RatingComposable
import com.aurora.store.compose.composables.details.ScreenshotComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.ui.dev.DevProfileScreen
import com.aurora.store.compose.ui.dialogs.ManualDownloadDialog
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.Report
@@ -84,17 +99,16 @@ import com.aurora.store.util.CommonUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import kotlinx.coroutines.launch
import java.util.Locale
import kotlin.random.Random
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@Composable
fun AppDetailsScreen(
packageName: String,
onNavigateUp: () -> Unit,
onNavigateToDetailsMore: () -> Unit,
onNavigateToDetailsScreenshot: (index: Int) -> Unit,
onNavigateToDetailsReview: () -> Unit,
onNavigateToDetailsExodus: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
) {
val context = LocalContext.current
@@ -104,17 +118,23 @@ fun AppDetailsScreen(
val dataSafetyReport by viewModel.dataSafetyReport.collectAsStateWithLifecycle()
val plexusScores by viewModel.plexusScores.collectAsStateWithLifecycle()
val download by viewModel.download.collectAsStateWithLifecycle()
val installProgress by viewModel.installProgress.collectAsStateWithLifecycle()
val suggestions by viewModel.suggestions.collectAsStateWithLifecycle()
var shouldShowManualDownloadDialog by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(key1 = Unit) { viewModel.fetchAppDetails(packageName) }
LaunchedEffect(key1 = Unit) {
viewModel.purchaseStatus.collect { success ->
if (success) {
shouldShowManualDownloadDialog = false
context.toast(R.string.toast_manual_available)
if (shouldShowManualDownloadDialog) {
if (success) {
shouldShowManualDownloadDialog = false
context.toast(R.string.toast_manual_available)
} else {
context.toast(R.string.toast_manual_unavailable)
}
} else {
context.toast(R.string.toast_manual_unavailable)
context.toast(R.string.download_failed)
}
}
}
@@ -135,19 +155,18 @@ fun AppDetailsScreen(
if (this.packageName.isBlank()) {
ScreenContentLoading(onNavigateUp = onNavigateUp)
} else {
ScreenContent(
ScreenContentApp(
app = this,
isAnonymous = viewModel.authProvider.isAnonymous,
suggestions = suggestions,
download = download,
installProgress = installProgress,
plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport,
hasValidUpdate = viewModel.hasValidUpdate,
showSimilarApps = viewModel.showSimilarApps,
onNavigateUp = onNavigateUp,
onNavigateToDetailsMore = onNavigateToDetailsMore,
onNavigateToDetailsScreenshot = onNavigateToDetailsScreenshot,
onNavigateToDetailsReview = onNavigateToDetailsReview,
onNavigateToDetailsExodus = onNavigateToDetailsExodus,
onNavigateToAppDetails = onNavigateToAppDetails,
onDownload = { viewModel.download(this) },
onManualDownload = { shouldShowManualDownloadDialog = true },
onCancelDownload = { viewModel.cancelDownload(this) },
@@ -160,6 +179,9 @@ fun AppDetailsScreen(
} catch (exception: ActivityNotFoundException) {
context.toast(context.getString(R.string.unable_to_open))
}
},
onTestingSubscriptionChange = { subscribe ->
viewModel.updateTestingProgramStatus(packageName, subscribe)
}
)
}
@@ -171,6 +193,9 @@ fun AppDetailsScreen(
}
}
/**
* Composable to show progress while fetching app details
*/
@Composable
private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
Scaffold(
@@ -180,6 +205,9 @@ private fun ScreenContentLoading(onNavigateUp: () -> Unit = {}) {
}
}
/**
* Composable to display errors related to fetching app details
*/
@Composable
private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
Scaffold(
@@ -193,28 +221,186 @@ private fun ScreenContentError(onNavigateUp: () -> Unit = {}) {
}
}
/**
* Composable to display app details and suggestions
*/
@Composable
private fun ScreenContent(
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ScreenContentApp(
app: App,
isAnonymous: Boolean = true,
suggestions: List<App> = emptyList(),
download: Download? = null,
installProgress: Float? = null,
plexusScores: Scores? = null,
dataSafetyReport: DataSafetyReport? = null,
exodusReport: Report? = null,
hasValidUpdate: Boolean = false,
showSimilarApps: Boolean = false,
onNavigateUp: () -> Unit = {},
onNavigateToDetailsMore: () -> Unit = {},
onNavigateToDetailsScreenshot: (index: Int) -> Unit = {},
onNavigateToDetailsReview: () -> Unit = {},
onNavigateToDetailsExodus: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
onDownload: () -> Unit = {},
onManualDownload: () -> Unit = {},
onCancelDownload: () -> Unit = {},
onUninstall: () -> Unit = {},
onOpen: () -> Unit = {}
onOpen: () -> Unit = {},
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit = {}
) {
val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<Screen>()
val coroutineScope = rememberCoroutineScope()
fun showMainPane() {
coroutineScope.launch {
scaffoldNavigator.navigateBack()
}
}
fun showExtraPane(screen: Screen) {
coroutineScope.launch {
scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Extra, screen)
}
}
NavigableSupportingPaneScaffold(
navigator = scaffoldNavigator,
mainPane = {
AnimatedPane {
ScreenContentAppMainPane(
app = app,
isAnonymous = isAnonymous,
download = download,
installProgress = installProgress,
plexusScores = plexusScores,
dataSafetyReport = dataSafetyReport,
exodusReport = exodusReport,
hasValidUpdate = hasValidUpdate,
onNavigateUp = onNavigateUp,
onDownload = onDownload,
onManualDownload = onManualDownload,
onCancelDownload = onCancelDownload,
onUninstall = onUninstall,
onOpen = onOpen,
onNavigateToDetailsDevProfile = { showExtraPane(Screen.DevProfile(it)) },
onNavigateToDetailsMore = { showExtraPane(Screen.DetailsMore) },
onNavigateToDetailsReview = { showExtraPane(Screen.DetailsReview) },
onNavigateToDetailsExodus = { showExtraPane(Screen.DetailsExodus) },
onNavigateToDetailsScreenshot = { showExtraPane(Screen.DetailsScreenshot(it)) },
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
},
supportingPane = {
AnimatedPane(modifier = Modifier.safeContentPadding()) {
ScreenContentAppSupportingPane(
suggestions = suggestions,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
},
extraPane = {
scaffoldNavigator.currentDestination?.contentKey?.let { screen ->
AnimatedPane {
when (screen) {
is Screen.DetailsReview -> DetailsReviewScreen(onNavigateUp = ::showMainPane)
is Screen.DetailsExodus -> DetailsExodusScreen(onNavigateUp = ::showMainPane)
is Screen.DetailsMore -> DetailsMoreScreen(
onNavigateUp = ::showMainPane,
onNavigateToAppDetails = onNavigateToAppDetails
)
is Screen.DetailsScreenshot -> DetailsScreenshotScreen(
index = screen.index,
onNavigateUp = ::showMainPane
)
// TODO: Pass the real developerId
is Screen.DevProfile -> DevProfileScreen(
developerId = app.developerName,
onNavigateUp = ::showMainPane,
onNavigateToAppDetails = { onNavigateToAppDetails(it) }
)
else -> {}
}
}
}
}
)
}
/**
* Composable to display app details
*/
@Composable
private fun ScreenContentAppMainPane(
app: App,
download: Download?,
installProgress: Float?,
isAnonymous: Boolean,
plexusScores: Scores?,
dataSafetyReport: DataSafetyReport?,
exodusReport: Report?,
hasValidUpdate: Boolean,
onNavigateUp: () -> Unit,
onNavigateToDetailsDevProfile: (developerName: String) -> Unit,
onNavigateToDetailsMore: () -> Unit,
onNavigateToDetailsScreenshot: (index: Int) -> Unit,
onNavigateToDetailsReview: () -> Unit,
onNavigateToDetailsExodus: () -> Unit,
onDownload: () -> Unit,
onManualDownload: () -> Unit,
onCancelDownload: () -> Unit,
onUninstall: () -> Unit,
onOpen: () -> Unit,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit
) {
val context = LocalContext.current
@Composable
fun SetupAppActions() {
when {
download?.isRunning == true -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload
)
}
app.isInstalled && hasValidUpdate -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_update),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onDownload,
onSecondaryAction = onUninstall
)
}
app.isInstalled -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onOpen,
onSecondaryAction = onUninstall
)
}
else -> {
val primaryActionName = if (PackageUtil.isArchived(context, app.packageName)) {
stringResource(R.string.action_unarchive)
} else {
if (app.isFree) stringResource(R.string.action_install) else app.price
}
AppActions(
primaryActionDisplayName = primaryActionName,
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
onPrimaryAction = onDownload,
onSecondaryAction = onManualDownload
)
}
}
}
Scaffold(
topBar = {
TopAppBarComposable(
@@ -230,53 +416,24 @@ private fun ScreenContent(
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
// TODO: Deal with download status
AppDetails(app = app, hasValidUpdate = hasValidUpdate)
when {
download?.isRunning == true -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_cancel),
isPrimaryActionEnabled = false,
onSecondaryAction = onCancelDownload
)
}
app.isInstalled && hasValidUpdate -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_update),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onDownload,
onSecondaryAction = onUninstall
)
}
app.isInstalled -> {
AppActions(
primaryActionDisplayName = stringResource(R.string.action_open),
secondaryActionDisplayName = stringResource(R.string.action_uninstall),
onPrimaryAction = onOpen,
onSecondaryAction = onUninstall
)
}
else -> {
val primaryActionName = if (PackageUtil.isArchived(context, app.packageName)) {
stringResource(R.string.action_unarchive)
} else {
if (app.isFree) stringResource(R.string.action_install) else app.price
}
AppActions(
primaryActionDisplayName = primaryActionName,
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
onPrimaryAction = onDownload,
onSecondaryAction = onManualDownload
)
}
val isDownloading = download != null && download.isRunning
val isInstalling = installProgress != null
val progress = when {
isDownloading -> download?.progress?.toFloat() ?: 0F
isInstalling -> installProgress ?: 0F
else -> 0F
}
AppDetails(
app = app,
inProgress = isDownloading || isInstalling,
progress = progress,
onNavigateToDetailsDevApps = onNavigateToDetailsDevProfile,
hasValidUpdate = hasValidUpdate
)
SetupAppActions()
AppTags(app = app)
AppChangelog(changelog = app.changes)
HeaderComposable(
@@ -292,6 +449,13 @@ private fun ScreenContent(
AppReviews(rating = app.rating, onNavigateToDetailsReview = onNavigateToDetailsReview)
if (!isAnonymous && app.testingProgram?.isAvailable == true) {
AppTesting(
isSubscribed = app.testingProgram!!.isSubscribed,
onTestingSubscriptionChange = onTestingSubscriptionChange
)
}
AppCompatibility(
needsGms = app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS),
plexusScores = plexusScores
@@ -322,24 +486,67 @@ private fun ScreenContent(
}
}
/**
* Composable to display similar and related app suggestions
*/
@Composable
private fun ScreenContentAppSupportingPane(
suggestions: List<App> = emptyList(),
onNavigateToAppDetails: (packageName: String) -> Unit = {}
) {
Scaffold(
topBar = { TopAppBarComposable() }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Row(
modifier = Modifier.padding(dimensionResource(R.dimen.margin_medium)),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_suggestions),
contentDescription = null
)
HeaderComposable(title = stringResource(R.string.pref_ui_similar_apps))
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(vertical = dimensionResource(R.dimen.padding_medium))
) {
items(items = suggestions, key = { item -> item.id }) { app ->
AppListComposable(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
}
}
/**
* Composable to display basic app details
*/
@Composable
private fun AppDetails(app: App, hasValidUpdate: Boolean = false) {
private fun AppDetails(
app: App,
progress: Float = 0F,
inProgress: Boolean = false,
onNavigateToDetailsDevApps: (developerName: String) -> Unit,
hasValidUpdate: Boolean = false,
) {
val context = LocalContext.current
Row(modifier = Modifier.fillMaxWidth()) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(app.iconArtwork.url)
.crossfade(true)
.build(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.requiredSize(dimensionResource(R.dimen.icon_size_large))
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
AnimatedAppIconComposable(
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_large)),
iconUrl = app.iconArtwork.url,
inProgress = inProgress,
progress = progress
)
Column(modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.margin_small))) {
Text(
@@ -349,6 +556,8 @@ private fun AppDetails(app: App, hasValidUpdate: Boolean = false) {
overflow = TextOverflow.Ellipsis
)
Text(
modifier = Modifier
.clickable(onClick = { onNavigateToDetailsDevApps(app.developerName) }),
text = app.developerName,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
@@ -383,14 +592,20 @@ private fun AppActions(
isPrimaryActionEnabled: Boolean = true,
isSecondaryActionEnabled: Boolean = true,
onPrimaryAction: () -> Unit = {},
onSecondaryAction: () -> Unit = {}
onSecondaryAction: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium))
) {
val buttonWidthModifier = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> Modifier.weight(1F)
else -> Modifier.widthIn(min = dimensionResource(R.dimen.width_button))
}
FilledTonalButton(
modifier = Modifier.weight(1F),
modifier = buttonWidthModifier,
onClick = onSecondaryAction,
enabled = isSecondaryActionEnabled
) {
@@ -402,7 +617,7 @@ private fun AppActions(
}
Button(
modifier = Modifier.weight(1F),
modifier = buttonWidthModifier,
onClick = onPrimaryAction,
enabled = isPrimaryActionEnabled
) {
@@ -551,7 +766,6 @@ private fun AppReviews(rating: Rating, onNavigateToDetailsReview: () -> Unit) {
HeaderComposable(
title = stringResource(R.string.details_ratings),
subtitle = stringResource(R.string.details_ratings_subtitle),
onClick = onNavigateToDetailsReview
)
@@ -719,12 +933,57 @@ private fun AppPrivacy(report: Report?, onNavigateToDetailsExodus: (() -> Unit)?
)
}
@Preview
/**
* Composable to display app's beta testing status
*/
@Composable
private fun AppTesting(
isSubscribed: Boolean = false,
onTestingSubscriptionChange: (subscribe: Boolean) -> Unit
) {
HeaderComposable(title = stringResource(R.string.details_beta))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_small))
) {
InfoComposable(
modifier = Modifier.weight(1F),
icon = R.drawable.ic_experiment,
title = AnnotatedString(
text = if (isSubscribed) {
stringResource(R.string.details_beta_subscribed)
} else {
stringResource(R.string.details_beta_available)
}
),
description = AnnotatedString(text = stringResource(R.string.details_beta_description))
)
FilledTonalButton(onClick = { onTestingSubscriptionChange(!isSubscribed) }) {
Text(
text = if (isSubscribed) {
stringResource(R.string.action_leave)
} else {
stringResource(R.string.action_join)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@PreviewScreenSizes
@Composable
@OptIn(ExperimentalCoilApi::class)
private fun AppDetailsScreenPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides coilPreviewProvider) {
ScreenContent(app = app, hasValidUpdate = false)
ScreenContentApp(
app = app,
isAnonymous = false,
suggestions = List(5) { app.copy(id = Random.nextInt()) },
hasValidUpdate = false
)
}
}

View File

@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -20,7 +22,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.Constants.EXODUS_REPORT_URL
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.extensions.browse
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
@@ -30,20 +34,31 @@ import com.aurora.store.compose.composables.details.ExodusComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsExodusViewModel
@Composable
fun DetailsExodusScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsExodusViewModel: DetailsExodusViewModel = hiltViewModel { factory: DetailsExodusViewModel.Factory ->
factory.create(appDetailsViewModel.exodusReport.value!!)
},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val exodusReport by viewModel.exodusReport.collectAsStateWithLifecycle()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val exodusReport by appDetailsViewModel.exodusReport.collectAsStateWithLifecycle()
val trackers by detailsExodusViewModel.trackers.collectAsStateWithLifecycle()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_privacy)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
id = exodusReport!!.id,
version = exodusReport!!.version,
trackers = viewModel.getExodusTrackersFromReport(),
trackers = trackers,
onNavigateUp = onNavigateUp,
)
}
@@ -55,12 +70,17 @@ private fun ScreenContent(
version: String = String(),
trackers: List<ExodusTracker> = emptyList(),
onNavigateUp: () -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val context = LocalContext.current
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
LazyColumn(

View File

@@ -8,12 +8,17 @@ package com.aurora.store.compose.ui.details
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -26,32 +31,63 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import coil3.annotation.ExperimentalCoilApi
import coil3.compose.LocalAsyncImagePreviewHandler
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.compose.composables.HeaderComposable
import com.aurora.store.compose.composables.InfoComposable
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.app.AppComposable
import com.aurora.store.compose.composables.preview.AppPreviewProvider
import com.aurora.store.compose.composables.preview.coilPreviewProvider
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsMoreViewModel
import java.util.Locale
@Composable
fun DetailsMoreScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
onNavigateToAppDetails: (packageName: String) -> Unit,
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsMoreViewModel: DetailsMoreViewModel = hiltViewModel { factory: DetailsMoreViewModel.Factory ->
factory.create(appDetailsViewModel.app.value!!.dependencies.dependentPackages)
}
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val dependencies by detailsMoreViewModel.dependentApps.collectAsStateWithLifecycle()
ScreenContent(app = app!!, onNavigateUp = onNavigateUp)
ScreenContent(
app = app!!,
dependencies = dependencies,
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
@Composable
private fun ScreenContent(app: App, onNavigateUp: () -> Unit = {}) {
private fun ScreenContent(
app: App,
dependencies: List<App>? = null,
onNavigateUp: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app.displayName
else -> stringResource(R.string.details_more_about_app)
}
Scaffold(
topBar = { TopAppBarComposable(title = app.displayName, onNavigateUp = onNavigateUp) }
topBar = {
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
@@ -69,51 +105,88 @@ private fun ScreenContent(app: App, onNavigateUp: () -> Unit = {}) {
style = MaterialTheme.typography.bodyMedium
)
if (app.appInfo.appInfoMap.isNotEmpty()) {
HeaderComposable(title = stringResource(R.string.details_more_info))
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_package_name)
),
description = AnnotatedString(text = app.packageName)
if (dependencies != null) {
AppDependencies(
dependencies = dependencies,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_content_rating)
),
description = AnnotatedString(text = app.contentRating.title)
)
AppInfoMore(app = app)
}
}
}
app.appInfo.appInfoMap.forEach { (title, subtitle) ->
InfoComposable(
title = AnnotatedString(
text = title.replace("_", " ")
.lowercase(Locale.getDefault())
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
),
description = AnnotatedString(text = subtitle)
)
}
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_target_api)
),
description = AnnotatedString(text = "API ${app.targetSdk}")
/**
* Composable to show dependencies of an app
*/
@Composable
private fun AppDependencies(
dependencies: List<App>,
onNavigateToAppDetails: (packageName: String) -> Unit
) {
HeaderComposable(title = stringResource(R.string.details_dependencies))
if (dependencies.isEmpty()) {
InfoComposable(
title = AnnotatedString(text = stringResource(R.string.details_no_dependencies))
)
} else {
LazyRow(modifier = Modifier.fillMaxWidth()) {
items(items = dependencies, key = { item -> item.id }) { app ->
AppComposable(
app = app,
onClick = { onNavigateToAppDetails(app.packageName) }
)
}
}
}
}
/**
* Composable to show more information about the app that maybe advanced
*/
@Composable
private fun AppInfoMore(app: App) {
HeaderComposable(title = stringResource(R.string.details_more_info))
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_package_name)
),
description = AnnotatedString(text = app.packageName)
)
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_target_api)
),
description = AnnotatedString(text = "API ${app.targetSdk}")
)
InfoComposable(
title = AnnotatedString(
text = stringResource(R.string.details_more_content_rating)
),
description = AnnotatedString(text = app.contentRating.title)
)
app.appInfo.appInfoMap.forEach { (title, subtitle) ->
InfoComposable(
title = AnnotatedString(
text = title.replace("_", " ")
.lowercase(Locale.getDefault())
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
),
description = AnnotatedString(text = subtitle)
)
}
}
@Preview
@Composable
@OptIn(ExperimentalCoilApi::class)

View File

@@ -18,6 +18,8 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -34,26 +36,38 @@ import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.compose.composables.details.ReviewComposable
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.review.DetailsReviewViewModel
import kotlinx.coroutines.flow.flowOf
@Composable
fun DetailsReviewScreen(
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
appDetailsViewModel: AppDetailsViewModel = hiltViewModel(),
detailsReviewViewModel: DetailsReviewViewModel = hiltViewModel { factory: DetailsReviewViewModel.Factory ->
factory.create(appDetailsViewModel.app.value!!.packageName)
},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val reviews = viewModel.reviews.collectAsLazyPagingItems()
val app by appDetailsViewModel.app.collectAsStateWithLifecycle()
val reviews = detailsReviewViewModel.reviews.collectAsLazyPagingItems()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_ratings)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
reviews = reviews,
onNavigateUp = onNavigateUp,
onFilter = { filter -> viewModel.fetchReviews(filter) }
onFilter = { filter -> detailsReviewViewModel.fetchReviews(filter) }
)
}
@@ -62,12 +76,17 @@ private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
reviews: LazyPagingItems<Review>,
onFilter: (filter: Review.Filter) -> Unit = {}
onFilter: (filter: Review.Filter) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
@@ -90,7 +109,7 @@ private fun ScreenContent(
count = reviews.itemCount,
key = reviews.itemKey { it.commentId }
) { index ->
ReviewComposable(review = reviews[index]!!)
reviews[index]?.let { review -> ReviewComposable(review = review) }
}
}
}

View File

@@ -10,7 +10,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -18,6 +21,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
@@ -28,12 +33,18 @@ import com.aurora.store.viewmodel.details.AppDetailsViewModel
fun DetailsScreenshotScreen(
index: Int,
onNavigateUp: () -> Unit,
viewModel: AppDetailsViewModel = hiltViewModel()
viewModel: AppDetailsViewModel = hiltViewModel(),
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val app by viewModel.app.collectAsStateWithLifecycle()
val topAppBarTitle = when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> app!!.displayName
else -> stringResource(R.string.details_more_about_app)
}
ScreenContent(
topAppBarTitle = app!!.displayName,
topAppBarTitle = topAppBarTitle,
onNavigateUp = onNavigateUp,
screenshots = app!!.screenshots,
index = index
@@ -45,14 +56,23 @@ private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
screenshots: List<Artwork> = emptyList(),
index: Int = 0
index: Int = 0,
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
val displayMetrics = LocalContext.current.resources.displayMetrics
val pagerState = rememberPagerState(initialPage = index) { screenshots.size }
LaunchedEffect(key1 = index) {
if (pagerState.currentPage != index) pagerState.scrollToPage(index)
}
Scaffold(
topBar = {
TopAppBarComposable(title = topAppBarTitle, onNavigateUp = onNavigateUp)
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
HorizontalPager(

View File

@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.dev
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.adaptive.WindowAdaptiveInfo
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.aurora.extensions.adaptiveNavigationIcon
import com.aurora.store.R
import com.aurora.store.compose.composables.TopAppBarComposable
import com.aurora.store.viewmodel.details.DevProfileViewModel
@Composable
fun DevProfileScreen(
developerId: String,
onNavigateUp: () -> Unit,
onNavigateToAppDetails: (packageName: String) -> Unit,
devProfileViewModel: DevProfileViewModel = hiltViewModel()
) {
ScreenContent(
topAppBarTitle = "",
onNavigateUp = onNavigateUp,
onNavigateToAppDetails = onNavigateToAppDetails
)
}
@Composable
private fun ScreenContent(
topAppBarTitle: String? = null,
onNavigateUp: () -> Unit = {},
onNavigateToAppDetails: (packageName: String) -> Unit = {},
windowAdaptiveInfo: WindowAdaptiveInfo = currentWindowAdaptiveInfo()
) {
Scaffold(
topBar = {
TopAppBarComposable(
title = topAppBarTitle,
navigationIcon = windowAdaptiveInfo.adaptiveNavigationIcon,
onNavigateUp = onNavigateUp
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium)),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
) {
}
}
}
@Preview
@Composable
private fun DevProfileScreenPreview() {
ScreenContent()
}

View File

@@ -26,7 +26,6 @@ sealed class BusEvent : Event() {
lateinit var error: String
data class Blacklisted(val packageName: String) : BusEvent()
data class ManualDownload(val packageName: String, val versionCode: Long) : BusEvent()
}
sealed class AuthEvent : Event() {

View File

@@ -35,7 +35,6 @@ object Preferences {
const val PREFERENCE_THEME_STYLE = "PREFERENCE_THEME_STYLE"
const val PREFERENCE_FOR_YOU = "PREFERENCE_FOR_YOU"
const val PREFERENCE_DEFAULT_SELECTED_TAB = "PREFERENCE_DEFAULT_SELECTED_TAB"
const val PREFERENCE_SIMILAR = "PREFERENCE_SIMILAR"
const val PREFERENCE_INTRO = "PREFERENCE_INTRO"
const val PREFERENCE_FILTER_FDROID = "PREFERENCE_FILTER_FDROID"

View File

@@ -1,54 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.controller
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.view.epoxy.groups.CarouselShimmerGroup
import com.aurora.store.view.epoxy.groups.DeveloperModelGroup
open class DeveloperCarouselController(private val callbacks: Callbacks) :
GenericCarouselController(callbacks) {
override fun applyFilter(streamBundle: StreamCluster): Boolean {
return streamBundle.clusterTitle.isNotBlank() //Filter noisy cluster
&& streamBundle.clusterAppList.isNotEmpty() //Filter empty clusters
}
override fun buildModels(streamBundle: StreamBundle?) {
setFilterDuplicates(true)
if (streamBundle == null) {
for (i in 1..2) {
add(
CarouselShimmerGroup()
.id(i)
)
}
} else {
streamBundle
.streamClusters
.values
.filter { applyFilter(it) }
.forEach { streamCluster ->
add(DeveloperModelGroup(streamCluster, callbacks))
}
}
}
}

View File

@@ -1,128 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.groups
import android.util.Log
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyModelGroup
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.R
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.app.AppViewModel_
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
class DeveloperModelGroup(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
) :
EpoxyModelGroup(
R.layout.model_developer_carousel_group, buildModels(
streamCluster,
callbacks
)
) {
companion object {
private const val TAG = "DeveloperModelGroup"
private fun buildModels(
streamCluster: StreamCluster,
callbacks: GenericCarouselController.Callbacks
): List<EpoxyModel<*>> {
val models = ArrayList<EpoxyModel<*>>()
val clusterViewModels = mutableListOf<EpoxyModel<*>>()
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
val idPrefix = streamCluster.id
models.add(
HeaderViewModel_()
.id("${idPrefix}_header")
.title(streamCluster.clusterTitle)
.browseUrl(streamCluster.clusterBrowseUrl)
.click { _ ->
callbacks.onHeaderClicked(streamCluster)
}
)
if (streamCluster.clusterAppList.size == 1) {
val app = streamCluster.clusterAppList[0]
for (artwork in app.screenshots) {
screenshotsViewModels.add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
)
}
clusterViewModels.add(
AppListViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
)
} else {
for (app in streamCluster.clusterAppList) {
clusterViewModels.add(
AppViewModel_()
.id(app.id)
.app(app)
.click { _ ->
callbacks.onAppClick(app)
}
.longClick { _ ->
callbacks.onAppLongClick(app)
false
}
.onBind { _, _, position ->
val itemCount = clusterViewModels.count()
if (itemCount >= 2) {
if (position == clusterViewModels.count() - 2) {
callbacks.onClusterScrolled(streamCluster)
Log.i(TAG, "Cluster ${streamCluster.clusterTitle} Scrolled")
}
}
}
)
}
}
if (screenshotsViewModels.isNotEmpty()) {
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_screenshots")
.models(screenshotsViewModels)
)
}
models.add(
CarouselHorizontalModel_()
.id("${idPrefix}_cluster")
.models(clusterViewModels)
)
return models
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.databinding.ViewExodusBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ExodusView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewExodusBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun tracker(report: ExodusTracker) {
binding.txtTitle.text = report.name
binding.txtSubtitle.text = report.signature
binding.txtDescription.text = report.date
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
@OnViewRecycled
fun clear() {
}
}

View File

@@ -1,47 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.databinding.ViewFileBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class FileView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewFileBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun file(file: PlayFile) {
binding.line1.text = file.name
binding.line2.text = CommonUtil.addSiPrefix(file.size)
}
}

View File

@@ -1,52 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotLargeBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT,
baseModelClass = BaseModel::class
)
class LargeScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotLargeBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun artwork(artwork: Artwork) {
val displayMetrics = Resources.getSystem().displayMetrics
binding.img.load("${artwork.url}=rw-w${displayMetrics.widthPixels}-v1-e15") {
placeholder(R.drawable.bg_placeholder)
}
}
}

View File

@@ -1,102 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.px
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotMiniBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class MiniScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotMiniBinding>(context, attrs, defStyleAttr) {
private var position: Int = 0
interface ScreenshotCallback {
fun onClick(position: Int = 0)
}
@ModelProp
fun position(pos: Int) {
position = pos
}
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
binding.img.load("${artwork.url}=rw-w480-v1-e15") {
placeholder(R.drawable.bg_rounded)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
}
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val artworkHeight = artwork.height
val artworkWidth = artwork.width
val normalizedHeight: Float
val normalizedWidth: Float
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 120f
normalizedWidth = 120f
}
else -> {
val factor = artworkHeight / 120f
normalizedHeight = 120f
normalizedWidth = (artworkWidth / factor)
}
}
binding.img.layoutParams.height = normalizedHeight.px.toInt()
binding.img.layoutParams.width = normalizedWidth.px.toInt()
binding.img.requestLayout()
}
}
@CallbackProp
fun callback(screenshotCallback: ScreenshotCallback?) {
binding.img.setOnClickListener {
screenshotCallback?.onClick(position)
}
}
}

View File

@@ -1,71 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.toDate
import com.aurora.gplayapi.data.models.Review
import com.aurora.store.R
import com.aurora.store.databinding.ViewReviewBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ReviewView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewReviewBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun review(review: Review) {
binding.txtAuthor.text = review.userName
binding.txtTime.text = ("${review.timeStamp.toDate()} • v${review.appVersion}")
binding.txtComment.text = review.comment
binding.img.load(review.userPhotoUrl) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(32F))
}
binding.rating.rating = review.rating.toFloat()
}
@CallbackProp
fun click(onClickListener: OnClickListener?) {
binding.root.setOnClickListener(onClickListener)
}
@CallbackProp
fun longClick(onClickListener: OnLongClickListener?) {
binding.root.setOnLongClickListener(onClickListener)
}
}

View File

@@ -1,102 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views.details
import android.content.Context
import android.util.AttributeSet
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.airbnb.epoxy.CallbackProp
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.extensions.px
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.ViewScreenshotBinding
import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView
@ModelView(
autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class ScreenshotView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewScreenshotBinding>(context, attrs, defStyleAttr) {
private var position: Int = 0
interface ScreenshotCallback {
fun onClick(position: Int = 0)
}
@ModelProp
fun position(pos: Int) {
position = pos
}
@ModelProp
fun artwork(artwork: Artwork) {
normalizeSize(artwork)
binding.img.load("${artwork.url}=rw-w480-v1-e15") {
placeholder(R.drawable.bg_rounded)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
}
private fun normalizeSize(artwork: Artwork) {
if (artwork.height != 0 && artwork.width != 0) {
val artworkHeight = artwork.height
val artworkWidth = artwork.width
val normalizedHeight: Float
val normalizedWidth: Float
when {
artworkHeight == artworkWidth -> {
normalizedHeight = 192f
normalizedWidth = 192f
}
else -> {
val factor = artworkHeight / 192f
normalizedHeight = 192f
normalizedWidth = (artworkWidth / factor)
}
}
binding.img.layoutParams.height = normalizedHeight.px.toInt()
binding.img.layoutParams.width = normalizedWidth.px.toInt()
binding.img.requestLayout()
}
}
@CallbackProp
fun callback(screenshotCallback: ScreenshotCallback?) {
binding.img.setOnClickListener {
screenshotCallback?.onClick(position)
}
}
}

View File

@@ -121,15 +121,6 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
)
}
fun openScreenshotFragment(app: App, position: Int) {
findNavController().navigate(
MobileNavigationDirections.actionGlobalScreenshotFragment(
position,
app.screenshots.toTypedArray()
)
)
}
fun openAppMenuSheet(app: MinimalApp) {
findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app))
}

View File

@@ -24,15 +24,11 @@ import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.airbnb.epoxy.EpoxyModel
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.groups.CarouselHorizontalModel_
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.app.AppListViewModel_
import com.aurora.store.view.epoxy.views.details.MiniScreenshotView
import com.aurora.store.view.epoxy.views.details.MiniScreenshotViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.browse.ExpandedStreamBrowseViewModel
import dagger.hilt.android.AndroidEntryPoint
@@ -92,30 +88,6 @@ class ExpandedStreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBind
}
} else {
streamCluster.clusterAppList.forEach {
val screenshotsViewModels = mutableListOf<EpoxyModel<*>>()
for ((position, artwork) in it.screenshots.withIndex()) {
screenshotsViewModels.add(
MiniScreenshotViewModel_()
.id(artwork.url)
.position(position)
.artwork(artwork)
.callback(object : MiniScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotFragment(it, position)
}
})
)
}
if (screenshotsViewModels.isNotEmpty()) {
add(
CarouselHorizontalModel_()
.id("${it.id}_screenshots")
.models(screenshotsViewModels)
)
}
add(
AppListViewModel_()
.id(it.packageName.hashCode())

View File

@@ -63,7 +63,6 @@ import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
@@ -77,14 +76,11 @@ import com.aurora.store.util.CommonUtil
import com.aurora.store.util.FlavouredUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.view.custom.RatingView
import com.aurora.store.view.epoxy.controller.DetailsCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.epoxy.views.details.ScreenshotView
import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel
@@ -114,8 +110,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED)
private val showSimilarApps: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_SIMILAR)
private fun onEvent(event: Event) {
when (event) {
@@ -131,20 +125,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
}
is BusEvent.ManualDownload -> {
if (app.packageName == event.packageName) {
val requestedApp = app.copy(
versionCode = event.versionCode,
dependencies = app.dependencies.copy(
dependentLibraries = app.dependencies.dependentLibraries.map { lib ->
lib.copy(versionCode = event.versionCode)
}
)
)
purchase(requestedApp)
}
}
is InstallerEvent.Failed -> {
if (app.packageName == event.packageName) {
findNavController().navigate(
@@ -241,16 +221,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
text =
"${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}"
}
binding.layoutDetailsPrivacy.headerPrivacy.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToDetailsExodusFragment(
app.displayName,
report
)
)
}
} else {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(ContextCompat.getColor(requireContext(), R.color.colorGreen))
@@ -618,16 +588,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
purchase(app)
}
}
binding.layoutDetailsApp.btnSecondaryAction.apply {
text = getString(R.string.title_manual_download)
setOnClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToManualDownloadSheet(app)
)
}
}
}
// Lay out the toolbar again
@@ -651,7 +611,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
private fun updateExtraDetails(app: App) {
binding.viewFlipper.displayedChild = 1
updateAppDescription(app)
updateAppRatingAndReviews(app)
@@ -671,10 +630,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
updateBetaSubscription(app)
}
if (showSimilarApps) {
updateAppStream(app)
}
checkAndSetupInstall()
}
@@ -706,45 +661,11 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
)
}
}
headerDescription.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsMoreFragment(app)
)
}
epoxyRecycler.withModels {
setFilterDuplicates(true)
var position = 0
app.screenshots
//.sortedWith { o1, o2 -> o2.height.compareTo(o1.height) }
.forEach { artwork ->
add(
ScreenshotViewModel_()
.id(artwork.url)
.artwork(artwork)
.position(position++)
.callback(object : ScreenshotView.ScreenshotCallback {
override fun onClick(position: Int) {
openScreenshotFragment(app, position)
}
})
)
}
}
}
}
private fun updateAppRatingAndReviews(app: App) {
binding.layoutDetailsReview.apply {
headerRatingReviews.addClickListener {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToDetailsReviewFragment(
app.displayName,
app.packageName
)
)
}
var totalStars = 0L
totalStars += app.rating.oneStar
@@ -833,15 +754,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
imgBeta.load(betaProgram.artwork.url) {
}
btnBetaAction.setOnClickListener {
btnBetaAction.text = getString(R.string.action_pending)
btnBetaAction.isEnabled = false
viewModel.fetchTestingProgramStatus(
app.packageName,
!betaProgram.isSubscribed
)
}
} else {
root.hide()
}
@@ -956,7 +868,6 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private fun updateCompatibilityInfo() {
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
viewModel.fetchPlexusReport(app.packageName)
binding.layoutDetailsCompatibility.txtGmsDependency.apply {
title = getString(R.string.details_compatibility_gms_required_title)

View File

@@ -1,98 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.Constants
import com.aurora.extensions.browse
import com.aurora.store.R
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import com.aurora.store.databinding.FragmentGenericWithToolbarBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.details.ExodusViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DetailsExodusViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DetailsExodusFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
private val viewModel: DetailsExodusViewModel by viewModels()
private val args: DetailsExodusFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.toolbar.apply {
title = args.displayName
setNavigationOnClickListener { findNavController().navigateUp() }
}
updateController(getExodusTrackersFromReport(args.report))
}
private fun updateController(reviews: List<ExodusTracker>) {
binding.recycler.withModels {
add(
HeaderViewModel_()
.id("header")
.title(getString(R.string.exodus_view_report))
.browseUrl("browse")
.click { _ -> context?.browse(Constants.EXODUS_REPORT_URL + args.report.id) }
)
reviews.forEach {
add(
ExodusViewModel_()
.id(it.id)
.tracker(it)
.click { _ ->
context?.browse(it.url)
}
)
}
}
}
private fun getExodusTrackersFromReport(report: Report): List<ExodusTracker> {
val trackerObjects = report.trackers.map {
viewModel.exodusTrackers.getJSONObject(it.toString())
}.toList()
return trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
}
}

View File

@@ -1,172 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.text.HtmlCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.databinding.FragmentDetailsMoreBinding
import com.aurora.store.view.epoxy.views.HeaderViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppAltViewModel_
import com.aurora.store.view.epoxy.views.details.AppDependentViewModel_
import com.aurora.store.view.epoxy.views.details.FileViewModel_
import com.aurora.store.view.epoxy.views.details.InfoViewModel_
import com.aurora.store.view.epoxy.views.details.MoreBadgeViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DetailsMoreViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.UUID
@AndroidEntryPoint
class DetailsMoreFragment : BaseFragment<FragmentDetailsMoreBinding>() {
private val viewModel: DetailsMoreViewModel by viewModels()
private val args: DetailsMoreFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Adjust layout for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerMore) { layout, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
layout.setPadding(0, 0, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
// Toolbar
binding.toolbar.setOnClickListener { findNavController().navigateUp() }
inflateDescription(args.app)
inflateFiles(args.app)
viewModel.fetchDependentApps(args.app)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.dependentApps.collect { list ->
if (list.isNotEmpty()) {
binding.recyclerDependency.withModels {
list.filter { it.displayName.isNotEmpty() }.forEach {
add(
AppDependentViewModel_()
.id(it.id)
.app(it)
.click { _ -> openDetailsFragment(it.packageName, it) }
)
}
}
} else {
binding.recyclerDependency.withModels {
add(
NoAppAltViewModel_()
.id("no_app")
.message(getString(R.string.details_no_dependencies))
)
}
}
}
}
}
private fun inflateDescription(app: App) {
binding.toolbar.title = app.displayName
binding.txtDescription.text = HtmlCompat.fromHtml(
app.description,
HtmlCompat.FROM_HTML_MODE_COMPACT
)
}
private fun inflateFiles(app: App) {
binding.recyclerMore.withModels {
//Add dependent files
if (app.fileList.isNotEmpty()) {
add(
HeaderViewModel_()
.id("badge_header")
.title("Files")
)
app.fileList.forEach {
add(
FileViewModel_()
.id(it.id)
.file(it)
)
}
}
//Add display & extra badges
if (app.infoBadges.isNotEmpty()) {
add(
HeaderViewModel_()
.id("badge_header")
.title("More")
)
app.infoBadges.forEach {
add(
MoreBadgeViewModel_()
.id(it.id)
.badge(it)
)
}
if (app.displayBadges.isNotEmpty()) {
app.displayBadges
.filter { it.textMajor.isNotEmpty() }
.forEach {
add(
MoreBadgeViewModel_()
.id(it.id)
.badge(it)
)
}
}
}
if (app.appInfo.appInfoMap.isNotEmpty()) {
add(
HeaderViewModel_()
.id("info_header")
.title("Info")
)
app.appInfo.appInfoMap.forEach {
add(
InfoViewModel_()
.id(it.key)
.badge(it)
)
}
add(
InfoViewModel_()
.id(UUID.randomUUID().toString())
.badge(mapOf("TARGET" to "${app.targetSdk}").entries.first())
)
}
}
}
}

View File

@@ -1,126 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.ReviewCluster
import com.aurora.store.R
import com.aurora.store.databinding.FragmentDetailsReviewBinding
import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener
import com.aurora.store.view.epoxy.views.AppProgressViewModel_
import com.aurora.store.view.epoxy.views.details.ReviewViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.review.ReviewViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class DetailsReviewFragment : BaseFragment<FragmentDetailsReviewBinding>() {
private val args: DetailsReviewFragmentArgs by navArgs()
private val viewModel: ReviewViewModel by viewModels()
private lateinit var endlessRecyclerOnScrollListener: EndlessRecyclerOnScrollListener
private lateinit var filter: Review.Filter
private lateinit var reviewCluster: ReviewCluster
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Adjust layout for edgeToEdge display
ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { layout, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
layout.setPadding(0, 0, 0, insets.bottom)
WindowInsetsCompat.CONSUMED
}
// Toolbar
binding.toolbar.apply {
title = args.displayName
setNavigationOnClickListener { findNavController().navigateUp() }
}
viewModel.liveData.observe(viewLifecycleOwner) {
if (!::reviewCluster.isInitialized) {
endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() {
override fun onLoadMore(currentPage: Int) {
if (::reviewCluster.isInitialized) {
viewModel.next(reviewCluster.nextPageUrl)
}
}
}
binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener)
}
it?.let {
reviewCluster = it
updateController(reviewCluster)
}
}
// Fetch Reviews
filter = Review.Filter.ALL
viewModel.fetchReview(args.packageName, filter)
// Chips
binding.chipGroup.setOnCheckedStateChangeListener { _, checkedIds ->
when (checkedIds[0]) {
R.id.filter_review_all -> filter = Review.Filter.ALL
R.id.filter_newest_first -> filter = Review.Filter.NEWEST
R.id.filter_review_critical -> filter = Review.Filter.CRITICAL
R.id.filter_review_positive -> filter = Review.Filter.POSITIVE
R.id.filter_review_five -> filter = Review.Filter.FIVE
R.id.filter_review_four -> filter = Review.Filter.FOUR
R.id.filter_review_three -> filter = Review.Filter.THREE
R.id.filter_review_two -> filter = Review.Filter.TWO
R.id.filter_review_one -> filter = Review.Filter.ONE
}
endlessRecyclerOnScrollListener.resetPageCount()
viewModel.fetchReview(args.packageName, filter)
}
}
private fun updateController(reviewCluster: ReviewCluster) {
binding.recycler.withModels {
setFilterDuplicates(true)
reviewCluster.reviewList.forEach {
add(
ReviewViewModel_()
.id(it.commentId)
.review(it)
)
}
if (reviewCluster.hasNext()) {
add(
AppProgressViewModel_()
.id("progress")
)
}
}
}
}

View File

@@ -31,7 +31,6 @@ import com.aurora.gplayapi.data.models.details.DevStream
import com.aurora.store.R
import com.aurora.store.data.model.ViewState
import com.aurora.store.databinding.FragmentDevProfileBinding
import com.aurora.store.view.epoxy.controller.DeveloperCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.DevProfileViewModel
@@ -47,7 +46,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val developerCarouselController = DeveloperCarouselController(this)
// Toolbar
binding.toolbar.apply {
@@ -56,7 +54,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
}
// RecyclerView
binding.recycler.setController(developerCarouselController)
viewModel.liveData.observe(viewLifecycleOwner) {
when (it) {
@@ -82,7 +79,6 @@ class DevProfileFragment : BaseFragment<FragmentDevProfileBinding>(),
binding.txtDevDescription.text = description
binding.imgIcon.load(imgUrl)
binding.viewFlipper.displayedChild = 0
developerCarouselController.setData(streamBundle)
}
}
}

View File

@@ -1,73 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.PagerSnapHelper
import androidx.recyclerview.widget.RecyclerView
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.store.R
import com.aurora.store.databinding.FragmentScreenshotBinding
import com.aurora.store.view.epoxy.views.details.LargeScreenshotViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ScreenshotFragment : BaseFragment<FragmentScreenshotBinding>() {
private val args: ScreenshotFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.toolbar.apply {
elevation = 0f
navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
setNavigationOnClickListener { findNavController().navigateUp() }
}
// Recycler View
binding.recyclerView.apply {
layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
PagerSnapHelper().attachToRecyclerView(this)
}
updateController(args.arrayOfArtwork, args.position)
}
private fun updateController(artworks: Array<Artwork>, position: Int) {
binding.recyclerView.withModels {
artworks.forEach {
add(
LargeScreenshotViewModel_()
.id(it.url)
.artwork(it)
)
}
binding.recyclerView.scrollToPosition(position)
}
}
}

View File

@@ -1,106 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2019, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
package com.aurora.store.view.ui.sheets
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import coil3.load
import coil3.request.placeholder
import coil3.request.transformations
import coil3.transform.RoundedCornersTransformation
import com.aurora.extensions.toast
import com.aurora.store.R
import com.aurora.store.databinding.SheetManualDownloadBinding
import com.aurora.store.viewmodel.sheets.ManualDownloadViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@AndroidEntryPoint
class ManualDownloadSheet : BaseDialogSheet<SheetManualDownloadBinding>() {
private val viewModel: ManualDownloadViewModel by viewModels()
private val args: ManualDownloadSheetArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false
inflateData()
attachActions()
viewLifecycleOwner.lifecycleScope.launch {
viewModel.purchaseStatus.collectLatest {
if (it) {
toast(R.string.toast_manual_available)
dismissAllowingStateLoss()
} else {
toast(R.string.toast_manual_unavailable)
}
}
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
this.lifecycleScope.launch {
viewModel.purchaseStatus.collectLatest {
if (it) {
toast(R.string.toast_manual_available)
dismissAllowingStateLoss()
} else {
toast(R.string.toast_manual_unavailable)
}
}
}
return super.onCreateDialog(savedInstanceState)
}
private fun inflateData() {
binding.imgIcon.load(args.app.iconArtwork.url) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(32F))
}
binding.txtLine1.text = args.app.displayName
binding.txtLine2.text = args.app.packageName
binding.txtLine3.text = ("${args.app.versionName} (${args.app.versionCode})")
binding.versionCodeLayout.hint = "${args.app.versionCode}"
binding.versionCodeLayout.editText?.setText("${args.app.versionCode}")
}
private fun attachActions() {
binding.btnPrimary.setOnClickListener {
val customVersionString = (binding.versionCodeInp.text).toString()
if (customVersionString.isEmpty())
binding.versionCodeInp.error = "Enter version code"
else {
viewModel.purchase(args.app, customVersionString.toLong())
}
}
binding.btnSecondary.setOnClickListener {
dismissAllowingStateLoss()
}
}
}

View File

@@ -7,11 +7,10 @@
package com.aurora.store.viewmodel.details
import android.content.Context
import android.content.pm.PackageInstaller
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
@@ -24,11 +23,9 @@ import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.BuildConfig
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.PlexusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.model.Scores
import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
@@ -36,20 +33,18 @@ import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PackageUtil.PACKAGE_NAME_GMS
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
@@ -68,8 +63,7 @@ class AppDetailsViewModel @Inject constructor(
private val downloadHelper: DownloadHelper,
private val favouriteDao: FavouriteDao,
private val httpClient: IHttpClient,
private val json: Json,
private val exodusTrackers: JSONObject
private val json: Json
) : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
@@ -77,9 +71,8 @@ class AppDetailsViewModel @Inject constructor(
private val _app = MutableStateFlow<App?>(App(""))
val app = _app.asStateFlow()
private var reviewsNextPageUrl: String? = null
private val _reviews = MutableStateFlow<PagingData<Review>>(PagingData.empty())
val reviews = _reviews.asStateFlow()
private val _suggestions = MutableStateFlow<List<App>>(emptyList())
val suggestions = _suggestions.asStateFlow()
private val _userReview = MutableStateFlow<Review?>(null)
val userReview = _userReview.asStateFlow()
@@ -107,6 +100,27 @@ class AppDetailsViewModel @Inject constructor(
list.find { d -> d.packageName == a?.packageName }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
// TODO: Save sessionId to downloads to monitor packageInstall progress
val installProgress = callbackFlow<Float?> {
val packageInstaller = context.packageManager.packageInstaller
val callback = object : PackageInstaller.SessionCallback() {
override fun onActiveChanged(p0: Int, p1: Boolean) {}
override fun onBadgingChanged(p0: Int) {}
override fun onCreated(p0: Int) {
// trySend(0F)
}
override fun onFinished(p0: Int, p1: Boolean) {
// trySend(null)
}
override fun onProgressChanged(p0: Int, p1: Float) {
// trySend(p1)
}
}
packageInstaller.registerSessionCallback(callback)
awaitClose { packageInstaller.unregisterSessionCallback(callback) }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(context, PREFERENCE_UPDATES_EXTENDED)
@@ -114,7 +128,7 @@ class AppDetailsViewModel @Inject constructor(
get() = PackageUtil.isUpdatable(
context,
app.value!!.packageName,
app.value!!.versionCode.toLong()
app.value!!.versionCode
)
private val hasValidCerts: Boolean
@@ -128,15 +142,9 @@ class AppDetailsViewModel @Inject constructor(
val hasValidUpdate: Boolean
get() = (isUpdatable && hasValidCerts) || (isUpdatable && isExtendedUpdateEnabled)
val showSimilarApps: Boolean
get() = Preferences.getBoolean(context, PREFERENCE_SIMILAR)
fun fetchAppDetails(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_favourite.value = favouriteDao.isFavourite(packageName)
_dataSafetyReport.value = webDataSafetyHelper.fetch(packageName)
_app.value = appDetailsHelper.getAppByPackageName(packageName).copy(
isInstalled = PackageUtil.isInstalled(context, packageName)
)
@@ -148,7 +156,9 @@ class AppDetailsViewModel @Inject constructor(
// Only proceed if there was no error while fetching the app details
if (throwable != null) return@invokeOnCompletion
fetchReviews()
fetchFavourite(packageName)
fetchDataSafetyReport(packageName)
fetchSuggestions()
fetchExodusPrivacyReport(packageName)
if (app.value!!.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
fetchPlexusReport(packageName)
@@ -156,48 +166,11 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun fetchReviews(filter: Review.Filter = Review.Filter.ALL) {
reviewsNextPageUrl = null
createPager {
val reviewsCluster = reviewsNextPageUrl?.let { nextPageUrl ->
reviewsHelper.next(nextPageUrl)
} ?: reviewsHelper.getReviews(app.value!!.packageName, filter)
reviewsNextPageUrl = reviewsCluster.nextPageUrl
reviewsCluster.reviewList
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _reviews.value = it }
.launchIn(viewModelScope)
}
private fun fetchExodusPrivacyReport(packageName: String) {
fun updateTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
_exodusReport.value = getLatestExodusReport(packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception)
_exodusReport.value = null
}
}
}
fun fetchPlexusReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_plexusScores.value = getPlexusReport(packageName)?.report?.scores
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch compatibility report", exception)
_plexusScores.value = null
}
}
}
fun fetchTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
_testingProgramStatus.value = appDetailsHelper.testingProgram(packageName, subscribe)
_testingProgramStatus.value =
appDetailsHelper.testingProgram(packageName, subscribe)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch testing program status", exception)
}
@@ -272,24 +245,50 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun getExodusTrackersFromReport(): List<ExodusTracker> {
val trackerObjects = exodusReport.value!!.trackers.map {
exodusTrackers.getJSONObject(it.toString())
}.toList()
private fun fetchFavourite(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_favourite.value = favouriteDao.isFavourite(packageName)
}
}
return trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
private fun fetchDataSafetyReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
_dataSafetyReport.value = webDataSafetyHelper.fetch(packageName)
}
}
private fun fetchSuggestions() {
// Bail out if we go no suggestions to offer
if (app.value!!.detailsStreamUrl.isNullOrBlank()) return
viewModelScope.launch(Dispatchers.IO) {
val streamBundle = appDetailsHelper.getDetailsStream(app.value!!.detailsStreamUrl!!)
_suggestions.value = streamBundle.streamClusters.values
.flatMap { it.clusterAppList }
.distinctBy { it.packageName }
}
}
private fun fetchExodusPrivacyReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_exodusReport.value = getLatestExodusReport(packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch privacy report", exception)
_exodusReport.value = null
}
}
}
private fun fetchPlexusReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
_plexusScores.value = getPlexusReport(packageName)?.report?.scores
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch compatibility report", exception)
_plexusScores.value = null
}
}
}
private fun getLatestExodusReport(packageName: String): Report? {
@@ -305,12 +304,6 @@ class AppDetailsViewModel @Inject constructor(
.firstOrNull()
}
private fun getPlexusReport(packageName: String): PlexusReport? {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
return json.decodeFromString<PlexusReport>(String(playResponse.responseBytes))
}
private fun parseExodusResponse(response: String, packageName: String): List<Report> {
try {
val jsonObject = JSONObject(response)
@@ -322,4 +315,10 @@ class AppDetailsViewModel @Inject constructor(
return emptyList()
}
}
private fun getPlexusReport(packageName: String): PlexusReport? {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
return json.decodeFromString<PlexusReport>(String(playResponse.responseBytes))
}
}

View File

@@ -6,9 +6,56 @@
package com.aurora.store.viewmodel.details
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
import javax.inject.Inject
@HiltViewModel
class DetailsExodusViewModel @Inject constructor(val exodusTrackers: JSONObject) : ViewModel()
@HiltViewModel(assistedFactory = DetailsExodusViewModel.Factory::class)
class DetailsExodusViewModel @AssistedInject constructor(
@Assisted private val exodusReport: Report,
private val exodusTrackers: JSONObject
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(exodusReport: Report): DetailsExodusViewModel
}
private val _trackers = MutableStateFlow<List<ExodusTracker>>(emptyList())
val trackers = _trackers.asStateFlow()
init {
getExodusTrackersFromReport()
}
fun getExodusTrackersFromReport() {
viewModelScope.launch(Dispatchers.IO) {
val trackerObjects = exodusReport.trackers.map {
exodusTrackers.getJSONObject(it.toString())
}.toList()
_trackers.value = trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
}
}
}

View File

@@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2023-2025 The Calyx Institute
* SPDX-FileCopyrightText: 2024 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.details
import android.util.Log
@@ -5,36 +11,42 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.AppDetailsHelper
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class DetailsMoreViewModel @Inject constructor(
@HiltViewModel(assistedFactory = DetailsMoreViewModel.Factory::class)
class DetailsMoreViewModel @AssistedInject constructor(
@Assisted private val dependencies: List<String>,
private val appDetailsHelper: AppDetailsHelper
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(dependencies: List<String>): DetailsMoreViewModel
}
private val TAG = DetailsMoreViewModel::class.java.simpleName
private val dependantAppsStash = mutableMapOf<String, List<App>>()
private val _dependentApps = MutableSharedFlow<List<App>>()
val dependentApps = _dependentApps.asSharedFlow()
private val _dependentApps = MutableStateFlow<List<App>?>(emptyList())
val dependentApps = _dependentApps.asStateFlow()
fun fetchDependentApps(app: App) {
init {
fetchDependencies()
}
private fun fetchDependencies() {
viewModelScope.launch(Dispatchers.IO) {
try {
val dependantApps = dependantAppsStash.getOrPut(app.packageName) {
appDetailsHelper.getAppByPackageName(app.dependencies.dependentPackages)
}
_dependentApps.emit(dependantApps)
_dependentApps.value = appDetailsHelper.getAppByPackageName(dependencies)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch dependencies", exception)
dependantAppsStash[app.packageName] = emptyList()
_dependentApps.emit(emptyList())
_dependentApps.value = null
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2021-2023 AuroraOSS
* SPDX-FileCopyrightText: 2024-2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.review
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.store.data.paging.GenericPagingSource.Companion.createPager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@HiltViewModel(assistedFactory = DetailsReviewViewModel.Factory::class)
class DetailsReviewViewModel @AssistedInject constructor(
@Assisted private val packageName: String,
private val reviewsHelper: ReviewsHelper
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(packageName: String): DetailsReviewViewModel
}
private val TAG = DetailsReviewViewModel::class.java.simpleName
private var reviewsNextPageUrl: String? = null
private val _reviews = MutableStateFlow<PagingData<Review>>(PagingData.empty())
val reviews = _reviews.asStateFlow()
init {
fetchReviews()
}
fun fetchReviews(filter: Review.Filter = Review.Filter.ALL) {
reviewsNextPageUrl = null
createPager { page ->
try {
when (page) {
1 -> reviewsHelper.getReviews(packageName, filter).also {
reviewsNextPageUrl = it.nextPageUrl
}.reviewList
else -> {
if (!reviewsNextPageUrl.isNullOrBlank()) {
reviewsHelper.next(reviewsNextPageUrl!!).also {
reviewsNextPageUrl = it.nextPageUrl
}.reviewList
} else {
emptyList()
}
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
emptyList()
}
}.flow.distinctUntilChanged()
.cachedIn(viewModelScope)
.onEach { _reviews.value = it }
.launchIn(viewModelScope)
}
}

View File

@@ -1,72 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.viewmodel.review
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.ReviewCluster
import com.aurora.gplayapi.helpers.ReviewsHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ReviewViewModel @Inject constructor(
private val reviewsHelper: ReviewsHelper
) : ViewModel() {
val TAG = javaClass.simpleName
val liveData: MutableLiveData<ReviewCluster> = MutableLiveData()
private lateinit var reviewsCluster: ReviewCluster
fun fetchReview(packageName: String, filter: Review.Filter) {
viewModelScope.launch(Dispatchers.IO) {
try {
reviewsCluster = reviewsHelper.getReviews(packageName, filter)
liveData.postValue(reviewsCluster)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch reviews", e)
}
}
}
fun next(nextReviewPageUrl: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val currentCluster = reviewsCluster
val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl)
reviewsCluster = currentCluster.copy(
nextPageUrl = nextReviewCluster.nextPageUrl,
reviewList = currentCluster.reviewList + nextReviewCluster.reviewList
)
liveData.postValue(reviewsCluster)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch next reviews $nextReviewPageUrl", e)
}
}
}
}

View File

@@ -1,43 +0,0 @@
package com.aurora.store.viewmodel.sheets
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ManualDownloadViewModel @Inject constructor(
private val purchaseHelper: PurchaseHelper
) : ViewModel() {
private val TAG = ManualDownloadViewModel::class.java.simpleName
private val _purchaseStatus = MutableSharedFlow<Boolean>()
val purchaseStatus = _purchaseStatus.asSharedFlow()
fun purchase(app: App, customVersion: Long) {
viewModelScope.launch(Dispatchers.IO) {
try {
val files = purchaseHelper.purchase(app.packageName, customVersion, app.offerType)
if (files.isNotEmpty()) {
AuroraApp.events.send(
BusEvent.ManualDownload(app.packageName, customVersion)
)
}
_purchaseStatus.emit(files.isNotEmpty())
} catch (exception: Exception) {
_purchaseStatus.emit(false)
Log.e(TAG, "Failed to find version: $customVersion", exception)
}
}
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q149,840 127.5,794.5Q106,749 138,710L360,440L360,200L320,200Q303,200 291.5,188.5Q280,177 280,160Q280,143 291.5,131.5Q303,120 320,120L640,120Q657,120 668.5,131.5Q680,143 680,160Q680,177 668.5,188.5Q657,200 640,200L600,200L600,440L822,710Q854,749 832.5,794.5Q811,840 760,840L200,840ZM280,720L680,720L544,560L416,560L280,720ZM200,760L760,760L520,468L520,200L440,200L440,468L200,760ZM480,480L480,480L480,480L480,480L480,480L480,480Z"/>
</vector>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: Material Design Authors / Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q447,880 423.5,856.5Q400,833 400,800L560,800Q560,833 536.5,856.5Q513,880 480,880ZM320,760L320,680L640,680L640,760L320,760ZM330,640Q261,599 220.5,530Q180,461 180,380Q180,255 267.5,167.5Q355,80 480,80Q605,80 692.5,167.5Q780,255 780,380Q780,461 739.5,530Q699,599 630,640L330,640ZM354,560L606,560Q651,528 675.5,481Q700,434 700,380Q700,288 636,224Q572,160 480,160Q388,160 324,224Q260,288 260,380Q260,434 284.5,481Q309,528 354,560ZM480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Q480,560 480,560Z"/>
</vector>

View File

@@ -56,74 +56,57 @@
android:id="@+id/layout_details_app"
layout="@layout/layout_details_app" />
<ViewFlipper
android:id="@+id/view_flipper"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<RelativeLayout
<include
android:id="@+id/layout_detail_description"
layout="@layout/layout_details_description" />
<include
android:id="@+id/layout_details_review"
layout="@layout/layout_details_review" />
<include
android:id="@+id/layout_details_beta"
layout="@layout/layout_details_beta" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler_stream"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="320dp">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:clipToPadding="false"
android:orientation="vertical"
android:showDividers="middle">
android:overScrollMode="never"
android:scrollbars="none"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_app" />
<include
android:id="@+id/layout_detail_description"
layout="@layout/layout_details_description" />
<include
android:id="@+id/layout_details_compatibility"
layout="@layout/layout_details_compatibility" />
<include
android:id="@+id/layout_details_review"
layout="@layout/layout_details_review" />
<include
android:id="@+id/layout_details_permissions"
layout="@layout/layout_details_permissions" />
<include
android:id="@+id/layout_details_beta"
layout="@layout/layout_details_beta" />
<include
android:id="@+id/layout_details_data_safety"
layout="@layout/layout_details_data_safety" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxy_recycler_stream"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:overScrollMode="never"
android:scrollbars="none"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_app" />
<include
android:id="@+id/layout_details_privacy"
layout="@layout/layout_details_privacy" />
<include
android:id="@+id/layout_details_compatibility"
layout="@layout/layout_details_compatibility" />
<include
android:id="@+id/layout_details_permissions"
layout="@layout/layout_details_permissions" />
<include
android:id="@+id/layout_details_data_safety"
layout="@layout/layout_details_data_safety" />
<include
android:id="@+id/layout_details_privacy"
layout="@layout/layout_details_privacy" />
<include
android:id="@+id/layout_details_dev"
layout="@layout/layout_details_dev" />
</LinearLayout>
</ViewFlipper>
<include
android:id="@+id/layout_details_dev"
layout="@layout/layout_details_dev" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -1,106 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.ui.details.DetailsMoreFragment">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="middle">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_normal"
android:showDividers="middle">
<com.aurora.store.view.custom.layouts.ActionHeaderLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
app:headerTitle="@string/details_description" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="all"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
android:textIsSelectable="true" />
<com.aurora.store.view.custom.layouts.ActionHeaderLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
app:headerTitle="@string/details_dependencies" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler_dependency"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="true"
android:nestedScrollingEnabled="false"
android:orientation="horizontal"
android:overScrollMode="never"
android:scrollbars="none"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/view_app_dependent" />
</LinearLayout>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler_more"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="true"
android:nestedScrollingEnabled="false"
android:orientation="vertical"
android:overScrollMode="never"
android:scrollbars="none"
app:itemSpacing="@dimen/margin_small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="4"
tools:listitem="@layout/view_file" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.ui.details.ScreenshotFragment">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:translationZ="1dp"
app:navigationIcon="@drawable/ic_arrow_back" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_screenshot" />
</LinearLayout>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/header_view" />
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/recycler_view_01" />
<ViewStub
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/recycler_view_02" />
</LinearLayout>

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:divider="@drawable/divider"
android:orientation="vertical"
android:padding="@dimen/padding_large"
android:showDividers="middle">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_icon"
android:layout_width="@dimen/icon_size_category"
android:layout_height="@dimen/icon_size_category"
android:layout_centerVertical="true"
tools:src="@drawable/bg_placeholder" />
<TextView
android:id="@+id/txt_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginBottom="@dimen/margin_normal"
android:layout_toEndOf="@id/img_icon"
android:maxLines="1"
android:text="@string/title_manual_download"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Aurora.SubTitle" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txt_line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
tools:text="App Name" />
<TextView
android:id="@+id/txt_line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_line1"
android:layout_alignStart="@id/txt_line1"
android:layout_alignEnd="@id/txt_line1"
android:textAppearance="@style/TextAppearance.Aurora.Line2"
android:textColor="?colorAccent"
tools:text="Package Name" />
<TextView
android:id="@+id/txt_line3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_line2"
android:layout_alignStart="@id/txt_line1"
android:layout_alignEnd="@id/txt_line1"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Aurora.Line3"
tools:text="Base version" />
</RelativeLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/version_code_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:helperText="@string/manual_download_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/version_code_inp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="2">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_secondary"
style="@style/Widget.Material3.Button.TextButton.Dialog.Flush"
android:layout_width="0dp"
android:layout_height="@dimen/height_button"
android:layout_weight="1"
android:text="@string/action_cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_primary"
style="@style/Widget.Material3.Button.TextButton.Dialog.Flush"
android:layout_width="0dp"
android:layout_height="@dimen/height_button"
android:layout_weight="1"
android:text="@string/action_install" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_small"
android:textIsSelectable="true">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
android:textIsSelectable="true"
tools:text="Line1" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_title"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Aurora.Line2"
android:textIsSelectable="true"
tools:text="Line2" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_subtitle"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Aurora.Line3"
android:textIsSelectable="true"
tools:text="Line3" />
</RelativeLayout>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_xsmall"
android:paddingStart="@dimen/padding_small"
android:paddingEnd="@dimen/padding_xsmall">
<TextView
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
tools:text="File Name" />
<TextView
android:id="@+id/line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/line1"
android:layout_alignStart="@id/line1"
android:layout_alignEnd="@id/line1"
android:maxLines="1"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Aurora.Line2"
tools:text="File Size" />
</RelativeLayout>

View File

@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/padding_xsmall">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img"
android:layout_width="@dimen/icon_size_small"
android:layout_height="@dimen/icon_size_small"
android:layout_marginEnd="@dimen/margin_small" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_author"
style="@style/TextAppearance.Aurora.Line1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/img"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
android:textSize="14sp"
tools:text="Author Name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_time"
style="@style/TextAppearance.Aurora.Line3"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/txt_author"
android:layout_alignStart="@id/txt_author"
android:layout_alignEnd="@id/txt_author"
android:ellipsize="end"
android:singleLine="true"
android:textAlignment="viewStart"
tools:text="Date" />
<RatingBar
android:id="@+id/rating"
style="@style/Widget.AppCompat.RatingBar.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/txt_time"
android:layout_alignStart="@id/txt_author"
android:numStars="5" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_comment"
style="@style/TextAppearance.Aurora.Line2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@id/img"
android:layout_alignStart="@id/txt_author"
android:layout_marginTop="@dimen/margin_xsmall"
android:ellipsize="end"
android:maxLines="10"
android:textIsSelectable="true"
tools:text="Comment" />
</RelativeLayout>

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img"
android:layout_width="@dimen/screenshot_width"
android:layout_height="@dimen/screenshot_height"
android:adjustViewBounds="true" />
</RelativeLayout>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Aurora Store
~ Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
~
~ Aurora Store is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 2 of the License, or
~ (at your option) any later version.
~
~ Aurora Store is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img"
android:layout_width="@dimen/screenshot_width_mini"
android:layout_height="@dimen/screenshot_height_mini"
android:adjustViewBounds="true" />
</RelativeLayout>

View File

@@ -32,9 +32,6 @@
<action
android:id="@+id/action_global_expandedStreamBrowseFragment"
app:destination="@id/expandedStreamBrowseFragment" />
<action
android:id="@+id/action_global_screenshotFragment"
app:destination="@id/screenshotFragment" />
<action
android:id="@+id/action_global_devProfileFragment"
app:destination="@id/devProfileFragment" />
@@ -165,18 +162,11 @@
<action
android:id="@+id/action_appDetailsFragment_to_devAppsFragment"
app:destination="@id/devAppsFragment" />
<action
android:id="@+id/action_appDetailsFragment_to_detailsMoreFragment"
app:destination="@id/detailsMoreFragment" />
<action
android:id="@+id/action_appDetailsFragment_to_detailsReviewFragment"
app:destination="@id/detailsReviewFragment" />
<action
android:id="@+id/action_appDetailsFragment_to_detailsExodusFragment"
app:destination="@id/detailsExodusFragment" />
<action
android:id="@+id/action_appDetailsFragment_to_manualDownloadSheet"
app:destination="@id/manualDownloadSheet" />
<argument
android:name="app"
android:defaultValue="@null"
app:argType="com.aurora.gplayapi.data.models.App"
app:nullable="true" />
<action
android:id="@+id/action_appDetailsFragment_to_permissionBottomSheet"
app:destination="@id/permissionBottomSheet" />
@@ -209,17 +199,6 @@
android:name="expandedStreamUrl"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/screenshotFragment"
android:name="com.aurora.store.view.ui.details.ScreenshotFragment"
tools:layout="@layout/fragment_screenshot">
<argument
android:name="position"
app:argType="integer" />
<argument
android:name="arrayOfArtwork"
app:argType="com.aurora.gplayapi.data.models.Artwork[]" />
</fragment>
<fragment
android:id="@+id/devProfileFragment"
android:name="com.aurora.store.view.ui.details.DevProfileFragment"
@@ -253,36 +232,6 @@
android:name="developerName"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/detailsMoreFragment"
android:name="com.aurora.store.view.ui.details.DetailsMoreFragment"
tools:layout="@layout/fragment_details_more">
<argument
android:name="app"
app:argType="com.aurora.gplayapi.data.models.App" />
</fragment>
<fragment
android:id="@+id/detailsReviewFragment"
android:name="com.aurora.store.view.ui.details.DetailsReviewFragment"
tools:layout="@layout/fragment_details_review">
<argument
android:name="displayName"
app:argType="string" />
<argument
android:name="packageName"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/detailsExodusFragment"
android:name="com.aurora.store.view.ui.details.DetailsExodusFragment"
tools:layout="@layout/fragment_generic_with_toolbar">
<argument
android:name="displayName"
app:argType="string" />
<argument
android:name="report"
app:argType="com.aurora.store.data.model.Report" />
</fragment>
<fragment
android:id="@+id/splashFragment"
android:name="com.aurora.store.view.ui.splash.SplashFragment"
@@ -397,15 +346,6 @@
android:name="com.aurora.store.view.ui.sheets.FilterSheet"
android:label="FilterSheet"
tools:layout="@layout/sheet_filter" />
<dialog
android:id="@+id/manualDownloadSheet"
android:name="com.aurora.store.view.ui.sheets.ManualDownloadSheet"
android:label="ManualDownloadSheet"
tools:layout="@layout/sheet_manual_download">
<argument
android:name="app"
app:argType="com.aurora.gplayapi.data.models.App" />
</dialog>
<dialog
android:id="@+id/permissionBottomSheet"
android:name="com.aurora.store.view.ui.sheets.PermissionBottomSheet"

View File

@@ -208,7 +208,6 @@
<string name="title_library">المكتبة</string>
<string name="insecure_anonymous_apply">تأكد من إعادة تسجيل الدخول وإعادة تشغيل التطبيق لتطبيق التغييرات.</string>
<string name="pref_network_title">الشبكات</string>
<string name="pref_ui_similar_apps_desc">عرض مجموعات متشابهة وذات صلة في صفحة تفاصيل التطبيق</string>
<string name="pref_ui_similar_apps">تطبيقات مشابهة و ذات صلة</string>
<string name="pref_ui_no_for_you_desc">عرض صفحات لك على الشاشة الرئيسية</string>
<string name="pref_ui_no_for_you">صفحات لك</string>

View File

@@ -180,7 +180,6 @@
<string name="pref_install_mode_am">Instalador AM</string>
<string name="pref_install_mode_summary">Seleiciona\'l métodu d\'instalación de los archivos APK</string>
<string name="pref_ui_similar_apps">Aplicaciones asemeyaes y rellacionaes</string>
<string name="pref_ui_similar_apps_desc">Amuesa les aplicaciones asemeyaes y rellacionaes nes páxines de detalles de les aplicaciones</string>
<string name="tab_for_you">Pa ti</string>
<string name="title_installed">Instalóse</string>
<string name="title_installer">App Installer</string>

View File

@@ -179,7 +179,6 @@
<string name="pref_ui_layout">Tərtibat</string>
<string name="pref_ui_layout_tab">İlkin səhifəni seç</string>
<string name="pref_ui_similar_apps">Oxşar və əlaqəli tətbiqlər</string>
<string name="pref_ui_similar_apps_desc">Tətbiq təfərrüatları səhifəsində oxşar və əlaqəli qrupları göstər</string>
<string name="purchase_failed">Endirmə Alınmadı</string>
<string name="purchase_invalid">Tətbiq satın alınmayıb</string>
<string name="purchase_unsupported">Tətbiq dəstəklənmir</string>

View File

@@ -118,7 +118,6 @@
<string name="pref_network_proxy_url">URL проксі</string>
<string name="pref_network_proxy_url_message">Увядзіце сапраўдны URL для праходжання ўсяго трафіку праз проксі.</string>
<string name="pref_ui_similar_apps">Падобнае</string>
<string name="pref_ui_similar_apps_desc">Паказваць падобныя ці звязаныя кластары на старонцы праграмы</string>
<string name="pref_aurora_only_desc">Не правяраць наяўнасць абнаўленняў для праграм, усталяваных з крыніц па-за межамі крамы Aurora</string>
<string name="purchase_failed">Памылка спампоўкі</string>
<string name="purchase_no_file">Не ўдалося атрымаць файл</string>

View File

@@ -139,7 +139,6 @@
<string name="pref_install_mode_services">Услуги на Aurora</string>
<string name="pref_install_mode_summary">Изберете режим на инсталиране на APK</string>
<string name="pref_ui_no_for_you_desc">Показва страници за вас на началния екран</string>
<string name="pref_ui_similar_apps_desc">Показва подобни и свързани клъстери на страницата с подробности за приложението</string>
<string name="purchase_failed">Изтеглянето се провали</string>
<string name="purchase_unsupported">Приложението не се поддържа</string>
<string name="purchase_not_found">Приложението не е намерено</string>

View File

@@ -176,7 +176,6 @@
<string name="pref_ui_similar_apps">Aplicacions similars i relacionades</string>
<string name="purchase_unsupported">Aplicació no compatible</string>
<string name="purchase_not_found">No s\'ha trobat l\'aplicació</string>
<string name="pref_ui_similar_apps_desc">Mostrar grups similars i relacionats a la pàgina detalls de l\'aplicació</string>
<string name="purchase_failed">Error en descarregar</string>
<string name="title_blacklist_manager">Gestor de llistes negres</string>
<string name="pref_ui_no_for_you_desc">Visualitzar les Pàgines per a tu a la pantalla d\'inici</string>

View File

@@ -51,7 +51,6 @@
<string name="purchase_unsupported">Aplikace není kompatibilní</string>
<string name="purchase_invalid">Aplikace nebyla zakoupena</string>
<string name="purchase_failed">Stažení se nezdařilo</string>
<string name="pref_ui_similar_apps_desc">Zobrazit podobné a související skupiny na stránce s detaily aplikace</string>
<string name="pref_ui_similar_apps">Podobné a související aplikace</string>
<string name="pref_ui_no_for_you_desc">Zobrazit stránky pro vás na domovské obrazovce</string>
<string name="pref_ui_no_for_you">Stránky pro vás</string>

View File

@@ -105,7 +105,6 @@
<string name="pref_install_mode_am">AM-installationsprogram</string>
<string name="pref_ui_layout">Layout</string>
<string name="purchase_not_found">Appen blev ikke fundet</string>
<string name="pref_ui_similar_apps_desc">Vis lignende og relaterede samlinger på appens detaljeside</string>
<string name="spoof_apply">Sørg for at logge ind igen for at anvende spoofen</string>
<string name="title_manual_download">Manuel download</string>
<string name="toast_apk_whitelisted">Hvidlistet</string>

View File

@@ -208,7 +208,6 @@
<string name="toast_purchase_blocked">App-Käufe sind für anonyme Konten nicht verfügbar.</string>
<string name="insecure_anonymous_apply">Bitte neu anmelden und die App neu starten, um die Änderungen zu übernehmen.</string>
<string name="pref_network_title">Netzwerk</string>
<string name="pref_ui_similar_apps_desc">Ähnliche und verwandte Apps in der App-Detailansicht anzeigen</string>
<string name="pref_ui_similar_apps">Ähnliche Apps</string>
<string name="pref_ui_no_for_you_desc">„Für dich“-Tab auf der Startseite anzeigen</string>
<string name="pref_ui_no_for_you">„Für dich“-Tab</string>

View File

@@ -168,7 +168,6 @@
<string name="pref_ui_title">Προσαρμογή</string>
<string name="pref_ui_no_for_you_desc">Εμφάνιση σελίδων για εσάς στην αρχική οθόνη</string>
<string name="purchase_unsupported">Η εφαρμογή δεν υποστηρίζεται</string>
<string name="pref_ui_similar_apps_desc">Εμφάνιση παρόμοιων και σχετικών ομάδων στη σελίδα λεπτομερειών εφαρμογής</string>
<string name="purchase_session_expired">Η συνεδρία έληξε, συνδεθείτε ξανά για να λάβετε νέα.</string>
<string name="tab_top_paid">Κορυφαίες επί πληρωμή</string>
<string name="title_device">Συσκευή</string>

View File

@@ -176,7 +176,6 @@
<string name="download_eta_min">ankoraŭ %1$d m %2$d s</string>
<string name="pref_ui_title">Agordo</string>
<string name="pref_ui_no_for_you">\"Por vi\" paĝoj</string>
<string name="pref_ui_similar_apps_desc">Vidigi similajn kaj rilatajn apojn sur la paĝo de la apon detaloj</string>
<string name="pref_install_mode_shizuku">Shizuku instalilo</string>
<string name="pref_install_mode_am">AM instalilo</string>
<string name="action_request_analysis">Peti novan analizon</string>

View File

@@ -211,7 +211,6 @@
<string name="toast_manual_available">Felicidades, el código de la versión solicitada está disponible, descargándolo ahora.</string>
<string name="toast_manual_unavailable">El código de versión que estás solicitando no está disponible.</string>
<string name="title_manual_download">Descarga manual</string>
<string name="pref_ui_similar_apps_desc">Mostrar grupos similares y relacionados en la página de detalles de la aplicación</string>
<string name="pref_ui_similar_apps">Aplicaciones similares y relacionadas</string>
<string name="pref_ui_no_for_you_desc">Mostrar las páginas Para ti en la pantalla de inicio</string>
<string name="pref_ui_no_for_you">Páginas para ti</string>

View File

@@ -201,7 +201,6 @@
<string name="notification_channel_updates">Uuenduste märguanded</string>
<string name="permissions_denied">Vajalikud load on keelatud. Toimingu jätkamiseks luba need</string>
<string name="pref_updates_auto">Automaatsed uuendused</string>
<string name="pref_ui_similar_apps_desc">Kuva rakenduse üksikasjade lehel sarnaseid ja seotud rakendusi</string>
<string name="title_manual_download">Käsitsi allalaadimine</string>
<string name="toast_clipboard_copied">Kopeeriti lõikelauale</string>
<string name="toast_manual_available">Õnnitlused, taotletud versioonikood on saadaval, laadime alla.</string>

View File

@@ -216,7 +216,6 @@
<string name="toast_manual_unavailable">Eskatzen ari zaren bertsio kodea ez dago erabilgarri.</string>
<string name="title_manual_download">Eskuzko deskargatzea</string>
<string name="insecure_anonymous_apply">Ziurtatu berriro hasten duzula saioa eta berrabiarazi aplikazioa aldaketak aplikatzeko.</string>
<string name="pref_ui_similar_apps_desc">Erakutsi antzeko taldeak eta erlazionatuak aplikazioaren xehetasunen orrian</string>
<string name="pref_ui_similar_apps">Antzeko aplikazioak eta erlazionatuak</string>
<string name="pref_ui_no_for_you_desc">Erakutsi zuretzako orriak hasierako pantailan</string>
<string name="pref_ui_no_for_you">Zuretzako orrialdeak</string>

View File

@@ -223,7 +223,6 @@
<string name="onboarding_permission_installer_desc">اجازه نصب برنامه‌ها از فروشگاه آرورا</string>
<string name="pref_install_delete_title">حذف APK پس از نصب</string>
<string name="pref_install_mode_am">نصب کننده AM.</string>
<string name="pref_ui_similar_apps_desc">نمایش خوشه های مشابه و مرتبط در صفحه جزئیات برنامه</string>
<string name="spoof_apply">اطمینان حاصل کنید که برای اعمال پارامتر جعلی دوباره وارد سیستم شوید</string>
<string name="tab_editor_choice">انتخاب سردبیران</string>
<string name="title_no_network">بدون شبکه</string>

View File

@@ -187,7 +187,6 @@
<string name="insecure_anonymous_apply">Varmista, että kirjaudut uudelleen ja käynnistät sovelluksen uudelleen ottaaksesi muutokset käyttöön.</string>
<string name="spoof_apply">Varmista, että kirjaudut uudelleen ottaaksesi huijauksen käyttöösi</string>
<string name="purchase_session_expired">Sessio päättyi, kirjaudu uudelleen uuden session aloittamiseksi.</string>
<string name="pref_ui_similar_apps_desc">Näytä sovelluksen lisätietosivulla sen kanssa samankaltaisia ja siihen liittyviä sovelluksia</string>
<string name="pref_ui_no_for_you_desc">Näytä Sinulle räätälöity-sivut kotinäytöllä</string>
<string name="pref_ui_layout_tab">Valitse oletusvälilehti</string>
<string name="pref_ui_layout">Ulkoasu</string>

View File

@@ -209,7 +209,6 @@
<string name="insecure_anonymous_apply">Reconnectez-vous et redémarrez lappli pour appliquer les changements.</string>
<string name="pref_network_title">Mise en réseau</string>
<string name="pref_ui_no_for_you">Pages « Pour vous »</string>
<string name="pref_ui_similar_apps_desc">Afficher les grappes semblables et connexes sur la page des détails de lappli</string>
<string name="pref_ui_similar_apps">Applis semblables et connexes</string>
<string name="pref_ui_no_for_you_desc">Afficher les pages « Pour vous » sur lécran daccueil</string>
<string name="pref_ui_layout_tab">Choisir longlet par défaut</string>

View File

@@ -17,7 +17,6 @@
<string name="purchase_unsupported">Aplicación non soportada</string>
<string name="purchase_invalid">Aplicación non mercada</string>
<string name="purchase_failed">Descarga fallida</string>
<string name="pref_ui_similar_apps_desc">Amosar grupos similares e relacionados na páxina de detalles da aplicación</string>
<string name="pref_ui_similar_apps">Aplicacións semellantes e relacionadas</string>
<string name="pref_ui_no_for_you_desc">Mostrar Suxestións Personalizadas na pantalla de inicio</string>
<string name="pref_ui_no_for_you">Suxestións personalizadas</string>

View File

@@ -207,7 +207,6 @@
<string name="pref_install_mode_session">מתקין מערכת</string>
<string name="pref_ui_layout_tab">בחר עמוד ברירת מחדל</string>
<string name="pref_ui_no_for_you_desc">הצגת עמודים בשבילך במסך הבית</string>
<string name="pref_ui_similar_apps_desc">יישומים דומים וקשורים במסך פרטי היישום</string>
<string name="title_spoof_manager">מנהל הזיוף</string>
<string name="details_beta_description">אתה תראה תכונות חדשות ובאגים לפני כולם. תן משוב למפתחים כדי לעזור להם להשתפר.</string>
<string name="device_miui_extra">אפשרי לבחור מתקין נתיב, אבל לא תוכל להתקין התקנות מאוחדות (מפוצלות - split), אז הבחירה בידך.</string>

View File

@@ -218,7 +218,6 @@
<string name="pref_install_mode_root">रूट इंस्टॉलर</string>
<string name="pref_install_mode_services">ऑरोरा सेवाएं (अस्वीकृत)</string>
<string name="purchase_session_expired">सत्र समाप्त हो गया, नया सत्र प्राप्त करने के लिए पुनः लॉगिन करें।</string>
<string name="pref_ui_similar_apps_desc">ऐप विवरण पृष्ठ पर समान और संबंधित समूह प्रदर्शित करें</string>
<string name="spoof_apply">सुनिश्चित करें कि आप छलावरण लागू करने के लिए पुनः लॉगिन करें</string>
<string name="purchase_unsupported">ऐप समर्थित नहीं है</string>
<string name="toast_apk_whitelisted">श्वेतसूचीबद्ध</string>

View File

@@ -211,7 +211,6 @@
<string name="toast_manual_available">Čestitamo, traženi kod verzije je dostupan, preuzima se.</string>
<string name="toast_manual_unavailable">Zatraženi kod verzije nije dostupan.</string>
<string name="title_manual_download">Ručno preuzimanje</string>
<string name="pref_ui_similar_apps_desc">Prikaži slične i srodne skupove podataka na stranici detalja aplikacije</string>
<string name="pref_ui_similar_apps">Slične i srodne aplikacije</string>
<string name="pref_ui_no_for_you_desc">Prikaži „Za tvoje stranice” na početnom ekranu</string>
<string name="pref_ui_no_for_you">Za tvoje stranice</string>

View File

@@ -208,7 +208,6 @@
<string name="action_installations">Telepítések</string>
<string name="action_disable">Tiltás</string>
<string name="action_clear">Törlés</string>
<string name="pref_ui_similar_apps_desc">Hasonló és releváns csoportok megjelenítése a részletek lapon</string>
<string name="pref_ui_similar_apps">Hasonló és releváns alkalmazások</string>
<string name="pref_ui_no_for_you_desc">„Önnek ajánlott” oldal megjelenítése a kezdőlapon</string>
<string name="pref_ui_no_for_you">Önnek ajánlott oldalak</string>

View File

@@ -198,7 +198,6 @@
<string name="purchase_not_found">Aplikasi tidak ditemukan</string>
<string name="purchase_unsupported">Aplikasi tidak didukung</string>
<string name="purchase_invalid">Aplikasi tidak dibeli</string>
<string name="pref_ui_similar_apps_desc">Tampilkan kelompok yang serupa dan terkait pada halaman detail aplikasi</string>
<string name="pref_ui_similar_apps">Aplikasi serupa dan terkait</string>
<string name="pref_ui_no_for_you_desc">Tampilkan halaman Untuk Anda di layar beranda</string>
<string name="pref_ui_no_for_you">Halaman Untuk Anda</string>

View File

@@ -208,7 +208,6 @@
<string name="toast_purchase_blocked">Gli acquisti di app non sono disponibili sui profili anonimi.</string>
<string name="insecure_anonymous_apply">Assicurati di effettuare nuovamente l\'accesso e di riavviare l\'app per applicare le modifiche.</string>
<string name="pref_network_title">Rete</string>
<string name="pref_ui_similar_apps_desc">Visualizza i gruppi di app simili e correlate nella pagina dei dettagli dell\'app</string>
<string name="pref_ui_similar_apps">App simili e correlate</string>
<string name="pref_ui_no_for_you_desc">Visualizza le pagine «Per te» sulla schermata iniziale</string>
<string name="pref_ui_no_for_you">Pagine «Per te»</string>

View File

@@ -212,7 +212,6 @@
<string name="toast_manual_unavailable">要求したバージョン番号は利用できません。</string>
<string name="toast_manual_available">おめでとう!要求したバージョンコードが利用できます。ダウンロード中。</string>
<string name="title_manual_download">手動ダウンロード</string>
<string name="pref_ui_similar_apps_desc">類似のアプリをアプリ詳細ページに表示する</string>
<string name="pref_ui_similar_apps">類似のアプリ</string>
<string name="pref_ui_no_for_you_desc">ホーム画面におすすめページを表示する</string>
<string name="pref_ui_no_for_you">おすすめページ</string>

View File

@@ -216,7 +216,6 @@
<string name="toast_manual_unavailable">Koda guhertoya ku tu dixwazî tune ye.</string>
<string name="title_manual_download">Bi destan daxe</string>
<string name="insecure_anonymous_apply">Ji bo sepandina guhertinan zanibe be ku tu ji nû ve têketiye û sepanê ji nû ve daye destpêkirin.</string>
<string name="pref_ui_similar_apps_desc">Komên wekhev û têkildar li ser rûpela hûrgiliyên sepanê nîşan bide</string>
<string name="pref_ui_similar_apps">Sepanên wekhev û têkildar</string>
<string name="pref_ui_no_for_you_desc">Rûpelên Bo Te li ser dîmendera malê rê bide</string>
<string name="pref_ui_no_for_you">Rûpelên Bo Te</string>

View File

@@ -207,7 +207,6 @@
<string name="title_apps">앱스</string>
<string name="title_app_settings">앱 설정</string>
<string name="purchase_failed">다운로드 실패됨</string>
<string name="pref_ui_similar_apps_desc">앱 세부 페이지에서 유사한 및 관련 클러스터 표시</string>
<string name="action_request_analysis">새 분석 요청</string>
<string name="action_home_screen">홈 화면에 추가</string>
<string name="download_clear_finished">완료건 지우기</string>

View File

@@ -151,7 +151,6 @@
<string name="download_force_clear_all">Priverstinai išvalyti visus</string>
<string name="download_pause_all">Pristabdyti visus</string>
<string name="pref_ui_no_for_you">„Jums“ puslapiai</string>
<string name="pref_ui_similar_apps_desc">Rodyti programėlės puslapyje panašių ir susijusių programėlių rinkinį</string>
<string name="tab_editor_choice">Redaktoriaus pasirinkimas</string>
<string name="tab_for_you">Jums</string>
<string name="title_apps_library">Programėlės bibliotekoje</string>

View File

@@ -111,7 +111,6 @@
<string name="pref_ui_no_for_you">Lapas \"Tev\"</string>
<string name="pref_ui_no_for_you_desc">Rādīt lapas \"Tev\" sākuma ekrānā</string>
<string name="pref_ui_similar_apps">Līdzīgas un saistītas lietotnes</string>
<string name="pref_ui_similar_apps_desc">Rādīt līdzīgas un saistītas lietotnes</string>
<string name="purchase_failed">Lejupielāde neizdevās</string>
<string name="tab_top_paid">Top maksas</string>
<string name="tab_trending">Populāri</string>

View File

@@ -187,7 +187,6 @@
<string name="toast_developer_setting_failed">Skru på utviklingsinnstillingene fra enhetsinnstillingene for å åpne dem.</string>
<string name="toast_page_unavailable">Utforskningssiden er utilgjengelig</string>
<string name="toast_spoof_applied">Enhetslureri i bruk.</string>
<string name="pref_ui_similar_apps_desc">Vis lignende og relaterte klynger på appdetaljsiden</string>
<string name="pref_ui_no_for_you_desc">Vis For You-sider på startskjermen</string>
<string name="installer_service_misconfigured">Sett opp Aurora-tjenester og innvilg alle tilganger først.</string>
<string name="title_spoof_manager">Lureri-håndterer</string>

View File

@@ -70,7 +70,6 @@
<string name="purchase_unsupported">App niet ondersteund</string>
<string name="purchase_invalid">App niet aangeschaft</string>
<string name="purchase_failed">Installatie mislukt</string>
<string name="pref_ui_similar_apps_desc">Toon vergelijkbare en gerelateerde clusters op de app-detailspagina</string>
<string name="pref_ui_similar_apps">Vergelijkbare en gerelateerde apps</string>
<string name="pref_ui_no_for_you_desc">Pagina met aanbevelingen op thuisscherm tonen</string>
<string name="pref_ui_no_for_you">Voor jou pagina\'s</string>

View File

@@ -55,7 +55,6 @@
<string name="purchase_unsupported">ਐਪ ਸਪੋਰਟਡ ਨਹੀਂ</string>
<string name="purchase_invalid">ਇਹ ਐਪ ਅਜੇ ਤੁਹਾਡੇ ਵਲੋਂ ਖਰੀਦਿਆ ਨਹੀਂ ਗਿਆ</string>
<string name="purchase_failed">ਡਾਊਨਲੋਡ ਫੇਲ੍ਹ</string>
<string name="pref_ui_similar_apps_desc">ਐਪ ਜਾਣਕਾਰੀ ਵਾਲੇ ਪੇਜ ਤੇ ਇਸੇ ਸ਼੍ਰੇਣੀ ਦੇ ਕੁੱਝ ਹੋਰ ਐਪਸ ਵਾਲਾ ਖ਼ਾਨਾ ਵਿਖਾਓ</string>
<string name="pref_ui_similar_apps">ਇਸੇ ਸ਼੍ਰੇਣੀ ਦੇ ਕੁੱਝ ਹੋਰ ਐਪਸ</string>
<string name="pref_ui_no_for_you_desc">ਖਾਸ ਤੁਹਾਡੇ ਲਈ ਚੁਣੇ ਹੋਏ ਐਪਸ ਵਾਲਾ ਪੰਨਾ ਵਿਖਾਓ</string>
<string name="pref_ui_no_for_you">ਤੁਹਾਡੇ ਲਈ ਚੁਣੇ ਹੋਏ ਐਪਸ</string>

View File

@@ -211,7 +211,6 @@
<string name="pref_common_extra">Dodatkowe</string>
<string name="pref_ui_no_for_you">Strony dla Ciebie</string>
<string name="pref_ui_no_for_you_desc">Wyświetl strony Dla Ciebie na ekranie głównym</string>
<string name="pref_ui_similar_apps_desc">Wyświetl grupy podobnych i powiązanych na stronie szczegółów aplikacji</string>
<string name="pref_ui_similar_apps">Aplikacje podobne i powiązane</string>
<string name="pref_ui_layout_tab">Wybierz domyślną kartę</string>
<string name="pref_ui_layout">Układ</string>

View File

@@ -211,7 +211,6 @@
<string name="toast_manual_available">O código da versão solicitada está disponível, baixando agora.</string>
<string name="toast_manual_unavailable">O código de versão que você solicitou não está disponível.</string>
<string name="title_manual_download">Baixar manualmente</string>
<string name="pref_ui_similar_apps_desc">Exibir apps semelhantes e relacionados na página de detalhes do app</string>
<string name="pref_ui_no_for_you_desc">Exibir páginas de recomendação na tela inicial</string>
<string name="pref_network_title">Rede</string>
<string name="details_no_app_match">Não há apps correspondentes</string>

View File

@@ -83,7 +83,6 @@
<string name="purchase_unsupported">Aplicação não suportada</string>
<string name="purchase_invalid">Aplicação não comprada</string>
<string name="purchase_failed">Descarga falhou</string>
<string name="pref_ui_similar_apps_desc">Mostrar grupos de aplicações semelhantes e relacionadas na página de detalhes das aplicações</string>
<string name="pref_ui_similar_apps">Aplicações semelhantes e relacionadas</string>
<string name="pref_ui_no_for_you_desc">Exibir páginas de recomendação no ecrã inicial</string>
<string name="pref_ui_no_for_you">Páginas de recomendação</string>

View File

@@ -207,7 +207,6 @@
<string name="toast_clipboard_copied">Copiat în clipboard</string>
<string name="pref_install_mode_session">Instalator sesiune</string>
<string name="pref_install_mode_title">Metoda de instalare</string>
<string name="pref_ui_similar_apps_desc">Afișare grupuri similare și asociate pe pagina de detalii a aplicației</string>
<string name="pref_network_title">Rețea</string>
<string name="session_good">Vai! Toate bune.</string>
<string name="title_purchase_history">Istoricul achizițiilor</string>

View File

@@ -208,7 +208,6 @@
<string name="toast_purchase_blocked">Покупка приложений недоступна когда анонимно.</string>
<string name="insecure_anonymous_apply">Убедитесь, что выполнили новый вход и перезапустили приложение для применения изменений.</string>
<string name="pref_network_title">Сеть</string>
<string name="pref_ui_similar_apps_desc">Показать секции похожих и связанных на странице приложения</string>
<string name="pref_ui_no_for_you_desc">Показать страницу рекомендаций на главном экране</string>
<string name="pref_ui_layout_tab">Вкладка по умолчанию</string>
<string name="pref_ui_layout">Вкладки</string>

View File

@@ -215,7 +215,6 @@
<string name="about_bhim">BHIM - UPI</string>
<string name="about_bhim_summary">Dona pro mèdiu de UPI</string>
<string name="download_metadata">Retzende meta-datos</string>
<string name="pref_ui_similar_apps_desc">Ammustra sos grupos de aplicatziones simigiantes e ligados in sa pàgina de sos detàllios de s\'aplicatzione</string>
<string name="about_fdroid_summary">Otene Aurora Store pro mèdiu de F-Droid.</string>
<string name="pref_install_mode_services">Servìtzios de Aurora</string>
<string name="title_installation">Installatzione</string>

View File

@@ -234,7 +234,6 @@
<string name="download_eta_min">වි. %1$d ත. %2$d ක් ඉතිරිය</string>
<string name="download_eta_sec">ත. %1$d ක් ඉතිරිය</string>
<string name="onboarding_permission_notifications">දැනුම්දීම්</string>
<string name="pref_ui_similar_apps_desc">යෙදුමේ විස්තර පිටුවේ සමාන හා ආශ්‍රිත පර්ෂද පෙන්වන්න</string>
<string name="session_good">ඔක්කොම හරි!</string>
<string name="session_enjoy">ඇතුළු වී අත්විඳින්න.</string>
<string name="session_init">සූදානම් කෙරෙමින්…</string>

View File

@@ -140,7 +140,6 @@
<string name="pref_install_mode_shizuku">Shizuku inštalátor</string>
<string name="pref_install_mode_session">Inštalátor relácie</string>
<string name="pref_vending_version_device">Predvolené (z konfigurácie zariadenia)</string>
<string name="pref_ui_similar_apps_desc">Zobraziť podobné a súvisiace skupiny na stránke podrobností aplikácie</string>
<string name="pref_updates_incompatible">Nekompatibilné aktualizácie</string>
<string name="pref_updates_incompatible_desc">Zobraziť aktualizácie pre nekompatibilné alebo zakázané aplikácie, ktoré môžu zlyhať pri nštalovaní</string>
<string name="pref_aurora_only">Iba aplikácie Aurora obchod</string>

View File

@@ -272,7 +272,6 @@
<string name="pref_filter_fdroid_title">Filtrirajte F-Droid aplikacije</string>
<string name="pref_ui_title">Prilagajanje</string>
<string name="pref_ui_no_for_you">Strani za vas</string>
<string name="pref_ui_similar_apps_desc">Prikaži podobne in sorodne gruče na strani s podrobnostmi o aplikaciji</string>
<string name="menu">Meni</string>
<string name="about_wiki_title">Wiki</string>
<string name="about_wiki_summary">Poiščite odgovore na pogosto zastavljena vprašanja (F.A.Q.), korake za odpravljanje težav in več</string>

View File

@@ -82,7 +82,6 @@
<string name="purchase_unsupported">Appka lama taageerin</string>
<string name="purchase_invalid">Appka lama iibsanin</string>
<string name="purchase_failed">Dajintii maguulaysan</string>
<string name="pref_ui_similar_apps_desc">Soodhig appska lamidka ah iyo kuwa laxidhiidha bogga faahfaahinta appka</string>
<string name="pref_ui_similar_apps">Lamid ah &amp; laxidhiidha</string>
<string name="pref_ui_no_for_you_desc">Boggaga Adiga shaashada hore soodhig</string>
<string name="pref_ui_no_for_you">Boggaga Adiga</string>

View File

@@ -173,7 +173,6 @@
<string name="pref_ui_no_for_you">Faqe “Për ju”</string>
<string name="pref_ui_no_for_you_desc">Shfaq te skena e kreut faqe “Për ju”</string>
<string name="pref_ui_similar_apps">Aplikacione të ngjashme dhe të afërta</string>
<string name="pref_ui_similar_apps_desc">Shfaq te faqja e hollësive të aplikacionit grumbuj të ngjashëm dhe të afërt</string>
<string name="purchase_failed">Shkarkimi dështoi</string>
<string name="purchase_invalid">Aplikacion jo i blerë</string>
<string name="purchase_unsupported">Aplikacioni nuk mbulohet</string>

View File

@@ -215,7 +215,6 @@
<string name="toast_manual_unavailable">Кôд верзије који тражите није доступан.</string>
<string name="title_manual_download">Ручно преузимање</string>
<string name="insecure_anonymous_apply">Обавезно се поново пријавите и поново покрените апликацију да бисте применили промене.</string>
<string name="pref_ui_similar_apps_desc">Прикажите сличне и сродне скупове на страници с детаљима о апликацији</string>
<string name="pref_ui_similar_apps">Сличне и сродне апликације</string>
<string name="pref_ui_no_for_you_desc">Прикажите странице за вас на почетном екрану</string>
<string name="pref_ui_no_for_you">Странице за вас</string>

View File

@@ -202,7 +202,6 @@
<string name="toast_export_failed">Kunde inte exportera enhetens konfiguration</string>
<string name="toast_export_success">Enhetens konfiguration exporterades</string>
<string name="toast_apk_blacklisted">Svartlistad</string>
<string name="pref_ui_similar_apps_desc">Visa liknande och relaterade klustrar på appens detaljsida</string>
<string name="onboarding_permission_esm">Hanterare av externt lagringsutrymme</string>
<string name="details_no_permission">Inga behörigheter</string>
<string name="action_pending">Avvaktande</string>

Some files were not shown because too many files have changed in this diff Show More