Add one-click install for favourite apps
Addresses #1508 Add an 'Install all' action to the Favourites screen that bulk-fetches details and enqueues every installable favourite (skipping already installed apps and paid apps anonymous accounts can't acquire), gated behind a confirmation dialog that warns about per-app install prompts. Show live state on each favourite row: an installed tick that updates on install/uninstall events, and download/install progress mirroring the Updates screen. Hide 'Install all' once every favourite is installed. Treat only non-finished download statuses as in progress on both the Favourites and Updates rows so a dismissed system install prompt no longer leaves a row stuck showing 'Installing'.
This commit is contained in:
@@ -14,7 +14,11 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -30,49 +34,94 @@ import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.AuroraApp
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.app.AnimatedAppIcon
|
||||
import com.aurora.store.compose.preview.AppPreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.theme.colorGreen
|
||||
import com.aurora.store.compose.theme.colorRed
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.data.model.DownloadStatus
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.data.room.favourite.Favourite
|
||||
import com.aurora.store.util.PackageUtil
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
@Composable
|
||||
fun FavouriteListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
favourite: Favourite,
|
||||
download: Download? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onClear: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isInstalled = remember(favourite.packageName) {
|
||||
PackageUtil.isInstalled(context, favourite.packageName)
|
||||
|
||||
// Seed from a one-shot check, then keep it live so the installed tick reflects the app
|
||||
// being installed or removed without leaving the screen.
|
||||
var isInstalled by remember(favourite.packageName) {
|
||||
mutableStateOf(PackageUtil.isInstalled(context, favourite.packageName))
|
||||
}
|
||||
LaunchedEffect(favourite.packageName) {
|
||||
AuroraApp.events.installerEvent
|
||||
.filter { it.packageName == favourite.packageName }
|
||||
.collect { event ->
|
||||
when (event) {
|
||||
is InstallerEvent.Installed -> isInstalled = true
|
||||
is InstallerEvent.Uninstalled -> isInstalled = false
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show download/install progress on the row; only non-finished statuses count as in
|
||||
// progress, so a downloaded-but-not-installed (COMPLETED) app stays in its idle state.
|
||||
val inProgress = download != null && !download.isFinished
|
||||
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||
download.progress.toFloat()
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
val statusText = when {
|
||||
download?.status == DownloadStatus.DOWNLOADING ->
|
||||
"${stringResource(R.string.status_downloading)} • ${download.progress}%"
|
||||
|
||||
inProgress && download != null -> stringResource(download.status.localized)
|
||||
else -> DateUtils.formatDateTime(context, favourite.added, DateUtils.FORMAT_SHOW_DATE)
|
||||
}
|
||||
|
||||
RemovableListItem(onRemove = onClear) { triggerRemove ->
|
||||
AuroraListItem(
|
||||
modifier = modifier,
|
||||
headline = favourite.displayName,
|
||||
supporting = favourite.packageName,
|
||||
tertiary = DateUtils.formatDateTime(
|
||||
context,
|
||||
favourite.added,
|
||||
DateUtils.FORMAT_SHOW_DATE
|
||||
),
|
||||
tertiary = statusText,
|
||||
headlineStyle = MaterialTheme.typography.bodyMedium,
|
||||
onClick = onClick,
|
||||
leading = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(favourite.iconURL)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
)
|
||||
if (inProgress) {
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||
iconUrl = favourite.iconURL,
|
||||
inProgress = true,
|
||||
progress = progress
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(favourite.iconURL)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||
)
|
||||
}
|
||||
},
|
||||
trailing = {
|
||||
Row(
|
||||
@@ -81,7 +130,7 @@ fun FavouriteListItem(
|
||||
dimensionResource(R.dimen.spacing_xsmall)
|
||||
)
|
||||
) {
|
||||
if (isInstalled) {
|
||||
if (isInstalled && !inProgress) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
contentDescription = stringResource(R.string.title_installed),
|
||||
@@ -107,3 +156,18 @@ fun FavouriteListItem(
|
||||
private fun FavouriteListItemPreview(@PreviewParameter(AppPreviewProvider::class) app: App) {
|
||||
FavouriteListItem(favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL))
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FavouriteListItemDownloadingPreview(
|
||||
@PreviewParameter(AppPreviewProvider::class) app: App
|
||||
) {
|
||||
FavouriteListItem(
|
||||
favourite = Favourite.fromApp(app, Favourite.Mode.MANUAL),
|
||||
download = Download.fromApp(app).copy(
|
||||
status = DownloadStatus.DOWNLOADING,
|
||||
progress = 45
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,8 +43,10 @@ fun AppUpdateItem(
|
||||
onCancel: () -> Unit = {},
|
||||
onUnignore: (() -> Unit)? = null
|
||||
) {
|
||||
// Only the INSTALLING status shows as "Installing"; a downloaded-but-not-installed
|
||||
// (COMPLETED) app falls back to the "Update" action.
|
||||
val inProgress = download != null && !download.isFinished
|
||||
val pendingInstall = download?.status == DownloadStatus.COMPLETED
|
||||
val installing = download?.status == DownloadStatus.INSTALLING
|
||||
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||
download.progress.toFloat()
|
||||
} else {
|
||||
@@ -65,7 +67,7 @@ fun AppUpdateItem(
|
||||
AnimatedAppIcon(
|
||||
modifier = Modifier.requiredSize(dimensionResource(R.dimen.icon_size_medium)),
|
||||
iconUrl = update.iconURL,
|
||||
inProgress = inProgress || pendingInstall,
|
||||
inProgress = inProgress,
|
||||
progress = progress
|
||||
)
|
||||
}
|
||||
@@ -100,7 +102,7 @@ fun AppUpdateItem(
|
||||
}
|
||||
}
|
||||
|
||||
pendingInstall -> {
|
||||
installing -> {
|
||||
OutlinedButton(onClick = {}, enabled = false) {
|
||||
Text(stringResource(R.string.action_installing))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.compose.ui.commons
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
|
||||
/**
|
||||
* Confirmation dialog for bulk-installing all favourite apps.
|
||||
* @param count Number of favourites that will be processed
|
||||
* @param onConfirm Callback on confirmation
|
||||
* @param onDismiss Callback on dismissal
|
||||
*/
|
||||
@Composable
|
||||
fun InstallFavouritesDialog(
|
||||
count: Int = 0,
|
||||
onConfirm: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.title_install_favourites)) },
|
||||
text = {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.spacing_small)
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.install_favourites_summary, count))
|
||||
Text(text = stringResource(R.string.install_favourites_warning))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = stringResource(R.string.action_install))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InstallFavouritesDialogPreview() {
|
||||
InstallFavouritesDialog(count = 5)
|
||||
}
|
||||
@@ -15,9 +15,13 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -25,11 +29,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
@@ -43,12 +49,15 @@ import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||
import com.aurora.store.compose.composable.FavouriteListItem
|
||||
import com.aurora.store.compose.composable.Placeholder
|
||||
import com.aurora.store.compose.composable.ScrollHint
|
||||
import com.aurora.store.compose.composable.SectionHeader
|
||||
import com.aurora.store.compose.composable.TopAppBar
|
||||
import com.aurora.store.compose.navigation.Destination
|
||||
import com.aurora.store.compose.preview.FavouritePreviewProvider
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.ui.commons.InstallFavouritesDialog
|
||||
import com.aurora.store.compose.ui.favourite.menu.FavouriteMenu
|
||||
import com.aurora.store.compose.ui.favourite.menu.MenuItem
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.data.room.favourite.Favourite
|
||||
import com.aurora.store.viewmodel.all.FavouriteViewModel
|
||||
import java.util.Calendar
|
||||
@@ -62,6 +71,24 @@ fun FavouriteScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val favourites = viewModel.favourites.collectAsLazyPagingItems()
|
||||
val isEnqueuing by viewModel.isEnqueuing.collectAsStateWithLifecycle()
|
||||
val hasInstallableFavourites by viewModel.hasInstallableFavourites
|
||||
.collectAsStateWithLifecycle()
|
||||
val downloads by viewModel.downloadsList.collectAsStateWithLifecycle()
|
||||
val downloadMap = remember(downloads) { downloads.associateBy { it.packageName } }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.enqueueResult.collect { count ->
|
||||
when {
|
||||
count > 0 -> context.toast(
|
||||
context.getString(R.string.toast_fav_install_enqueued, count)
|
||||
)
|
||||
|
||||
count == 0 -> context.toast(R.string.toast_fav_install_none)
|
||||
else -> context.toast(R.string.toast_fav_install_failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val documentOpenLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
@@ -89,7 +116,11 @@ fun FavouriteScreen(
|
||||
|
||||
ScreenContent(
|
||||
favourites = favourites,
|
||||
downloads = downloadMap,
|
||||
isEnqueuing = isEnqueuing,
|
||||
showInstallAll = hasInstallableFavourites,
|
||||
onNavigateTo = onNavigateTo,
|
||||
onInstallAll = { viewModel.installAll() },
|
||||
onRemoveFavourite = { packageName -> viewModel.removeFavourite(packageName) },
|
||||
onImportFavourites = {
|
||||
documentOpenLauncher.launch(arrayOf(JSON_MIME_TYPE))
|
||||
@@ -105,7 +136,11 @@ fun FavouriteScreen(
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
favourites: LazyPagingItems<Favourite> = emptyPagingItems(),
|
||||
downloads: Map<String, Download> = emptyMap(),
|
||||
isEnqueuing: Boolean = false,
|
||||
showInstallAll: Boolean = true,
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
onInstallAll: () -> Unit = {},
|
||||
onRemoveFavourite: (packageName: String) -> Unit = {},
|
||||
onImportFavourites: () -> Unit = {},
|
||||
onExportFavourites: () -> Unit = {}
|
||||
@@ -117,6 +152,18 @@ private fun ScreenContent(
|
||||
* Save the initial loading state to make sure we don't replay the loading animation again.
|
||||
*/
|
||||
var initialLoad by rememberSaveable { mutableStateOf(true) }
|
||||
var showInstallDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showInstallDialog) {
|
||||
InstallFavouritesDialog(
|
||||
count = favourites.itemCount,
|
||||
onConfirm = {
|
||||
showInstallDialog = false
|
||||
onInstallAll()
|
||||
},
|
||||
onDismiss = { showInstallDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetupMenu() {
|
||||
@@ -162,6 +209,29 @@ private fun ScreenContent(
|
||||
LazyColumn(
|
||||
state = listState
|
||||
) {
|
||||
item(key = "header") {
|
||||
SectionHeader(
|
||||
title = pluralStringResource(
|
||||
R.plurals.favourites_count,
|
||||
favourites.itemCount,
|
||||
favourites.itemCount
|
||||
),
|
||||
trailing = if (showInstallAll) {
|
||||
{
|
||||
TextButton(
|
||||
onClick = { showInstallDialog = true },
|
||||
enabled = !isEnqueuing
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.action_install_all)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
items(
|
||||
count = favourites.itemCount,
|
||||
key = favourites.itemKey { it.packageName }
|
||||
@@ -170,6 +240,7 @@ private fun ScreenContent(
|
||||
FavouriteListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
favourite = favourite,
|
||||
download = downloads[favourite.packageName],
|
||||
onClick = {
|
||||
onNavigateTo(
|
||||
Destination.AppDetails(favourite.packageName)
|
||||
|
||||
@@ -14,20 +14,35 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.aurora.extensions.TAG
|
||||
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
||||
import com.aurora.store.AuroraApp
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.data.helper.DownloadHelper
|
||||
import com.aurora.store.data.paging.GenericPagingSource.Companion.pager
|
||||
import com.aurora.store.data.providers.AuthProvider
|
||||
import com.aurora.store.data.room.favourite.Favourite
|
||||
import com.aurora.store.data.room.favourite.FavouriteDao
|
||||
import com.aurora.store.data.room.favourite.ImportExport
|
||||
import com.aurora.store.util.PackageUtil
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -35,12 +50,40 @@ import kotlinx.serialization.json.Json
|
||||
class FavouriteViewModel @Inject constructor(
|
||||
private val favouriteDao: FavouriteDao,
|
||||
private val json: Json,
|
||||
private val appDetailsHelper: AppDetailsHelper,
|
||||
private val downloadHelper: DownloadHelper,
|
||||
private val authProvider: AuthProvider,
|
||||
@ApplicationContext private val context: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val _favourites = MutableStateFlow<PagingData<Favourite>>(PagingData.empty())
|
||||
val favourites = _favourites.asStateFlow()
|
||||
|
||||
val downloadsList get() = downloadHelper.downloadsList
|
||||
|
||||
private val _isEnqueuing = MutableStateFlow(false)
|
||||
val isEnqueuing = _isEnqueuing.asStateFlow()
|
||||
|
||||
// Emits the number of favourites actually enqueued for install (0 when nothing was
|
||||
// applicable, -1 when fetching details failed) so the screen can show accurate feedback.
|
||||
private val _enqueueResult = MutableSharedFlow<Int>()
|
||||
val enqueueResult = _enqueueResult.asSharedFlow()
|
||||
|
||||
// Whether at least one favourite is still not installed, recomputed whenever the favourites
|
||||
// list changes or any app is installed/removed. Drives visibility of the "Install all"
|
||||
// action so it hides once every favourite is already installed. Defaults to true to avoid
|
||||
// briefly hiding the action before the first check completes.
|
||||
val hasInstallableFavourites = combine(
|
||||
favouriteDao.favourites(),
|
||||
AuroraApp.events.installerEvent
|
||||
.filter { it is InstallerEvent.Installed || it is InstallerEvent.Uninstalled }
|
||||
.map { }
|
||||
.onStart { emit(Unit) }
|
||||
) { favourites, _ ->
|
||||
favourites.any { !PackageUtil.isInstalled(context, it.packageName) }
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
|
||||
|
||||
init {
|
||||
getPagedFavourites()
|
||||
}
|
||||
@@ -86,6 +129,34 @@ class FavouriteViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches details for all favourites and enqueues the installable ones for download &
|
||||
* install. Already installed (and up-to-date) apps are skipped, as are paid apps that an
|
||||
* anonymous account can't acquire. The actual count enqueued is emitted via [enqueueResult].
|
||||
*/
|
||||
fun installAll() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_isEnqueuing.value = true
|
||||
try {
|
||||
val packageNames = favouriteDao.favourites().first().map { it.packageName }
|
||||
val installable = appDetailsHelper.getAppByPackageName(packageNames).filter { app ->
|
||||
val needsInstall = !PackageUtil.isInstalled(context, app.packageName) ||
|
||||
PackageUtil.isUpdatable(context, app.packageName, app.versionCode)
|
||||
val acquirable = app.isFree || !authProvider.isAnonymous
|
||||
needsInstall && acquirable
|
||||
}
|
||||
|
||||
installable.forEach { downloadHelper.enqueueApp(it) }
|
||||
_enqueueResult.emit(installable.size)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, "Failed to enqueue favourites for install", exception)
|
||||
_enqueueResult.emit(-1)
|
||||
} finally {
|
||||
_isEnqueuing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPagedFavourites() {
|
||||
pager { favouriteDao.pagedFavourites() }.flow
|
||||
.distinctUntilChanged()
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
<string name="action_granted">"Granted"</string>
|
||||
<string name="action_ignore">"Ignore"</string>
|
||||
<string name="action_install">"Install"</string>
|
||||
<string name="action_install_all">"Install all"</string>
|
||||
<string name="action_install_microG">"Install microG Bundle"</string>
|
||||
<string name="action_installations">"Installations"</string>
|
||||
<string name="action_installing">"Installing"</string>
|
||||
@@ -296,6 +297,9 @@
|
||||
<string name="title_installation">"Installation"</string>
|
||||
<string name="title_installed">"Installed"</string>
|
||||
<string name="title_installer">"App installer"</string>
|
||||
<string name="title_install_favourites">"Install favourites?"</string>
|
||||
<string name="install_favourites_summary">"This will download and install %1$d apps."</string>
|
||||
<string name="install_favourites_warning">"You may see a separate installation prompt for each app."</string>
|
||||
<string name="title_language">"Language"</string>
|
||||
<string name="title_library">"Library"</string>
|
||||
<string name="title_no_network">"No network"</string>
|
||||
@@ -508,6 +512,9 @@
|
||||
<string name="toast_fav_export_failed">Failed to export favourites!</string>
|
||||
<string name="toast_fav_import_success">Favourites imported!</string>
|
||||
<string name="toast_fav_export_success">Favourites exported!</string>
|
||||
<string name="toast_fav_install_enqueued">"Installing %1$d apps"</string>
|
||||
<string name="toast_fav_install_none">"Nothing to install"</string>
|
||||
<string name="toast_fav_install_failed">"Failed to fetch app details"</string>
|
||||
|
||||
<!-- BlacklistFragment-->
|
||||
<string name="toast_black_import_failed">Failed to import blacklist!</string>
|
||||
@@ -586,6 +593,10 @@
|
||||
<item quantity="one">Permission required</item>
|
||||
<item quantity="other">Permissions required</item>
|
||||
</plurals>
|
||||
<plurals name="favourites_count">
|
||||
<item quantity="one">%1$d favourite</item>
|
||||
<item quantity="other">%1$d favourites</item>
|
||||
</plurals>
|
||||
<plurals name="notification_installed_summary">
|
||||
<item quantity="one">%d app installed</item>
|
||||
<item quantity="other">%d apps installed</item>
|
||||
|
||||
Reference in New Issue
Block a user