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,
+ )
+}