From 26967f123087ae162f7c34621244988b4fe4f353 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 10:09:27 +0500 Subject: [PATCH 1/7] feat(details): Implement open, uninstall, and downgrade functionality This commit introduces the ability for users to open and uninstall applications directly from the details screen. It also adds a warning mechanism for downgrades, requiring users to uninstall the current version before installing an older one. - **feat(details)**: Added `Open` and `Uninstall` buttons and their corresponding logic to the app details screen. - **feat(android)**: Implemented `openApp` and `uninstall` functionality in `AndroidInstaller` using Android's `Intent` system. - **feat(i18n)**: Added new string resources for uninstall, open, and downgrade warning dialogs. - **refactor(details)**: Introduced a `ShowDowngradeWarning` event to inform the user when an installation would result in a downgrade, prompting them to uninstall the existing version first. - **chore(desktop)**: Provided stub implementations for `openApp` and `uninstall` on desktop, as these actions are not supported in the same way. --- .../core/data/services/AndroidInstaller.kt | 21 +++ .../core/data/services/DesktopInstaller.kt | 11 ++ .../rainxch/core/domain/system/Installer.kt | 4 + .../composeResources/values/strings.xml | 9 ++ .../details/presentation/DetailsAction.kt | 2 + .../details/presentation/DetailsEvent.kt | 5 + .../components/SmartInstallButton.kt | 121 +++++++++++++++++- 7 files changed, 166 insertions(+), 7 deletions(-) diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt index 8c7c56e1..176c8bb2 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt @@ -158,6 +158,27 @@ class AndroidInstaller( } } + override fun uninstall(packageName: String) { + Logger.d { "Requesting uninstall for: $packageName" } + val intent = Intent(Intent.ACTION_DELETE).apply { + data = "package:$packageName".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + + override fun openApp(packageName: String): Boolean { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + return if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) + true + } else { + Logger.w { "No launch intent found for $packageName" } + false + } + } + override fun openInAppManager( filePath: String, onOpenInstaller: () -> Unit diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt index d182f828..d2e910c1 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt @@ -56,6 +56,17 @@ class DesktopInstaller( } + override fun uninstall(packageName: String) { + // Desktop doesn't have a unified uninstall mechanism + Logger.d { "Uninstall not supported on desktop for: $packageName" } + } + + override fun openApp(packageName: String): Boolean { + // Desktop apps are launched differently per platform + Logger.d { "Open app not supported on desktop for: $packageName" } + return false + } + override fun isAssetInstallable(assetName: String): Boolean { val name = assetName.lowercase() diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt index fbb5c936..ceee0a8d 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt @@ -27,4 +27,8 @@ interface Installer { ) fun getApkInfoExtractor(): InstallerInfoExtractor + + fun uninstall(packageName: String) + + fun openApp(packageName: String): Boolean } \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 45fd37f0..83d17d80 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -171,6 +171,15 @@ Installing Pending install + + Uninstall + Open + Downgrade requires uninstall + Installing version %1$s requires uninstalling the current version (%2$s) first. Your app data will be lost. + Uninstall first + Install %1$s + Failed to open %1$s + Open in Obtainium Manage updates automatically diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt index fa4677d6..10064610 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt @@ -28,6 +28,8 @@ sealed interface DetailsAction { data object OnToggleFavorite : DetailsAction data object CheckForUpdates : DetailsAction data object UpdateApp : DetailsAction + data object UninstallApp : DetailsAction + data object OpenApp : DetailsAction data class OnMessage(val messageText: StringResource) : DetailsAction diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt index c559f81a..e940dfd4 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt @@ -4,4 +4,9 @@ sealed interface DetailsEvent { data class OnOpenRepositoryInApp(val repositoryId: Long) : DetailsEvent data class InstallTrackingFailed(val message: String) : DetailsEvent data class OnMessage(val message: String) : DetailsEvent + data class ShowDowngradeWarning( + val packageName: String, + val currentVersion: String, + val targetVersion: String + ) : DetailsEvent } \ No newline at end of file diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt index 7c36fdd6..f769153f 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.Update import androidx.compose.material3.CardDefaults @@ -62,12 +64,111 @@ fun SmartInstallButton( val isInstalled = installedApp != null && !installedApp.isPendingInstall val isUpdateAvailable = installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall + val isSameVersionInstalled = isInstalled && + installedApp != null && + normalizeVersion(installedApp.installedVersion) == normalizeVersion( + state.selectedRelease?.tagName ?: "" + ) + val enabled = remember(primaryAsset, isDownloading, isInstalling) { primaryAsset != null && !isDownloading && !isInstalling } val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE + // When same version is installed, show Open + Uninstall (Play Store style) + if (isSameVersionInstalled && !isActiveDownload) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Uninstall button + ElevatedCard( + modifier = Modifier + .weight(1f) + .height(52.dp) + .clickable { onAction(DetailsAction.UninstallApp) } + .liquefiable(liquidState), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + shape = RoundedCornerShape( + topStart = 24.dp, + bottomStart = 24.dp, + topEnd = 6.dp, + bottomEnd = 6.dp + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onErrorContainer + ) + Text( + text = stringResource(Res.string.uninstall), + color = MaterialTheme.colorScheme.onErrorContainer, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium + ) + } + } + } + + // Open button + ElevatedCard( + modifier = Modifier + .weight(1f) + .height(52.dp) + .clickable { onAction(DetailsAction.OpenApp) } + .liquefiable(liquidState), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.primary + ), + shape = RoundedCornerShape( + topStart = 6.dp, + bottomStart = 6.dp, + topEnd = 24.dp, + bottomEnd = 24.dp + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onPrimary + ) + Text( + text = stringResource(Res.string.open_app), + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium + ) + } + } + } + } + return + } + + // Regular install/update button for all other cases val buttonColor = when { !enabled && !isActiveDownload -> MaterialTheme.colorScheme.surfaceContainer isUpdateAvailable -> MaterialTheme.colorScheme.tertiary @@ -77,16 +178,18 @@ fun SmartInstallButton( val buttonText = when { !enabled && primaryAsset == null -> stringResource(Res.string.not_available) - installedApp != null && installedApp.installedVersion != state.selectedRelease?.tagName -> stringResource( - Res.string.update_app - ) - isUpdateAvailable -> stringResource( Res.string.update_to_version, - installedApp.latestVersion.toString() + installedApp?.latestVersion.toString() ) - isInstalled -> stringResource(Res.string.reinstall) + isInstalled && installedApp != null && + installedApp.installedVersion != state.selectedRelease?.tagName -> + stringResource( + Res.string.install_version, + state.selectedRelease?.tagName ?: "" + ) + else -> stringResource(Res.string.install_latest) } @@ -342,6 +445,10 @@ fun SmartInstallButton( } } +private fun normalizeVersion(version: String): String { + return version.removePrefix("v").removePrefix("V").trim() +} + @Preview @Composable fun SmartInstallButtonDownloadingPreview() { @@ -369,4 +476,4 @@ fun SmartInstallButtonDownloadingPreview() { downloadProgressPercent = 45 ) ) -} \ No newline at end of file +} From 7c49347d5648845c079ca22c7d9e4a86a1f4984c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 10:09:37 +0500 Subject: [PATCH 2/7] feat(android): Add broadcast receiver for package install events This commit introduces `PackageEventReceiver`, a `BroadcastReceiver` that listens for system-wide package installation, update, and removal events (`ACTION_PACKAGE_ADDED`, `ACTION_PACKAGE_REPLACED`, `ACTION_PACKAGE_FULLY_REMOVED`). This receiver improves data consistency by reacting to out-of-app package changes. When an app is installed or updated, it updates its record in the local database with the correct version information from the system and clears any "pending install" flags. If an app is uninstalled, its corresponding entry is removed from the database. - **feat(data)**: Created `PackageEventReceiver.kt` to handle package-related broadcast intents. - **refactor(data)**: Moved package event logic into a dedicated receiver to centralize state management for installed applications. --- .../data/services/PackageEventReceiver.kt | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt new file mode 100644 index 00000000..ff864acc --- /dev/null +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/PackageEventReceiver.kt @@ -0,0 +1,104 @@ +package zed.rainxch.core.data.services + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import zed.rainxch.core.domain.repository.InstalledAppsRepository +import zed.rainxch.core.domain.system.PackageMonitor + +/** + * Listens to system package install/uninstall/replace broadcasts. + * When a tracked package is installed or updated, it resolves the pending + * install flag and updates version info from the system PackageManager. + * When a tracked package is removed, it deletes the record from the database. + */ +class PackageEventReceiver( + private val installedAppsRepository: InstalledAppsRepository, + private val packageMonitor: PackageMonitor +) : BroadcastReceiver() { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun onReceive(context: Context?, intent: Intent?) { + val packageName = intent?.data?.schemeSpecificPart ?: return + + Logger.d { "PackageEventReceiver: ${intent.action} for $packageName" } + + when (intent.action) { + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_REPLACED -> { + scope.launch { onPackageInstalled(packageName) } + } + + Intent.ACTION_PACKAGE_FULLY_REMOVED -> { + scope.launch { onPackageRemoved(packageName) } + } + } + } + + private suspend fun onPackageInstalled(packageName: String) { + try { + val app = installedAppsRepository.getAppByPackage(packageName) ?: return + + if (app.isPendingInstall) { + val systemInfo = packageMonitor.getInstalledPackageInfo(packageName) + if (systemInfo != null) { + installedAppsRepository.updateApp( + app.copy( + isPendingInstall = false, + isUpdateAvailable = false, + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode, + latestVersionName = systemInfo.versionName, + latestVersionCode = systemInfo.versionCode + ) + ) + Logger.i { "Resolved pending install via broadcast: $packageName (v${systemInfo.versionName})" } + } else { + installedAppsRepository.updatePendingStatus(packageName, false) + Logger.i { "Resolved pending install via broadcast (no system info): $packageName" } + } + } else { + val systemInfo = packageMonitor.getInstalledPackageInfo(packageName) + if (systemInfo != null) { + installedAppsRepository.updateApp( + app.copy( + installedVersionName = systemInfo.versionName, + installedVersionCode = systemInfo.versionCode + ) + ) + Logger.d { "Updated version info via broadcast: $packageName (v${systemInfo.versionName})" } + } + } + } catch (e: Exception) { + Logger.e { "PackageEventReceiver error for $packageName: ${e.message}" } + } + } + + private suspend fun onPackageRemoved(packageName: String) { + try { + val app = installedAppsRepository.getAppByPackage(packageName) ?: return + installedAppsRepository.deleteInstalledApp(packageName) + Logger.i { "Removed uninstalled app via broadcast: $packageName" } + } catch (e: Exception) { + Logger.e { "PackageEventReceiver remove error for $packageName: ${e.message}" } + } + } + + companion object { + fun createIntentFilter(): IntentFilter { + return IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_REPLACED) + addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + addDataScheme("package") + } + } + } +} From 1106543798d6aa2da639989773471add0ff4a7fb Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 10:10:22 +0500 Subject: [PATCH 3/7] feat(details): Add downgrade warnings and app management actions This commit introduces a downgrade warning mechanism on Android and adds new user actions for opening and uninstalling applications directly from the details screen. A check is now performed before installation to detect potential downgrades. If the selected version is different from the installed version and is not marked as an update, a warning is displayed to the user. Additionally, new actions have been implemented to allow users to open or uninstall the currently viewed application. - **feat(android)**: Added a downgrade warning that triggers when a user attempts to install an older version of an app. - **feat(details)**: Implemented `OpenApp` and `UninstallApp` actions in `DetailsViewModel`. - **refactor(details)**: Introduced a `normalizeVersion` utility function to improve version comparison logic. --- .../details/presentation/DetailsViewModel.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 29628fdf..4935b948 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -298,7 +298,29 @@ class DetailsViewModel( DetailsAction.InstallPrimary -> { val primary = _state.value.primaryAsset val release = _state.value.selectedRelease + val installedApp = _state.value.installedApp + if (primary != null && release != null) { + // Downgrade detection: if app is installed, selected version differs, + // and it's NOT flagged as an update, it might be a downgrade + if (installedApp != null && + !installedApp.isPendingInstall && + !installedApp.isUpdateAvailable && + normalizeVersion(release.tagName) != normalizeVersion(installedApp.installedVersion) && + platform == Platform.ANDROID + ) { + viewModelScope.launch { + _events.send( + DetailsEvent.ShowDowngradeWarning( + packageName = installedApp.packageName, + currentVersion = installedApp.installedVersion, + targetVersion = release.tagName + ) + ) + } + return + } + installAsset( downloadUrl = primary.downloadUrl, assetName = primary.name, @@ -433,6 +455,29 @@ class DetailsViewModel( } } + DetailsAction.UninstallApp -> { + val installedApp = _state.value.installedApp ?: return + logger.debug("Uninstalling app: ${installedApp.packageName}") + installer.uninstall(installedApp.packageName) + } + + DetailsAction.OpenApp -> { + val installedApp = _state.value.installedApp ?: return + val launched = installer.openApp(installedApp.packageName) + if (!launched) { + viewModelScope.launch { + _events.send( + DetailsEvent.OnMessage( + getString( + Res.string.failed_to_open_app, + arrayOf(installedApp.appName) + ) + ) + ) + } + } + } + DetailsAction.OpenRepoInBrowser -> { _state.value.repository?.htmlUrl?.let { helper.openUrl(url = it) @@ -910,6 +955,10 @@ class DetailsViewModel( } } + private fun normalizeVersion(version: String): String { + return version.removePrefix("v").removePrefix("V").trim() + } + private companion object { const val OBTAINIUM_REPO_ID: Long = 523534328 const val APP_MANAGER_REPO_ID: Long = 268006778 From b08bf7e7e7e18822ce91bb959f3da695a4cd51a9 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 10:14:29 +0500 Subject: [PATCH 4/7] feat(apps): Add uninstall functionality This commit introduces the ability for users to uninstall applications directly from the "Apps" screen. An "Uninstall" button is now displayed on each application card. Tapping this button initiates the uninstallation process for the corresponding app. This functionality is also integrated with the app details screen, which will now prompt the user with a downgrade warning, offering to uninstall the current version before proceeding. - **feat(apps)**: Added an uninstall icon button to `AppCard` to trigger uninstallation. - **feat(apps)**: Implemented `OnUninstallApp` action and corresponding logic in `AppsViewModel` to handle the uninstall request via the `installer`. - **feat(details)**: Added a confirmation dialog to the details screen to handle downgrades, which require uninstalling the app first. --- .../rainxch/apps/presentation/AppsAction.kt | 1 + .../zed/rainxch/apps/presentation/AppsRoot.kt | 23 ++++++++- .../apps/presentation/AppsViewModel.kt | 16 +++++++ .../details/presentation/DetailsRoot.kt | 48 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt index 1e25b4ce..73327800 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt @@ -12,5 +12,6 @@ sealed interface AppsAction { data object OnCancelUpdateAll : AppsAction data object OnCheckAllForUpdates : AppsAction data object OnRefresh : AppsAction + data class OnUninstallApp(val app: InstalledApp) : AppsAction data class OnNavigateToRepo(val repoId: Long) : AppsAction } \ No newline at end of file diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index e19f7dd3..06b83f07 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Update +import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card @@ -300,6 +301,7 @@ fun AppsScreen( onOpenClick = { onAction(AppsAction.OnOpenApp(appItem.installedApp)) }, onUpdateClick = { onAction(AppsAction.OnUpdateApp(appItem.installedApp)) }, onCancelClick = { onAction(AppsAction.OnCancelUpdate(appItem.installedApp.packageName)) }, + onUninstallClick = { onAction(AppsAction.OnUninstallApp(appItem.installedApp)) }, onRepoClick = { onAction(AppsAction.OnNavigateToRepo(appItem.installedApp.repoId)) }, modifier = Modifier.liquefiable(liquidState) ) @@ -373,6 +375,7 @@ fun AppItemCard( onOpenClick: () -> Unit, onUpdateClick: () -> Unit, onCancelClick: () -> Unit, + onUninstallClick: () -> Unit, onRepoClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -548,8 +551,26 @@ fun AppItemCard( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically ) { + // Uninstall icon button (shown when not pending and not actively updating) + if (!app.isPendingInstall && + appItem.updateState !is UpdateState.Downloading && + appItem.updateState !is UpdateState.Installing && + appItem.updateState !is UpdateState.CheckingUpdate + ) { + IconButton( + onClick = onUninstallClick + ) { + Icon( + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = stringResource(Res.string.uninstall), + tint = MaterialTheme.colorScheme.error + ) + } + } + Button( onClick = onOpenClick, modifier = Modifier.weight(1f), diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index 878bfb4c..def22416 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -188,6 +188,10 @@ class AppsViewModel( refresh() } + is AppsAction.OnUninstallApp -> { + uninstallApp(action.app) + } + is AppsAction.OnNavigateToRepo -> { viewModelScope.launch { _events.send(AppsEvent.NavigateToRepo(action.repoId)) @@ -196,6 +200,18 @@ class AppsViewModel( } } + private fun uninstallApp(app: InstalledApp) { + try { + installer.uninstall(app.packageName) + logger.debug("Requested uninstall for ${app.packageName}") + } catch (e: Exception) { + logger.error("Failed to request uninstall for ${app.packageName}: ${e.message}") + viewModelScope.launch { + _events.send(AppsEvent.ShowError("Failed to uninstall ${app.appName}")) + } + } + } + private fun openApp(app: InstalledApp) { viewModelScope.launch { try { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index 396b922e..d2ef6fb1 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -20,6 +20,7 @@ import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.StarBorder +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -30,13 +31,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -76,6 +81,9 @@ fun DetailsRoot( val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + var downgradeWarning by remember { + mutableStateOf(null) + } ObserveAsEvents(viewModel.events) { event -> when (event) { @@ -92,9 +100,49 @@ fun DetailsRoot( snackbarHostState.showSnackbar(event.message) } } + + is DetailsEvent.ShowDowngradeWarning -> { + downgradeWarning = event + } } } + downgradeWarning?.let { warning -> + AlertDialog( + onDismissRequest = { downgradeWarning = null }, + title = { + Text(text = stringResource(Res.string.downgrade_requires_uninstall)) + }, + text = { + Text( + text = stringResource( + Res.string.downgrade_warning_message, + warning.targetVersion, + warning.currentVersion + ) + ) + }, + confirmButton = { + TextButton( + onClick = { + downgradeWarning = null + viewModel.onAction(DetailsAction.UninstallApp) + } + ) { + Text( + text = stringResource(Res.string.uninstall_first), + color = MaterialTheme.colorScheme.error + ) + } + }, + dismissButton = { + TextButton(onClick = { downgradeWarning = null }) { + Text(text = stringResource(Res.string.cancel)) + } + } + ) + } + DetailsScreen( state = state, snackbarHostState = snackbarHostState, From 387c7453dce1ce8e57534cd5b4a3f895015a98af Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 10:55:56 +0500 Subject: [PATCH 5/7] refactor(details): Improve downgrade detection and error handling This commit refines the downgrade detection logic and improves user feedback for various actions. The downgrade warning now uses `versionCode` for a more reliable comparison when available, preventing false positives where version names might be the same. Additionally, error handling has been improved for failed app uninstalls and launches, providing clearer messages to the user. - **refactor(details)**: Improved downgrade detection by comparing `versionCode` in `DetailsViewModel` for greater accuracy. - **fix(details)**: Added a `try-catch` block for app launch failures and now shows a localized error message. - **fix(apps)**: Wrapped the uninstall logic in `AppsViewModel` in a `try-catch` block to handle exceptions and show a localized error message. - **feat(i18n)**: Added a new string resource for failed uninstalls. - **refactor(core)**: Made the `normalizeVersion` function in `DetailsViewModel` null-safe. --- .../core/data/services/AndroidInstaller.kt | 46 +++++++++++++++---- .../composeResources/values/strings.xml | 1 + .../apps/presentation/AppsViewModel.kt | 21 +++++---- .../details/presentation/DetailsViewModel.kt | 36 +++++++++------ .../components/SmartInstallButton.kt | 10 ++-- 5 files changed, 80 insertions(+), 34 deletions(-) diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt index 176c8bb2..dd2735c0 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt @@ -24,6 +24,7 @@ class AndroidInstaller( override fun getApkInfoExtractor(): InstallerInfoExtractor { return installerInfoExtractor } + override fun detectSystemArchitecture(): SystemArchitecture { val arch = Build.SUPPORTED_ABIS.firstOrNull() ?: return SystemArchitecture.UNKNOWN return when { @@ -42,7 +43,10 @@ class AndroidInstaller( return isArchitectureCompatible(name, systemArch) } - private fun isArchitectureCompatible(assetName: String, systemArch: SystemArchitecture): Boolean { + private fun isArchitectureCompatible( + assetName: String, + systemArch: SystemArchitecture + ): Boolean { return AssetArchitectureMatcher.isCompatible(assetName, systemArch) } @@ -57,17 +61,37 @@ class AndroidInstaller( val name = asset.name.lowercase() val archBoost = when (systemArch) { SystemArchitecture.X86_64 -> { - if (AssetArchitectureMatcher.isExactMatch(name, SystemArchitecture.X86_64)) 10000 else 0 + if (AssetArchitectureMatcher.isExactMatch( + name, + SystemArchitecture.X86_64 + ) + ) 10000 else 0 } + SystemArchitecture.AARCH64 -> { - if (AssetArchitectureMatcher.isExactMatch(name, SystemArchitecture.AARCH64)) 10000 else 0 + if (AssetArchitectureMatcher.isExactMatch( + name, + SystemArchitecture.AARCH64 + ) + ) 10000 else 0 } + SystemArchitecture.X86 -> { - if (AssetArchitectureMatcher.isExactMatch(name, SystemArchitecture.X86)) 10000 else 0 + if (AssetArchitectureMatcher.isExactMatch( + name, + SystemArchitecture.X86 + ) + ) 10000 else 0 } + SystemArchitecture.ARM -> { - if (AssetArchitectureMatcher.isExactMatch(name, SystemArchitecture.ARM)) 10000 else 0 + if (AssetArchitectureMatcher.isExactMatch( + name, + SystemArchitecture.ARM + ) + ) 10000 else 0 } + SystemArchitecture.UNKNOWN -> 0 } archBoost + asset.size @@ -170,9 +194,15 @@ class AndroidInstaller( override fun openApp(packageName: String): Boolean { val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) return if (launchIntent != null) { - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(launchIntent) - true + try { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) + true + } catch (e: ActivityNotFoundException) { + Logger.w { "Failed to launch $packageName: ${e.message}" } + false + } + } else { Logger.w { "No launch intent found for $packageName" } false diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 83d17d80..86b001fe 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -179,6 +179,7 @@ Uninstall first Install %1$s Failed to open %1$s + Failed to uninstall %1$s Open in Obtainium diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index def22416..7fb51fb6 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -201,13 +201,17 @@ class AppsViewModel( } private fun uninstallApp(app: InstalledApp) { - try { - installer.uninstall(app.packageName) - logger.debug("Requested uninstall for ${app.packageName}") - } catch (e: Exception) { - logger.error("Failed to request uninstall for ${app.packageName}: ${e.message}") - viewModelScope.launch { - _events.send(AppsEvent.ShowError("Failed to uninstall ${app.appName}")) + viewModelScope.launch { + try { + installer.uninstall(app.packageName) + logger.debug("Requested uninstall for ${app.packageName}") + } catch (e: Exception) { + logger.error("Failed to request uninstall for ${app.packageName}: ${e.message}") + _events.send( + AppsEvent.ShowError( + getString(Res.string.failed_to_uninstall, arrayOf(app.appName)) + ) + ) } } } @@ -300,7 +304,8 @@ class AppsViewModel( installer.getApkInfoExtractor().extractPackageInfo(existingPath) val normalizedExisting = apkInfo?.versionName?.removePrefix("v")?.removePrefix("V") ?: "" - val normalizedLatest = latestVersion.removePrefix("v").removePrefix("V") + val normalizedLatest = + latestVersion.removePrefix("v").removePrefix("V") if (normalizedExisting != normalizedLatest) { val deleted = file.delete() logger.debug("Deleted mismatched existing file ($normalizedExisting != $normalizedLatest): $deleted") diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 4935b948..d7ab318d 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -301,24 +301,34 @@ class DetailsViewModel( val installedApp = _state.value.installedApp if (primary != null && release != null) { - // Downgrade detection: if app is installed, selected version differs, - // and it's NOT flagged as an update, it might be a downgrade if (installedApp != null && !installedApp.isPendingInstall && !installedApp.isUpdateAvailable && normalizeVersion(release.tagName) != normalizeVersion(installedApp.installedVersion) && platform == Platform.ANDROID ) { - viewModelScope.launch { - _events.send( - DetailsEvent.ShowDowngradeWarning( - packageName = installedApp.packageName, - currentVersion = installedApp.installedVersion, - targetVersion = release.tagName + val isConfirmedDowngrade = if ( + normalizeVersion(release.tagName) == normalizeVersion(installedApp.latestVersion) && + (installedApp.latestVersionCode ?: 0L) > 0 + ) { + installedApp.installedVersionCode > (installedApp.latestVersionCode + ?: 0L) + } else { + true + } + + if (isConfirmedDowngrade) { + viewModelScope.launch { + _events.send( + DetailsEvent.ShowDowngradeWarning( + packageName = installedApp.packageName, + currentVersion = installedApp.installedVersion, + targetVersion = release.tagName + ) ) - ) + } + return } - return } installAsset( @@ -470,7 +480,7 @@ class DetailsViewModel( DetailsEvent.OnMessage( getString( Res.string.failed_to_open_app, - arrayOf(installedApp.appName) + installedApp.appName ) ) ) @@ -955,8 +965,8 @@ class DetailsViewModel( } } - private fun normalizeVersion(version: String): String { - return version.removePrefix("v").removePrefix("V").trim() + private fun normalizeVersion(version: String?): String { + return version?.removePrefix("v")?.removePrefix("V")?.trim() ?: "" } private companion object { diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt index f769153f..b1fec6d4 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt @@ -62,7 +62,8 @@ fun SmartInstallButton( val installedApp = state.installedApp val isInstalled = installedApp != null && !installedApp.isPendingInstall - val isUpdateAvailable = installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall + val isUpdateAvailable = + installedApp?.isUpdateAvailable == true && !installedApp.isPendingInstall val isSameVersionInstalled = isInstalled && installedApp != null && @@ -85,10 +86,10 @@ fun SmartInstallButton( ) { // Uninstall button ElevatedCard( + onClick = { onAction(DetailsAction.UninstallApp) }, modifier = Modifier .weight(1f) .height(52.dp) - .clickable { onAction(DetailsAction.UninstallApp) } .liquefiable(liquidState), colors = CardDefaults.elevatedCardColors( containerColor = MaterialTheme.colorScheme.errorContainer @@ -180,11 +181,10 @@ fun SmartInstallButton( !enabled && primaryAsset == null -> stringResource(Res.string.not_available) isUpdateAvailable -> stringResource( Res.string.update_to_version, - installedApp?.latestVersion.toString() + installedApp.latestVersion.toString() ) - isInstalled && installedApp != null && - installedApp.installedVersion != state.selectedRelease?.tagName -> + isInstalled && installedApp.installedVersion != state.selectedRelease?.tagName -> stringResource( Res.string.install_version, state.selectedRelease?.tagName ?: "" From bc34dedc97295e0c92e79fc825bc4dc9fc73e49d Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 11:36:24 +0500 Subject: [PATCH 6/7] feat(android): Add uninstall permission and improve error handling This commit adds the `REQUEST_DELETE_PACKAGES` permission to the Android manifest, enabling the app to request uninstallation of other packages. It also improves error handling and robustness for app installation and uninstallation flows. - **feat(android)**: Added the `REQUEST_DELETE_PACKAGES` permission to `AndroidManifest.xml`. - **fix(details)**: Wrapped the uninstall action in `DetailsViewModel` within a `try-catch` block to gracefully handle failures and show a user-facing error message. - **fix(installer)**: Added a `try-catch` block in `AndroidInstaller` when starting the uninstall activity to prevent crashes. - **refactor(apps)**: Corrected the string formatting calls in `AppsViewModel` to properly pass arguments for error messages. --- .../src/androidMain/AndroidManifest.xml | 18 +++++++-------- .../core/data/services/AndroidInstaller.kt | 23 +++++++++++-------- .../apps/presentation/AppsViewModel.kt | 8 +++---- .../details/presentation/DetailsViewModel.kt | 14 ++++++++++- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 2074744d..6ef04901 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,11 +1,15 @@ - + - - + + - + + @@ -24,7 +28,6 @@ android:theme="@android:style/Theme.Material.Light.NoActionBar" android:usesCleartextTraffic="false"> - - @@ -47,7 +49,6 @@ android:scheme="githubstore" /> - @@ -72,8 +73,7 @@ android:scheme="https" /> - - + diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt index dd2735c0..ef2c3345 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt @@ -104,16 +104,14 @@ class AndroidInstaller( } override suspend fun ensurePermissionsOrThrow(extOrMime: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val pm = context.packageManager - if (!pm.canRequestPackageInstalls()) { - val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { - data = "package:${context.packageName}".toUri() - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - context.startActivity(intent) - throw IllegalStateException("Please enable 'Install unknown apps' for this app in Settings and try again.") + val pm = context.packageManager + if (!pm.canRequestPackageInstalls()) { + val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply { + data = "package:${context.packageName}".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + context.startActivity(intent) + throw IllegalStateException("Please enable 'Install unknown apps' for this app in Settings and try again.") } } @@ -188,7 +186,12 @@ class AndroidInstaller( data = "package:$packageName".toUri() addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - context.startActivity(intent) + try { + context.startActivity(intent) + } catch (e: Exception) { + Logger.w { "Failed to start uninstall for $packageName: ${e.message}" } + } + } override fun openApp(packageName: String): Boolean { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt index 7fb51fb6..00c92a66 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt @@ -209,7 +209,7 @@ class AppsViewModel( logger.error("Failed to request uninstall for ${app.packageName}: ${e.message}") _events.send( AppsEvent.ShowError( - getString(Res.string.failed_to_uninstall, arrayOf(app.appName)) + getString(Res.string.failed_to_uninstall, app.appName) ) ) } @@ -227,7 +227,7 @@ class AppsViewModel( AppsEvent.ShowError( getString( Res.string.cannot_launch, - arrayOf(app.appName) + app.appName ) ) ) @@ -240,7 +240,7 @@ class AppsViewModel( AppsEvent.ShowError( getString( Res.string.failed_to_open, - arrayOf(app.appName) + app.appName ) ) ) @@ -392,7 +392,7 @@ class AppsViewModel( AppsEvent.ShowError( getString( Res.string.failed_to_update, - arrayOf(app.appName, e.message ?: "") + app.appName, e.message ?: "" ) ) ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index d7ab318d..0e3439ba 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -468,7 +468,19 @@ class DetailsViewModel( DetailsAction.UninstallApp -> { val installedApp = _state.value.installedApp ?: return logger.debug("Uninstalling app: ${installedApp.packageName}") - installer.uninstall(installedApp.packageName) + viewModelScope.launch { + try { + installer.uninstall(installedApp.packageName) + } catch (e: Exception) { + logger.error("Failed to request uninstall for ${installedApp.packageName}: ${e.message}") + _events.send( + DetailsEvent.OnMessage( + getString(Res.string.failed_to_uninstall, installedApp.packageName) + ) + ) + } + } + } DetailsAction.OpenApp -> { From 04a57db294defe3341caa2b9237cd84fd535586c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Thu, 19 Feb 2026 16:49:45 +0500 Subject: [PATCH 7/7] refactor(details): Update installed app state reactively This commit refactors the details screen to reactively observe changes to an application's installation status. Previously, the installed app information was fetched only once when the screen loaded. Now, it updates in real-time if the app is installed or uninstalled while the user is on the details page. - **feat(data, domain)**: Introduced `getAppByRepoIdAsFlow` in the `InstalledAppsRepository` and `InstalledAppDao` to provide a reactive stream of an app's installation status. - **refactor(details)**: The `DetailsViewModel` now subscribes to the `getAppByRepoIdAsFlow` stream to continuously update the UI with the correct installation state. - **fix(apps)**: Corrected a missing click handler on the app list item, ensuring the entire card navigates to the repository details, not just a portion of it. - **chore(android)**: Removed an obsolete `DOWNLOAD_WITHOUT_NOTIFICATION` permission and unnecessary `` tags from `AndroidManifest.xml`. --- .../baselineProfiles/0/composeApp-release.dm | Bin 14140 -> 17702 bytes .../baselineProfiles/1/composeApp-release.dm | Bin 14128 -> 17674 bytes .../src/androidMain/AndroidManifest.xml | 12 +++--------- .../core/data/services/AndroidDownloader.kt | 1 - .../core/data/local/db/dao/InstalledAppDao.kt | 3 +++ .../repository/InstalledAppsRepositoryImpl.kt | 3 +++ .../repository/InstalledAppsRepository.kt | 1 + .../zed/rainxch/apps/presentation/AppsRoot.kt | 8 +++++--- .../details/presentation/DetailsViewModel.kt | 13 +++++++++++++ 9 files changed, 28 insertions(+), 13 deletions(-) diff --git a/composeApp/release/baselineProfiles/0/composeApp-release.dm b/composeApp/release/baselineProfiles/0/composeApp-release.dm index bb492694efa3378e0fee365b47ae1c1efca075ce..07706a1ce2a757011dc6c365bb936ea428f2e6cf 100644 GIT binary patch literal 17702 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

2?sVRz<*a+EQX8yK0oZ7Jm)@;_ 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 tK`v53hm)WJDewI9ssP_$;$u$ literal 14140 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 diff --git a/composeApp/release/baselineProfiles/1/composeApp-release.dm b/composeApp/release/baselineProfiles/1/composeApp-release.dm index ed8aa1074b033153f0febe6d6bda3ce20390b019..1ef032f7467792f12abcc4ad7c71c7c06f9bf61e 100644 GIT binary patch literal 17674 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 nPyzCBB6@NebK-@3h{g^iJ;2a~;{ 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 diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 6ef04901..a8dd5c0d 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -4,19 +4,13 @@ - - + + - - - - - - + @Query("SELECT COUNT(*) FROM installed_apps WHERE isUpdateAvailable = 1") fun getUpdateCount(): Flow diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt index 2f0e40dc..f49fa14e 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt @@ -65,6 +65,9 @@ class InstalledAppsRepositoryImpl( override suspend fun getAppByRepoId(repoId: Long): InstalledApp? = installedAppsDao.getAppByRepoId(repoId)?.toDomain() + override fun getAppByRepoIdAsFlow(repoId: Long): Flow = + installedAppsDao.getAppByRepoIdAsFlow(repoId).map { it?.toDomain() } + override suspend fun isAppInstalled(repoId: Long): Boolean = installedAppsDao.getAppByRepoId(repoId) != null diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt index ac466755..c21dfce2 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/InstalledAppsRepository.kt @@ -9,6 +9,7 @@ interface InstalledAppsRepository { fun getUpdateCount(): Flow suspend fun getAppByPackage(packageName: String): InstalledApp? suspend fun getAppByRepoId(repoId: Long): InstalledApp? + fun getAppByRepoIdAsFlow(repoId: Long): Flow suspend fun isAppInstalled(repoId: Long): Boolean suspend fun saveInstalledApp(app: InstalledApp) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt index 06b83f07..1c0ba136 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt @@ -2,6 +2,7 @@ package zed.rainxch.apps.presentation +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -382,14 +383,15 @@ fun AppItemCard( val app = appItem.installedApp Card( - modifier = modifier.fillMaxWidth(), - onClick = onRepoClick + modifier = modifier.fillMaxWidth() ) { Column( modifier = Modifier.padding(16.dp) ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .clickable { onRepoClick() }, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { CoilImage( diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 0e3439ba..00a269dd 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn @@ -271,6 +272,8 @@ class DetailsViewModel( isAppManagerEnabled = isAppManagerEnabled, installedApp = installedApp, ) + + observeInstalledApp(repo.id) } catch (e: RateLimitException) { logger.error("Rate limited: ${e.message}") _state.value = _state.value.copy( @@ -287,6 +290,16 @@ class DetailsViewModel( } } + private fun observeInstalledApp(repoId: Long) { + viewModelScope.launch { + installedAppsRepository.getAppByRepoIdAsFlow(repoId) + .distinctUntilChanged() + .collect { app -> + _state.update { it.copy(installedApp = app) } + } + } + } + @OptIn(ExperimentalTime::class) fun onAction(action: DetailsAction) { when (action) {