Loading app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +27 −20 Original line number Diff line number Diff line Loading @@ -113,30 +113,37 @@ class AppManagerWrapper @Inject constructor( val appDownload = getDownloadList() .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } ?: return 0 if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { return@let return calculateProgress(appDownload, progress) } if (!isProgressValidForApp(application, progress)) { return -1 return 0 } val downloadingMap = progress.totalSizeBytes.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 suspend fun calculateProgress( 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 } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) }.values.sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() } return 0 val downloadedSoFar = progress.bytesDownloadedSoFar .filterKeys { downloadIds.contains(it) } .values .sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100) .toInt() .coerceIn(0, 100) } private suspend fun isProgressValidForApp( Loading app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +3 −2 Original line number Diff line number Diff line Loading @@ -57,8 +57,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import foundation.e.apps.R 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.theme.AppTheme @Composable fun SearchResultListItem( Loading Loading @@ -303,7 +303,8 @@ private fun PrivacyBadge( Button( onClick = onPrimaryClick, enabled = uiState.enabled, modifier = Modifier.height(40.dp), modifier = Modifier .height(40.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( containerColor = when { Loading app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusReconciler.kt +19 −5 Original line number Diff line number Diff line Loading @@ -35,7 +35,7 @@ class InstallStatusReconciler @Inject constructor( // Prefer matching active download val activeDownload = snapshot.downloads.find { matches(app, it) } if (activeDownload != null) { val progressPercent = progressPercent(app, progress) val progressPercent = progressPercent(activeDownload, progress) app.status = activeDownload.status return Result(app, progressPercent) } Loading @@ -46,15 +46,29 @@ class InstallStatusReconciler @Inject constructor( } 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( app: Application, activeDownload: AppInstall, progress: DownloadProgress? ): Int? { if (progress == null) return null val percent = appManagerWrapper.calculateProgress(app, progress) return percent.takeIf { it in 0..100 } val percent = appManagerWrapper.calculateProgress(activeDownload, progress) 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 Diff line number Diff line Loading @@ -18,8 +18,8 @@ package foundation.e.apps.ui.search.v2 import android.content.SharedPreferences import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.utils.toApplication 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.search.CleanApkSearchParams import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.SearchPagingRepository 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.InstallStatusStream import foundation.e.apps.ui.compose.state.StatusSnapshot import androidx.lifecycle.asFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf Loading @@ -60,9 +58,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch 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 { STANDARD_APPS, Loading Loading
app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +27 −20 Original line number Diff line number Diff line Loading @@ -113,30 +113,37 @@ class AppManagerWrapper @Inject constructor( val appDownload = getDownloadList() .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } ?: return 0 if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { return@let return calculateProgress(appDownload, progress) } if (!isProgressValidForApp(application, progress)) { return -1 return 0 } val downloadingMap = progress.totalSizeBytes.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 suspend fun calculateProgress( 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 } val totalSizeBytes = downloadingMap.values.sum() val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> appDownload.downloadIdMap.keys.contains(item.key) }.values.sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() } return 0 val downloadedSoFar = progress.bytesDownloadedSoFar .filterKeys { downloadIds.contains(it) } .values .sum() return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100) .toInt() .coerceIn(0, 100) } private suspend fun isProgressValidForApp( Loading
app/src/main/java/foundation/e/apps/ui/compose/components/SearchResultListItem.kt +3 −2 Original line number Diff line number Diff line Loading @@ -57,8 +57,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberImagePainter import foundation.e.apps.R 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.theme.AppTheme @Composable fun SearchResultListItem( Loading Loading @@ -303,7 +303,8 @@ private fun PrivacyBadge( Button( onClick = onPrimaryClick, enabled = uiState.enabled, modifier = Modifier.height(40.dp), modifier = Modifier .height(40.dp), shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( containerColor = when { Loading
app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusReconciler.kt +19 −5 Original line number Diff line number Diff line Loading @@ -35,7 +35,7 @@ class InstallStatusReconciler @Inject constructor( // Prefer matching active download val activeDownload = snapshot.downloads.find { matches(app, it) } if (activeDownload != null) { val progressPercent = progressPercent(app, progress) val progressPercent = progressPercent(activeDownload, progress) app.status = activeDownload.status return Result(app, progressPercent) } Loading @@ -46,15 +46,29 @@ class InstallStatusReconciler @Inject constructor( } 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( app: Application, activeDownload: AppInstall, progress: DownloadProgress? ): Int? { if (progress == null) return null val percent = appManagerWrapper.calculateProgress(app, progress) return percent.takeIf { it in 0..100 } val percent = appManagerWrapper.calculateProgress(activeDownload, progress) 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 Diff line number Diff line Loading @@ -18,8 +18,8 @@ package foundation.e.apps.ui.search.v2 import android.content.SharedPreferences import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.utils.toApplication 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.search.CleanApkSearchParams import foundation.e.apps.data.search.PlayStorePagingRepository import foundation.e.apps.data.search.SearchPagingRepository 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.InstallStatusStream import foundation.e.apps.ui.compose.state.StatusSnapshot import androidx.lifecycle.asFlow import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf Loading @@ -60,9 +58,8 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch 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 { STANDARD_APPS, Loading