diff --git a/.gradle-local/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3-bin.zip.lck b/.gradle-local/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3-bin.zip.lck new file mode 100644 index 000000000..e69de29bb diff --git a/.gradle-local/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3-bin.zip.part b/.gradle-local/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3-bin.zip.part new file mode 100644 index 000000000..e69de29bb diff --git a/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/9.json b/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/9.json new file mode 100644 index 000000000..94a2c9787 --- /dev/null +++ b/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/9.json @@ -0,0 +1,597 @@ +{ + "formatVersion": 1, + "database": { + "version": 9, + "identityHash": "22580069eb4bcda62500f96e4753e316", + "entities": [ + { + "tableName": "installed_apps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `installedVersion` TEXT NOT NULL, `installedAssetName` TEXT, `installedAssetUrl` TEXT, `latestVersion` TEXT, `latestAssetName` TEXT, `latestAssetUrl` TEXT, `latestAssetSize` INTEGER, `appName` TEXT NOT NULL, `installSource` TEXT NOT NULL, `signingFingerprint` TEXT, `installedAt` INTEGER NOT NULL, `lastCheckedAt` INTEGER NOT NULL, `lastUpdatedAt` INTEGER NOT NULL, `isUpdateAvailable` INTEGER NOT NULL, `updateCheckEnabled` INTEGER NOT NULL, `releaseNotes` TEXT, `systemArchitecture` TEXT NOT NULL, `fileExtension` TEXT NOT NULL, `isPendingInstall` INTEGER NOT NULL, `installedVersionName` TEXT, `installedVersionCode` INTEGER NOT NULL, `latestVersionName` TEXT, `latestVersionCode` INTEGER, `latestReleasePublishedAt` TEXT, `includePreReleases` INTEGER NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwnerAvatarUrl", + "columnName": "repoOwnerAvatarUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoDescription", + "columnName": "repoDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryLanguage", + "columnName": "primaryLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedVersion", + "columnName": "installedVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedAssetName", + "columnName": "installedAssetName", + "affinity": "TEXT" + }, + { + "fieldPath": "installedAssetUrl", + "columnName": "installedAssetUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "latestVersion", + "columnName": "latestVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "latestAssetName", + "columnName": "latestAssetName", + "affinity": "TEXT" + }, + { + "fieldPath": "latestAssetUrl", + "columnName": "latestAssetUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "latestAssetSize", + "columnName": "latestAssetSize", + "affinity": "INTEGER" + }, + { + "fieldPath": "appName", + "columnName": "appName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installSource", + "columnName": "installSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "signingFingerprint", + "columnName": "signingFingerprint", + "affinity": "TEXT" + }, + { + "fieldPath": "installedAt", + "columnName": "installedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastCheckedAt", + "columnName": "lastCheckedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdatedAt", + "columnName": "lastUpdatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUpdateAvailable", + "columnName": "isUpdateAvailable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateCheckEnabled", + "columnName": "updateCheckEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseNotes", + "columnName": "releaseNotes", + "affinity": "TEXT" + }, + { + "fieldPath": "systemArchitecture", + "columnName": "systemArchitecture", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileExtension", + "columnName": "fileExtension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPendingInstall", + "columnName": "isPendingInstall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installedVersionName", + "columnName": "installedVersionName", + "affinity": "TEXT" + }, + { + "fieldPath": "installedVersionCode", + "columnName": "installedVersionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latestVersionName", + "columnName": "latestVersionName", + "affinity": "TEXT" + }, + { + "fieldPath": "latestVersionCode", + "columnName": "latestVersionCode", + "affinity": "INTEGER" + }, + { + "fieldPath": "latestReleasePublishedAt", + "columnName": "latestReleasePublishedAt", + "affinity": "TEXT" + }, + { + "fieldPath": "includePreReleases", + "columnName": "includePreReleases", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + } + }, + { + "tableName": "favorite_repos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwnerAvatarUrl", + "columnName": "repoOwnerAvatarUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoDescription", + "columnName": "repoDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryLanguage", + "columnName": "primaryLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isInstalled", + "columnName": "isInstalled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installedPackageName", + "columnName": "installedPackageName", + "affinity": "TEXT" + }, + { + "fieldPath": "latestVersion", + "columnName": "latestVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "latestReleaseUrl", + "columnName": "latestReleaseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedAt", + "columnName": "lastSyncedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + } + }, + { + "tableName": "update_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `packageName` TEXT NOT NULL, `appName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `fromVersion` TEXT NOT NULL, `toVersion` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, `updateSource` TEXT NOT NULL, `success` INTEGER NOT NULL, `errorMessage` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appName", + "columnName": "appName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fromVersion", + "columnName": "fromVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "toVersion", + "columnName": "toVersion", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updatedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateSource", + "columnName": "updateSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "success", + "columnName": "success", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorMessage", + "columnName": "errorMessage", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "starred_repos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `stargazersCount` INTEGER NOT NULL, `forksCount` INTEGER NOT NULL, `openIssuesCount` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `installedPackageName` TEXT, `latestVersion` TEXT, `latestReleaseUrl` TEXT, `starredAt` INTEGER, `addedAt` INTEGER NOT NULL, `lastSyncedAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwnerAvatarUrl", + "columnName": "repoOwnerAvatarUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoDescription", + "columnName": "repoDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryLanguage", + "columnName": "primaryLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "stargazersCount", + "columnName": "stargazersCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forksCount", + "columnName": "forksCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "openIssuesCount", + "columnName": "openIssuesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInstalled", + "columnName": "isInstalled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "installedPackageName", + "columnName": "installedPackageName", + "affinity": "TEXT" + }, + { + "fieldPath": "latestVersion", + "columnName": "latestVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "latestReleaseUrl", + "columnName": "latestReleaseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "starredAt", + "columnName": "starredAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "addedAt", + "columnName": "addedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSyncedAt", + "columnName": "lastSyncedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + } + }, + { + "tableName": "cache_entries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `jsonData` TEXT NOT NULL, `cachedAt` INTEGER NOT NULL, `expiresAt` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "jsonData", + "columnName": "jsonData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cachedAt", + "columnName": "cachedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expiresAt", + "columnName": "expiresAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "seen_repos", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `repoName` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoOwnerAvatarUrl` TEXT NOT NULL, `repoDescription` TEXT, `primaryLanguage` TEXT, `repoUrl` TEXT NOT NULL, `seenAt` INTEGER NOT NULL, PRIMARY KEY(`repoId`))", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwnerAvatarUrl", + "columnName": "repoOwnerAvatarUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoDescription", + "columnName": "repoDescription", + "affinity": "TEXT" + }, + { + "fieldPath": "primaryLanguage", + "columnName": "primaryLanguage", + "affinity": "TEXT" + }, + { + "fieldPath": "repoUrl", + "columnName": "repoUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "seenAt", + "columnName": "seenAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + } + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, `searchedAt` INTEGER NOT NULL, PRIMARY KEY(`query`))", + "fields": [ + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "searchedAt", + "columnName": "searchedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "query" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '22580069eb4bcda62500f96e4753e316')" + ] + } +} \ No newline at end of file diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt index 63cc0cb67..d6bda8652 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/initDatabase.kt @@ -10,6 +10,7 @@ import zed.rainxch.core.data.local.db.migrations.MIGRATION_4_5 import zed.rainxch.core.data.local.db.migrations.MIGRATION_5_6 import zed.rainxch.core.data.local.db.migrations.MIGRATION_6_7 import zed.rainxch.core.data.local.db.migrations.MIGRATION_7_8 +import zed.rainxch.core.data.local.db.migrations.MIGRATION_8_9 fun initDatabase(context: Context): AppDatabase { val appContext = context.applicationContext @@ -27,5 +28,6 @@ fun initDatabase(context: Context): AppDatabase { MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, + MIGRATION_8_9, ).build() } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_8_9.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_8_9.kt new file mode 100644 index 000000000..73c54343f --- /dev/null +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_8_9.kt @@ -0,0 +1,13 @@ +package zed.rainxch.core.data.local.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_8_9 = + object : Migration(8, 9) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE installed_apps ADD COLUMN latestReleasePublishedAt TEXT", + ) + } + } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt index b08cef57b..8376fd31c 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/AppDatabase.kt @@ -27,7 +27,7 @@ import zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity SeenRepoEntity::class, SearchHistoryEntity::class, ], - version = 8, + version = 9, exportSchema = true, ) abstract class AppDatabase : RoomDatabase() { diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt index 4ac3ed7b8..653031eea 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/InstalledAppDao.kt @@ -52,7 +52,8 @@ interface InstalledAppDao { releaseNotes = :releaseNotes, lastCheckedAt = :timestamp, latestVersionName = :latestVersionName, - latestVersionCode = :latestVersionCode + latestVersionCode = :latestVersionCode, + latestReleasePublishedAt = :latestReleasePublishedAt WHERE packageName = :packageName """, ) @@ -67,6 +68,7 @@ interface InstalledAppDao { timestamp: Long, latestVersionName: String?, latestVersionCode: Long?, + latestReleasePublishedAt: String?, ) @Query("UPDATE installed_apps SET includePreReleases = :enabled WHERE packageName = :packageName") diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt index b632626d5..a12e5a6c7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/InstalledAppEntity.kt @@ -37,5 +37,6 @@ data class InstalledAppEntity( val installedVersionCode: Long = 0L, val latestVersionName: String? = null, val latestVersionCode: Long? = null, + val latestReleasePublishedAt: String? = null, val includePreReleases: Boolean = false, ) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/InstalledAppsMappers.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/InstalledAppsMappers.kt index 3dc1aed46..88c234f04 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/InstalledAppsMappers.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/InstalledAppsMappers.kt @@ -35,6 +35,7 @@ fun InstalledApp.toEntity(): InstalledAppEntity = installedVersionCode = installedVersionCode, latestVersionName = latestVersionName, latestVersionCode = latestVersionCode, + latestReleasePublishedAt = latestReleasePublishedAt, signingFingerprint = signingFingerprint, includePreReleases = includePreReleases, ) @@ -71,6 +72,7 @@ fun InstalledAppEntity.toDomain(): InstalledApp = installedVersionCode = installedVersionCode, latestVersionName = latestVersionName, latestVersionCode = latestVersionCode, + latestReleasePublishedAt = latestReleasePublishedAt, signingFingerprint = signingFingerprint, includePreReleases = includePreReleases, ) 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 e761858a1..6da9595f1 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 @@ -149,6 +149,7 @@ class InstalledAppsRepositoryImpl( timestamp = System.currentTimeMillis(), latestVersionName = latestRelease.tagName, latestVersionCode = null, + latestReleasePublishedAt = latestRelease.publishedAt, ) return isUpdateAvailable diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt index 468c00bff..2f1d65cd5 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/InstalledApp.kt @@ -32,5 +32,6 @@ data class InstalledApp( val installedVersionCode: Long = 0L, val latestVersionName: String? = null, val latestVersionCode: Long? = null, + val latestReleasePublishedAt: String? = null, val includePreReleases: Boolean = false, ) diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index d7279dff5..fed219433 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -20,6 +20,9 @@ Search your apps No apps found + Sort apps + Updates first + Update All @@ -155,7 +158,7 @@ Warning! - Are you sure you want to logout? + Are you sure you want to log out? Dynamic @@ -258,7 +261,7 @@ Install permission unavailable - The APK was downloaded successfully but this device doesn\'t allow direct installation. Would you like to open it with an external installer? + The APK was downloaded successfully but this device doesn't allow direct installation. Would you like to open it with an external installer? Open with external installer Use a third-party app to install the APK @@ -626,4 +629,4 @@ Pre-releases - \ No newline at end of file + diff --git a/feature/apps/presentation/build.gradle.kts b/feature/apps/presentation/build.gradle.kts index 87bb876f0..94e5dfe34 100644 --- a/feature/apps/presentation/build.gradle.kts +++ b/feature/apps/presentation/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { implementation(libs.liquid) implementation(libs.kotlinx.collections.immutable) + implementation(libs.kotlinx.datetime) } } diff --git a/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt new file mode 100644 index 000000000..3b6b55f10 --- /dev/null +++ b/feature/apps/presentation/src/androidMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.android.kt @@ -0,0 +1,47 @@ +package zed.rainxch.apps.presentation.components + +import android.content.pm.PackageManager.NameNotFoundException +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.core.graphics.drawable.toBitmap +import org.jetbrains.compose.resources.painterResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.app_icon + +@Composable +actual fun InstalledAppIcon( + packageName: String, + appName: String, + modifier: Modifier, +) { + val packageManager = LocalContext.current.packageManager + val iconBitmap = + remember(packageName, packageManager) { + try { + packageManager + .getApplicationIcon(packageName) + .toBitmap() + .asImageBitmap() + } catch (_: NameNotFoundException) { + null + } + } + + if (iconBitmap != null) { + Image( + bitmap = iconBitmap, + contentDescription = appName, + modifier = modifier, + ) + } else { + Image( + painter = painterResource(Res.drawable.app_icon), + contentDescription = appName, + modifier = modifier, + ) + } +} 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 dad9df66f..8923c0d00 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 @@ -1,6 +1,7 @@ package zed.rainxch.apps.presentation import zed.rainxch.apps.presentation.model.InstalledAppUi +import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.DeviceAppUi import zed.rainxch.apps.presentation.model.GithubAssetUi @@ -12,6 +13,10 @@ sealed interface AppsAction { val query: String, ) : AppsAction + data class OnSortRuleSelected( + val sortRule: AppSortRule, + ) : AppsAction + data class OnOpenApp( val app: InstalledAppUi, ) : AppsAction 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 b6bf621c8..189e4d2ac 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 @@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.CheckCircle 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.Sort import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.FileDownload @@ -36,6 +37,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card +import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.DropdownMenu @@ -50,7 +52,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField @@ -67,17 +68,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.skydoves.landscapist.coil3.CoilImage import io.github.fletchmckee.liquid.liquefiable +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.apps.presentation.components.InstalledAppIcon import zed.rainxch.apps.presentation.components.LinkAppBottomSheet import zed.rainxch.apps.presentation.model.AppItem +import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.UpdateAllProgress import zed.rainxch.apps.presentation.model.UpdateState import zed.rainxch.core.presentation.components.ExpressiveCard @@ -111,6 +119,10 @@ import zed.rainxch.githubstore.core.presentation.res.open import zed.rainxch.githubstore.core.presentation.res.pending_install import zed.rainxch.githubstore.core.presentation.res.pre_release_badge import zed.rainxch.githubstore.core.presentation.res.search_your_apps +import zed.rainxch.githubstore.core.presentation.res.sort_apps +import zed.rainxch.githubstore.core.presentation.res.sort_name +import zed.rainxch.githubstore.core.presentation.res.sort_recently_updated +import zed.rainxch.githubstore.core.presentation.res.sort_updates_first import zed.rainxch.githubstore.core.presentation.res.uninstall import zed.rainxch.githubstore.core.presentation.res.update import zed.rainxch.githubstore.core.presentation.res.update_all @@ -179,6 +191,7 @@ fun AppsScreen( val liquidState = LocalBottomNavigationLiquid.current val bottomNavHeight = LocalBottomNavigationHeight.current var showOverflowMenu by remember { mutableStateOf(false) } + var showSortMenu by remember { mutableStateOf(false) } Scaffold( topBar = { @@ -192,6 +205,41 @@ fun AppsScreen( ) }, actions = { + Box { + IconButton(onClick = { showSortMenu = true }) { + Icon( + imageVector = Icons.Default.Sort, + contentDescription = stringResource(Res.string.sort_apps), + ) + } + DropdownMenu( + expanded = showSortMenu, + onDismissRequest = { showSortMenu = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.sort_updates_first)) }, + onClick = { + showSortMenu = false + onAction(AppsAction.OnSortRuleSelected(AppSortRule.UpdatesFirst)) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(Res.string.sort_recently_updated)) }, + onClick = { + showSortMenu = false + onAction(AppsAction.OnSortRuleSelected(AppSortRule.RecentlyUpdated)) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(Res.string.sort_name)) }, + onClick = { + showSortMenu = false + onAction(AppsAction.OnSortRuleSelected(AppSortRule.Name)) + }, + ) + } + } + IconButton( onClick = { onAction(AppsAction.OnCheckAllForUpdates) }, ) { @@ -547,6 +595,11 @@ fun AppItemCard( modifier: Modifier = Modifier, ) { val app = appItem.installedApp + val isBusy = + app.isPendingInstall || + appItem.updateState is UpdateState.Downloading || + appItem.updateState is UpdateState.Installing || + appItem.updateState is UpdateState.CheckingUpdate ExpressiveCard( onClick = onRepoClick, @@ -561,21 +614,15 @@ fun AppItemCard( Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - CoilImage( - imageModel = { app.repoOwnerAvatarUrl }, + InstalledAppIcon( + packageName = app.packageName, + appName = app.appName, modifier = Modifier .size(64.dp) - .clip(CircleShape), - loading = { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - CircularWavyProgressIndicator() - } - }, + .clip(RoundedCornerShape(18.dp)), ) Column(modifier = Modifier.weight(1f)) { @@ -586,11 +633,31 @@ fun AppItemCard( fontWeight = FontWeight.Bold, ) - Text( - text = app.repoOwner, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + CoilImage( + imageModel = { app.repoOwnerAvatarUrl }, + modifier = + Modifier + .size(18.dp) + .clip(CircleShape), + loading = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularWavyProgressIndicator() + } + }, + ) + Spacer(Modifier.width(6.dp)) + Text( + text = app.repoOwner, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } when { app.isPendingInstall -> { @@ -603,7 +670,13 @@ fun AppItemCard( app.isUpdateAvailable -> { Text( - text = "${app.installedVersion} → ${app.latestVersion}", + text = + buildVersionLabel( + installedVersion = app.installedVersion, + latestVersion = app.latestVersion, + latestReleasePublishedAt = app.latestReleasePublishedAt, + lastUpdatedAt = app.lastUpdatedAt, + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.primary, ) @@ -611,7 +684,13 @@ fun AppItemCard( else -> { Text( - text = app.installedVersion, + text = + buildVersionLabel( + installedVersion = app.installedVersion, + latestVersion = null, + latestReleasePublishedAt = null, + lastUpdatedAt = app.lastUpdatedAt, + ), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -638,18 +717,24 @@ fun AppItemCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { + val preReleaseString = stringResource(Res.string.pre_release_badge) Text( - text = stringResource(Res.string.pre_release_badge), + text = preReleaseString, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Switch( + Checkbox( checked = app.includePreReleases, onCheckedChange = onTogglePreReleases, + enabled = !isBusy, + modifier = + Modifier.semantics { + contentDescription = preReleaseString + }, ) } - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(12.dp)) when (val state = appItem.updateState) { is UpdateState.Downloading -> { @@ -746,39 +831,14 @@ fun AppItemCard( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { - 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( - shapes = ButtonDefaults.shapes(), - onClick = onOpenClick, - modifier = Modifier.weight(1f), - enabled = - !app.isPendingInstall && - appItem.updateState !is UpdateState.Downloading && - appItem.updateState !is UpdateState.Installing, + IconButton( + onClick = onUninstallClick, + enabled = !isBusy, ) { Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(4.dp)) - Text( - text = stringResource(Res.string.open), + imageVector = Icons.Outlined.DeleteOutline, + contentDescription = stringResource(Res.string.uninstall), + tint = MaterialTheme.colorScheme.error, ) } @@ -823,6 +883,23 @@ fun AppItemCard( text = stringResource(Res.string.update), ) } + } else { + Button( + shapes = ButtonDefaults.shapes(), + onClick = onOpenClick, + modifier = Modifier.weight(1f), + enabled = !isBusy, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(4.dp)) + Text( + text = stringResource(Res.string.open), + ) + } } } } @@ -831,6 +908,56 @@ fun AppItemCard( } } +private fun buildVersionLabel( + installedVersion: String, + latestVersion: String?, + latestReleasePublishedAt: String?, + lastUpdatedAt: Long, +): String { + val displayDate = + if (latestVersion != null) { + formatIsoDate(latestReleasePublishedAt) + } else { + formatEpochDate(lastUpdatedAt) + } + + return buildString { + append(installedVersion) + if (latestVersion != null) { + append(" → ") + append(latestVersion) + } + displayDate?.let { + append(" (") + append(it) + append(")") + } + } +} + +private fun formatIsoDate(isoTimestamp: String?): String? { + if (isoTimestamp.isNullOrBlank()) return null + + return try { + Instant + .parse(isoTimestamp) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .toString() + } catch (_: IllegalArgumentException) { + null + } +} + +private fun formatEpochDate(timestamp: Long): String? { + if (timestamp <= 0L) return null + return Instant + .fromEpochMilliseconds(timestamp) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date + .toString() +} + @Composable private fun formatLastChecked(timestamp: Long): String { val now = System.currentTimeMillis() diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt index d551fecea..69a70cebf 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt @@ -5,6 +5,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import zed.rainxch.apps.domain.model.GithubRepoInfo import zed.rainxch.apps.presentation.model.AppItem +import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.DeviceAppUi import zed.rainxch.apps.presentation.model.GithubAssetUi import zed.rainxch.apps.presentation.model.GithubRepoInfoUi @@ -17,6 +18,7 @@ data class AppsState( val apps: ImmutableList = persistentListOf(), val filteredApps: ImmutableList = persistentListOf(), val searchQuery: String = "", + val sortRule: AppSortRule = AppSortRule.UpdatesFirst, val isLoading: Boolean = false, val isUpdatingAll: Boolean = false, val updateAllProgress: UpdateAllProgress? = null, 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 55cfcc8e7..cb6b167a1 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 @@ -22,6 +22,7 @@ import zed.rainxch.apps.domain.repository.AppsRepository import zed.rainxch.apps.presentation.mappers.toDomain import zed.rainxch.apps.presentation.mappers.toUi import zed.rainxch.apps.presentation.model.AppItem +import zed.rainxch.apps.presentation.model.AppSortRule import zed.rainxch.apps.presentation.model.GithubAssetUi import zed.rainxch.apps.presentation.model.InstalledAppUi import zed.rainxch.apps.presentation.model.UpdateAllProgress @@ -112,7 +113,7 @@ class AppsViewModel( downloadProgress = existing?.downloadProgress, error = existing?.error, ) - }.sortedByDescending { it.installedApp.isUpdateAvailable } + }.sortedWith(appComparator(AppSortRule.UpdatesFirst)) .toImmutableList() _state.update { @@ -195,6 +196,14 @@ class AppsViewModel( filterApps() } + is AppsAction.OnSortRuleSelected -> { + _state.update { + it.copy(sortRule = action.sortRule) + } + + filterApps() + } + is AppsAction.OnOpenApp -> { openApp(action.app) } @@ -328,7 +337,7 @@ class AppsViewModel( private fun filterApps() { _state.update { current -> current.copy( - filteredApps = computeFilteredApps(current.apps, current.searchQuery), + filteredApps = computeFilteredApps(current.apps, current.searchQuery, current.sortRule), ) } } @@ -336,20 +345,40 @@ class AppsViewModel( private fun computeFilteredApps( apps: ImmutableList, query: String, + sortRule: AppSortRule = _state.value.sortRule, ): ImmutableList = if (query.isBlank()) { apps - .sortedBy { it.installedApp.isUpdateAvailable } + .sortedWith(appComparator(sortRule)) .toImmutableList() } else { apps .filter { appItem -> appItem.installedApp.appName.contains(query, ignoreCase = true) || appItem.installedApp.repoOwner.contains(query, ignoreCase = true) - }.sortedBy { it.installedApp.isUpdateAvailable } + }.sortedWith(appComparator(sortRule)) .toImmutableList() } + private fun appComparator(sortRule: AppSortRule): Comparator { + val updatesFirst = compareByDescending { it.installedApp.isUpdateAvailable } + return when (sortRule) { + AppSortRule.UpdatesFirst -> + updatesFirst + .thenByDescending { it.installedApp.latestReleasePublishedAt ?: "" } + .thenBy { it.installedApp.appName.lowercase() } + + AppSortRule.RecentlyUpdated -> + compareByDescending { it.installedApp.lastUpdatedAt } + .thenByDescending { it.installedApp.isUpdateAvailable } + .thenBy { it.installedApp.appName.lowercase() } + + AppSortRule.Name -> + compareBy { it.installedApp.appName.lowercase() } + .thenByDescending { it.installedApp.isUpdateAvailable } + } + } + private fun togglePreReleases(packageName: String, enabled: Boolean) { viewModelScope.launch { try { diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt new file mode 100644 index 000000000..bfc3f5c4f --- /dev/null +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.kt @@ -0,0 +1,11 @@ +package zed.rainxch.apps.presentation.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +expect fun InstalledAppIcon( + packageName: String, + appName: String, + modifier: Modifier = Modifier, +) diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/InstalledAppMapper.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/InstalledAppMapper.kt index 8e326a98c..7006b60fa 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/InstalledAppMapper.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/mappers/InstalledAppMapper.kt @@ -33,6 +33,7 @@ fun InstalledApp.toUi(): InstalledAppUi = installedVersionCode = installedVersionCode, latestVersionName = latestVersionName, latestVersionCode = latestVersionCode, + latestReleasePublishedAt = latestReleasePublishedAt, packageName = packageName, appName = appName, signingFingerprint = signingFingerprint, @@ -69,6 +70,7 @@ fun InstalledAppUi.toDomain(): InstalledApp = installedVersionCode = installedVersionCode, latestVersionName = latestVersionName, latestVersionCode = latestVersionCode, + latestReleasePublishedAt = latestReleasePublishedAt, packageName = packageName, appName = appName, signingFingerprint = signingFingerprint, diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/AppSortRule.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/AppSortRule.kt new file mode 100644 index 000000000..39b8378f2 --- /dev/null +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/AppSortRule.kt @@ -0,0 +1,7 @@ +package zed.rainxch.apps.presentation.model + +enum class AppSortRule { + UpdatesFirst, + RecentlyUpdated, + Name, +} diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/InstalledAppUi.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/InstalledAppUi.kt index 64aacec8a..be1b00248 100644 --- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/InstalledAppUi.kt +++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/model/InstalledAppUi.kt @@ -34,5 +34,6 @@ data class InstalledAppUi( val installedVersionCode: Long = 0L, val latestVersionName: String? = null, val latestVersionCode: Long? = null, + val latestReleasePublishedAt: String? = null, val includePreReleases: Boolean = false, ) diff --git a/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.jvm.kt b/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.jvm.kt new file mode 100644 index 000000000..f167eff7c --- /dev/null +++ b/feature/apps/presentation/src/jvmMain/kotlin/zed/rainxch/apps/presentation/components/InstalledAppIcon.jvm.kt @@ -0,0 +1,21 @@ +package zed.rainxch.apps.presentation.components + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.jetbrains.compose.resources.painterResource +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.app_icon + +@Composable +actual fun InstalledAppIcon( + packageName: String, + appName: String, + modifier: Modifier, +) { + Image( + painter = painterResource(Res.drawable.app_icon), + contentDescription = appName, + modifier = modifier, + ) +}