Loading app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +27 −20 Original line number Original line Diff line number Diff line Loading @@ -113,30 +113,37 @@ class AppManagerWrapper @Inject constructor( val appDownload = getDownloadList() val appDownload = getDownloadList() .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } ?: return 0 ?: return 0 return calculateProgress(appDownload, progress) if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { return@let } } return 0 if (!isProgressValidForApp(application, progress)) { return -1 } } val downloadingMap = progress.totalSizeBytes.filter { item -> suspend fun calculateProgress( appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 appInstall: AppInstall, progress: DownloadProgress ): Int { val downloadIds = appInstall.downloadIdMap.keys if (downloadIds.isEmpty()) { // Download request exists but ids not yet populated; show 0% instead of dropping percent. return 0 } } if (appDownload.downloadIdMap.size > downloadingMap.size) { // All files for download are not ready yet val totalSizeBytes = progress.totalSizeBytes .filterKeys { downloadIds.contains(it) } .values .sum() if (totalSizeBytes <= 0) { return 0 return 0 } } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> .filterKeys { downloadIds.contains(it) } appDownload.downloadIdMap.keys.contains(item.key) .values }.values.sum() .sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() } return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100) return 0 .toInt() .coerceIn(0, 100) } } private suspend fun isProgressValidForApp( private suspend fun isProgressValidForApp( Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +3 −2 Original line number Original line Diff line number Diff line Loading @@ -57,8 +57,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter import foundation.e.apps.R import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Application import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.theme.AppTheme @Composable @Composable fun SearchResultListItem( fun SearchResultListItem( Loading Loading @@ -303,7 +303,8 @@ private fun PrivacyBadge( Button( Button( onClick = onPrimaryClick, onClick = onPrimaryClick, enabled = uiState.enabled, enabled = uiState.enabled, modifier = Modifier.height(40.dp), modifier = Modifier .height(40.dp), shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors( containerColor = when { containerColor = when { Loading app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusReconciler.kt +19 −5 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,7 @@ class InstallStatusReconciler @Inject constructor( // Prefer matching active download // Prefer matching active download val activeDownload = snapshot.downloads.find { matches(app, it) } val activeDownload = snapshot.downloads.find { matches(app, it) } if (activeDownload != null) { if (activeDownload != null) { val progressPercent = progressPercent(app, progress) val progressPercent = progressPercent(activeDownload, progress) app.status = activeDownload.status app.status = activeDownload.status return Result(app, progressPercent) return Result(app, progressPercent) } } Loading @@ -46,15 +46,29 @@ class InstallStatusReconciler @Inject constructor( } } private fun matches(app: Application, install: AppInstall): Boolean { private fun matches(app: Application, install: AppInstall): Boolean { return install.packageName == app.package_name || install.id == app._id val pkg = app.package_name val id = app._id return install.packageName == pkg || install.id == id || install.id == pkg } } private suspend fun progressPercent( private suspend fun progressPercent( app: Application, activeDownload: AppInstall, progress: DownloadProgress? progress: DownloadProgress? ): Int? { ): Int? { if (progress == null) return null if (progress == null) return null val percent = appManagerWrapper.calculateProgress(app, progress) val percent = appManagerWrapper.calculateProgress(activeDownload, progress) return percent.takeIf { it in 0..100 } if (percent in 0..100) return percent // Fallback: compute from the last downloadId emitted by DownloadProgress val id = progress.downloadId.takeIf { it != -1L } if (id != null && activeDownload.downloadIdMap.containsKey(id)) { val total = progress.totalSizeBytes[id] ?: return null if (total <= 0) return null val done = progress.bytesDownloadedSoFar[id] ?: 0L return ((done / total.toDouble()) * 100).toInt().coerceIn(0, 100) } return null } } } } app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +4 −7 Original line number Original line Diff line number Diff line Loading @@ -18,8 +18,8 @@ package foundation.e.apps.ui.search.v2 package foundation.e.apps.ui.search.v2 import android.content.SharedPreferences import android.content.Context import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.PagingData Loading @@ -33,25 +33,23 @@ import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Status import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.search.CleanApkSearchParams import foundation.e.apps.data.search.CleanApkSearchParams import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.SearchPagingRepository import foundation.e.apps.data.search.SearchPagingRepository import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallStatusReconciler import foundation.e.apps.ui.compose.state.InstallStatusReconciler import foundation.e.apps.ui.compose.state.InstallStatusStream import foundation.e.apps.ui.compose.state.InstallStatusStream import foundation.e.apps.ui.compose.state.StatusSnapshot import foundation.e.apps.ui.compose.state.StatusSnapshot import androidx.lifecycle.asFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf Loading @@ -60,9 +58,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject import foundation.e.apps.data.enums.Status private const val SUGGESTION_DEBOUNCE_MS = 200L private const val SUGGESTION_DEBOUNCE_MS = 500L enum class SearchTabType { enum class SearchTabType { STANDARD_APPS, STANDARD_APPS, Loading Loading
app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +27 −20 Original line number Original line Diff line number Diff line Loading @@ -113,30 +113,37 @@ class AppManagerWrapper @Inject constructor( val appDownload = getDownloadList() val appDownload = getDownloadList() .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } ?: return 0 ?: return 0 return calculateProgress(appDownload, progress) if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { return@let } } return 0 if (!isProgressValidForApp(application, progress)) { return -1 } } val downloadingMap = progress.totalSizeBytes.filter { item -> suspend fun calculateProgress( appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 appInstall: AppInstall, progress: DownloadProgress ): Int { val downloadIds = appInstall.downloadIdMap.keys if (downloadIds.isEmpty()) { // Download request exists but ids not yet populated; show 0% instead of dropping percent. return 0 } } if (appDownload.downloadIdMap.size > downloadingMap.size) { // All files for download are not ready yet val totalSizeBytes = progress.totalSizeBytes .filterKeys { downloadIds.contains(it) } .values .sum() if (totalSizeBytes <= 0) { return 0 return 0 } } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> .filterKeys { downloadIds.contains(it) } appDownload.downloadIdMap.keys.contains(item.key) .values }.values.sum() .sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() } return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100) return 0 .toInt() .coerceIn(0, 100) } } private suspend fun isProgressValidForApp( private suspend fun isProgressValidForApp( Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +3 −2 Original line number Original line Diff line number Diff line Loading @@ -57,8 +57,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter import foundation.e.apps.R import foundation.e.apps.R import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Application import foundation.e.apps.ui.compose.theme.AppTheme import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.theme.AppTheme @Composable @Composable fun SearchResultListItem( fun SearchResultListItem( Loading Loading @@ -303,7 +303,8 @@ private fun PrivacyBadge( Button( Button( onClick = onPrimaryClick, onClick = onPrimaryClick, enabled = uiState.enabled, enabled = uiState.enabled, modifier = Modifier.height(40.dp), modifier = Modifier .height(40.dp), shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors( containerColor = when { containerColor = when { Loading
app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusReconciler.kt +19 −5 Original line number Original line Diff line number Diff line Loading @@ -35,7 +35,7 @@ class InstallStatusReconciler @Inject constructor( // Prefer matching active download // Prefer matching active download val activeDownload = snapshot.downloads.find { matches(app, it) } val activeDownload = snapshot.downloads.find { matches(app, it) } if (activeDownload != null) { if (activeDownload != null) { val progressPercent = progressPercent(app, progress) val progressPercent = progressPercent(activeDownload, progress) app.status = activeDownload.status app.status = activeDownload.status return Result(app, progressPercent) return Result(app, progressPercent) } } Loading @@ -46,15 +46,29 @@ class InstallStatusReconciler @Inject constructor( } } private fun matches(app: Application, install: AppInstall): Boolean { private fun matches(app: Application, install: AppInstall): Boolean { return install.packageName == app.package_name || install.id == app._id val pkg = app.package_name val id = app._id return install.packageName == pkg || install.id == id || install.id == pkg } } private suspend fun progressPercent( private suspend fun progressPercent( app: Application, activeDownload: AppInstall, progress: DownloadProgress? progress: DownloadProgress? ): Int? { ): Int? { if (progress == null) return null if (progress == null) return null val percent = appManagerWrapper.calculateProgress(app, progress) val percent = appManagerWrapper.calculateProgress(activeDownload, progress) return percent.takeIf { it in 0..100 } if (percent in 0..100) return percent // Fallback: compute from the last downloadId emitted by DownloadProgress val id = progress.downloadId.takeIf { it != -1L } if (id != null && activeDownload.downloadIdMap.containsKey(id)) { val total = progress.totalSizeBytes[id] ?: return null if (total <= 0) return null val done = progress.bytesDownloadedSoFar[id] ?: 0L return ((done / total.toDouble()) * 100).toInt().coerceIn(0, 100) } return null } } } }
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +4 −7 Original line number Original line Diff line number Diff line Loading @@ -18,8 +18,8 @@ package foundation.e.apps.ui.search.v2 package foundation.e.apps.ui.search.v2 import android.content.SharedPreferences import android.content.Context import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.PagingData Loading @@ -33,25 +33,23 @@ import foundation.e.apps.data.Constants.PREFERENCE_SHOW_PWA import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Status import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.data.search.CleanApkSearchParams import foundation.e.apps.data.search.CleanApkSearchParams import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.SearchPagingRepository import foundation.e.apps.data.search.SearchPagingRepository import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.ui.compose.state.InstallButtonAction import foundation.e.apps.ui.compose.state.InstallStatusReconciler import foundation.e.apps.ui.compose.state.InstallStatusReconciler import foundation.e.apps.ui.compose.state.InstallStatusStream import foundation.e.apps.ui.compose.state.InstallStatusStream import foundation.e.apps.ui.compose.state.StatusSnapshot import foundation.e.apps.ui.compose.state.StatusSnapshot import androidx.lifecycle.asFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf Loading @@ -60,9 +58,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Inject import foundation.e.apps.data.enums.Status private const val SUGGESTION_DEBOUNCE_MS = 200L private const val SUGGESTION_DEBOUNCE_MS = 500L enum class SearchTabType { enum class SearchTabType { STANDARD_APPS, STANDARD_APPS, Loading