From 324139a0adeb990b3145b5e2f22b830523270e2d Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:11:34 +0500 Subject: [PATCH 01/12] feat(core): hidden_repositories table + repository --- .../17.json | 841 ++++++++++++++++++ .../core/data/local/db/initDatabase.kt | 2 + .../local/db/migrations/MIGRATION_16_17.kt | 21 + .../zed/rainxch/core/data/di/SharedModule.kt | 13 + .../rainxch/core/data/local/db/AppDatabase.kt | 6 +- .../core/data/local/db/dao/HiddenRepoDao.kt | 26 + .../local/db/entities/HiddenRepoEntity.kt | 14 + .../repository/HiddenReposRepositoryImpl.kt | 54 ++ .../rainxch/core/domain/model/HiddenRepo.kt | 9 + .../repository/HiddenReposRepository.kt | 21 + 10 files changed, 1006 insertions(+), 1 deletion(-) create mode 100644 core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/17.json create mode 100644 core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_16_17.kt create mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/HiddenRepoDao.kt create mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/HiddenRepoEntity.kt create mode 100644 core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HiddenReposRepositoryImpl.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HiddenRepo.kt create mode 100644 core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HiddenReposRepository.kt diff --git a/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/17.json b/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/17.json new file mode 100644 index 000000000..25d311877 --- /dev/null +++ b/core/data/schemas/zed.rainxch.core.data.local.db.AppDatabase/17.json @@ -0,0 +1,841 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "35f065ecb88f6bd274236292119e7819", + "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, `assetFilterRegex` TEXT, `fallbackToOlderReleases` INTEGER NOT NULL DEFAULT 0, `preferredAssetVariant` TEXT, `preferredVariantStale` INTEGER NOT NULL DEFAULT 0, `preferredAssetTokens` TEXT, `assetGlobPattern` TEXT, `pickedAssetIndex` INTEGER, `pickedAssetSiblingCount` INTEGER, `pendingInstallFilePath` TEXT, `pendingInstallVersion` TEXT, `pendingInstallAssetName` TEXT, `skippedReleaseTag` TEXT DEFAULT 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 + }, + { + "fieldPath": "assetFilterRegex", + "columnName": "assetFilterRegex", + "affinity": "TEXT" + }, + { + "fieldPath": "fallbackToOlderReleases", + "columnName": "fallbackToOlderReleases", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "preferredAssetVariant", + "columnName": "preferredAssetVariant", + "affinity": "TEXT" + }, + { + "fieldPath": "preferredVariantStale", + "columnName": "preferredVariantStale", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "preferredAssetTokens", + "columnName": "preferredAssetTokens", + "affinity": "TEXT" + }, + { + "fieldPath": "assetGlobPattern", + "columnName": "assetGlobPattern", + "affinity": "TEXT" + }, + { + "fieldPath": "pickedAssetIndex", + "columnName": "pickedAssetIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "pickedAssetSiblingCount", + "columnName": "pickedAssetSiblingCount", + "affinity": "INTEGER" + }, + { + "fieldPath": "pendingInstallFilePath", + "columnName": "pendingInstallFilePath", + "affinity": "TEXT" + }, + { + "fieldPath": "pendingInstallVersion", + "columnName": "pendingInstallVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "pendingInstallAssetName", + "columnName": "pendingInstallAssetName", + "affinity": "TEXT" + }, + { + "fieldPath": "skippedReleaseTag", + "columnName": "skippedReleaseTag", + "affinity": "TEXT", + "defaultValue": "NULL" + } + ], + "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" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `state` TEXT NOT NULL, `repoOwner` TEXT, `repoName` TEXT, `matchSource` TEXT, `matchConfidence` REAL, `signingFingerprint` TEXT, `installerKind` TEXT, `firstSeenAt` INTEGER NOT NULL, `lastReviewedAt` INTEGER NOT NULL, `skipExpiresAt` INTEGER, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT" + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT" + }, + { + "fieldPath": "matchSource", + "columnName": "matchSource", + "affinity": "TEXT" + }, + { + "fieldPath": "matchConfidence", + "columnName": "matchConfidence", + "affinity": "REAL" + }, + { + "fieldPath": "signingFingerprint", + "columnName": "signingFingerprint", + "affinity": "TEXT" + }, + { + "fieldPath": "installerKind", + "columnName": "installerKind", + "affinity": "TEXT" + }, + { + "fieldPath": "firstSeenAt", + "columnName": "firstSeenAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastReviewedAt", + "columnName": "lastReviewedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "skipExpiresAt", + "columnName": "skipExpiresAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [ + { + "name": "index_external_links_repoOwner_repoName", + "unique": false, + "columnNames": [ + "repoOwner", + "repoName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_external_links_repoOwner_repoName` ON `${TABLE_NAME}` (`repoOwner`, `repoName`)" + } + ] + }, + { + "tableName": "signing_fingerprints", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fingerprint` TEXT NOT NULL, `repoOwner` TEXT NOT NULL, `repoName` TEXT NOT NULL, `source` TEXT NOT NULL, `observedAt` INTEGER NOT NULL, PRIMARY KEY(`fingerprint`))", + "fields": [ + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoOwner", + "columnName": "repoOwner", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "repoName", + "columnName": "repoName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "observedAt", + "columnName": "observedAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "fingerprint" + ] + }, + "indices": [ + { + "name": "index_signing_fingerprints_repoOwner_repoName", + "unique": false, + "columnNames": [ + "repoOwner", + "repoName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_signing_fingerprints_repoOwner_repoName` ON `${TABLE_NAME}` (`repoOwner`, `repoName`)" + } + ] + }, + { + "tableName": "hidden_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, `hiddenAt` 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": "hiddenAt", + "columnName": "hiddenAt", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + } + } + ], + "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, '35f065ecb88f6bd274236292119e7819')" + ] + } +} \ 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 02b9cb4f0..a6872b382 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 @@ -18,6 +18,7 @@ import zed.rainxch.core.data.local.db.migrations.MIGRATION_12_13 import zed.rainxch.core.data.local.db.migrations.MIGRATION_13_14 import zed.rainxch.core.data.local.db.migrations.MIGRATION_14_15 import zed.rainxch.core.data.local.db.migrations.MIGRATION_15_16 +import zed.rainxch.core.data.local.db.migrations.MIGRATION_16_17 fun initDatabase(context: Context): AppDatabase { val appContext = context.applicationContext @@ -43,5 +44,6 @@ fun initDatabase(context: Context): AppDatabase { MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, + MIGRATION_16_17, ).build() } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_16_17.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_16_17.kt new file mode 100644 index 000000000..75e872b58 --- /dev/null +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/local/db/migrations/MIGRATION_16_17.kt @@ -0,0 +1,21 @@ +package zed.rainxch.core.data.local.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_16_17 = + object : Migration(16, 17) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS hidden_repos ( + repoId INTEGER NOT NULL PRIMARY KEY, + repoName TEXT NOT NULL, + repoOwner TEXT NOT NULL, + repoOwnerAvatarUrl TEXT NOT NULL, + hiddenAt INTEGER NOT NULL + ) + """.trimIndent(), + ) + } + } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt index 5375e1ef3..9414364b6 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt @@ -31,6 +31,7 @@ import zed.rainxch.core.data.local.db.dao.ExternalLinkDao import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao import zed.rainxch.core.data.local.db.dao.InstalledAppDao import zed.rainxch.core.data.local.db.dao.SearchHistoryDao +import zed.rainxch.core.data.local.db.dao.HiddenRepoDao import zed.rainxch.core.data.local.db.dao.SeenRepoDao import zed.rainxch.core.data.local.db.dao.SigningFingerprintDao import zed.rainxch.core.data.local.db.dao.StarredRepoDao @@ -57,6 +58,7 @@ import zed.rainxch.core.data.repository.DeviceIdentityRepositoryImpl import zed.rainxch.core.data.repository.RateLimitRepositoryImpl import zed.rainxch.core.data.repository.SearchHistoryRepositoryImpl import zed.rainxch.core.data.repository.TelemetryRepositoryImpl +import zed.rainxch.core.data.repository.HiddenReposRepositoryImpl import zed.rainxch.core.data.repository.SeenReposRepositoryImpl import zed.rainxch.core.data.repository.StarredRepositoryImpl import zed.rainxch.core.data.repository.AnnouncementsCacheStoreImpl @@ -85,6 +87,7 @@ import zed.rainxch.core.domain.repository.MirrorRepository import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.RateLimitRepository import zed.rainxch.core.domain.repository.SearchHistoryRepository +import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository import zed.rainxch.core.domain.repository.TelemetryRepository @@ -190,6 +193,12 @@ val coreModule = ) } + single { + HiddenReposRepositoryImpl( + hiddenRepoDao = get(), + ) + } + single { SearchHistoryRepositoryImpl( searchHistoryDao = get(), @@ -426,6 +435,10 @@ val databaseModule = get().seenRepoDao } + single { + get().hiddenRepoDao + } + single { get().searchHistoryDao } 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 e792e45cb..995e57e00 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 @@ -5,6 +5,7 @@ import androidx.room.RoomDatabase import zed.rainxch.core.data.local.db.dao.CacheDao import zed.rainxch.core.data.local.db.dao.ExternalLinkDao import zed.rainxch.core.data.local.db.dao.FavoriteRepoDao +import zed.rainxch.core.data.local.db.dao.HiddenRepoDao import zed.rainxch.core.data.local.db.dao.InstalledAppDao import zed.rainxch.core.data.local.db.dao.SearchHistoryDao import zed.rainxch.core.data.local.db.dao.SeenRepoDao @@ -14,6 +15,7 @@ import zed.rainxch.core.data.local.db.dao.UpdateHistoryDao import zed.rainxch.core.data.local.db.entities.CacheEntryEntity import zed.rainxch.core.data.local.db.entities.ExternalLinkEntity import zed.rainxch.core.data.local.db.entities.FavoriteRepoEntity +import zed.rainxch.core.data.local.db.entities.HiddenRepoEntity import zed.rainxch.core.data.local.db.entities.InstalledAppEntity import zed.rainxch.core.data.local.db.entities.SearchHistoryEntity import zed.rainxch.core.data.local.db.entities.SeenRepoEntity @@ -32,8 +34,9 @@ import zed.rainxch.core.data.local.db.entities.UpdateHistoryEntity SearchHistoryEntity::class, ExternalLinkEntity::class, SigningFingerprintEntity::class, + HiddenRepoEntity::class, ], - version = 16, + version = 17, exportSchema = true, ) abstract class AppDatabase : RoomDatabase() { @@ -46,4 +49,5 @@ abstract class AppDatabase : RoomDatabase() { abstract val searchHistoryDao: SearchHistoryDao abstract val externalLinkDao: ExternalLinkDao abstract val signingFingerprintDao: SigningFingerprintDao + abstract val hiddenRepoDao: HiddenRepoDao } diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/HiddenRepoDao.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/HiddenRepoDao.kt new file mode 100644 index 000000000..e089ddf2d --- /dev/null +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/dao/HiddenRepoDao.kt @@ -0,0 +1,26 @@ +package zed.rainxch.core.data.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import zed.rainxch.core.data.local.db.entities.HiddenRepoEntity + +@Dao +interface HiddenRepoDao { + @Query("SELECT repoId FROM hidden_repos") + fun getAllHiddenRepoIds(): Flow> + + @Query("SELECT * FROM hidden_repos ORDER BY hiddenAt DESC") + fun getAllHiddenRepos(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entity: HiddenRepoEntity) + + @Query("DELETE FROM hidden_repos WHERE repoId = :repoId") + suspend fun deleteById(repoId: Long) + + @Query("DELETE FROM hidden_repos") + suspend fun clearAll() +} diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/HiddenRepoEntity.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/HiddenRepoEntity.kt new file mode 100644 index 000000000..b88f95556 --- /dev/null +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/local/db/entities/HiddenRepoEntity.kt @@ -0,0 +1,14 @@ +package zed.rainxch.core.data.local.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "hidden_repos") +data class HiddenRepoEntity( + @PrimaryKey + val repoId: Long, + val repoName: String, + val repoOwner: String, + val repoOwnerAvatarUrl: String, + val hiddenAt: Long, +) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HiddenReposRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HiddenReposRepositoryImpl.kt new file mode 100644 index 000000000..b1ef855bc --- /dev/null +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/HiddenReposRepositoryImpl.kt @@ -0,0 +1,54 @@ +package zed.rainxch.core.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import zed.rainxch.core.data.local.db.dao.HiddenRepoDao +import zed.rainxch.core.data.local.db.entities.HiddenRepoEntity +import zed.rainxch.core.domain.model.HiddenRepo +import zed.rainxch.core.domain.repository.HiddenReposRepository + +class HiddenReposRepositoryImpl( + private val hiddenRepoDao: HiddenRepoDao, +) : HiddenReposRepository { + override fun getAllHiddenRepoIds(): Flow> = + hiddenRepoDao.getAllHiddenRepoIds().map { it.toSet() } + + override fun getAllHiddenRepos(): Flow> = + hiddenRepoDao.getAllHiddenRepos().map { entities -> + entities.map { it.toDomain() } + } + + override suspend fun hide( + repoId: Long, + repoName: String, + repoOwner: String, + repoOwnerAvatarUrl: String, + ) { + hiddenRepoDao.insert( + HiddenRepoEntity( + repoId = repoId, + repoName = repoName, + repoOwner = repoOwner, + repoOwnerAvatarUrl = repoOwnerAvatarUrl, + hiddenAt = System.currentTimeMillis(), + ), + ) + } + + override suspend fun unhide(repoId: Long) { + hiddenRepoDao.deleteById(repoId) + } + + override suspend fun clearAll() { + hiddenRepoDao.clearAll() + } + + private fun HiddenRepoEntity.toDomain(): HiddenRepo = + HiddenRepo( + repoId = repoId, + repoName = repoName, + repoOwner = repoOwner, + repoOwnerAvatarUrl = repoOwnerAvatarUrl, + hiddenAt = hiddenAt, + ) +} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HiddenRepo.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HiddenRepo.kt new file mode 100644 index 000000000..16b9c3798 --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/HiddenRepo.kt @@ -0,0 +1,9 @@ +package zed.rainxch.core.domain.model + +data class HiddenRepo( + val repoId: Long, + val repoName: String, + val repoOwner: String, + val repoOwnerAvatarUrl: String, + val hiddenAt: Long, +) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HiddenReposRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HiddenReposRepository.kt new file mode 100644 index 000000000..f8444edde --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/HiddenReposRepository.kt @@ -0,0 +1,21 @@ +package zed.rainxch.core.domain.repository + +import kotlinx.coroutines.flow.Flow +import zed.rainxch.core.domain.model.HiddenRepo + +interface HiddenReposRepository { + fun getAllHiddenRepoIds(): Flow> + + fun getAllHiddenRepos(): Flow> + + suspend fun hide( + repoId: Long, + repoName: String, + repoOwner: String, + repoOwnerAvatarUrl: String, + ) + + suspend fun unhide(repoId: Long) + + suspend fun clearAll() +} From be5a607f7cfdbc634ab82684dd3312f0c6f29769 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:11:40 +0500 Subject: [PATCH 02/12] feat(presentation): long-press menu with Hide repository on cards --- .../presentation/components/ExpressiveCard.kt | 72 +++++++++++++------ .../presentation/components/RepositoryCard.kt | 32 +++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt index 55e0c5d1e..2825c5061 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt @@ -1,5 +1,7 @@ package zed.rainxch.core.presentation.components +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardDefaults @@ -9,32 +11,60 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +@OptIn(ExperimentalFoundationApi::class) @Composable fun ExpressiveCard( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, content: @Composable () -> Unit, ) { - if (onClick != null) { - ElevatedCard( - modifier = modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - onClick = onClick, - shape = RoundedCornerShape(32.dp), - content = { content() }, - ) - } else { - ElevatedCard( - modifier = modifier.fillMaxWidth(), - colors = - CardDefaults.elevatedCardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - shape = RoundedCornerShape(32.dp), - content = { content() }, - ) + when { + onClick != null && onLongClick != null -> { + // ElevatedCard's built-in `onClick` doesn't expose long-press; + // route both gestures through `combinedClickable` on the modifier + // instead so callers can attach a hide-menu without sacrificing + // the tap ripple. + ElevatedCard( + modifier = + modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ), + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + shape = RoundedCornerShape(32.dp), + content = { content() }, + ) + } + + onClick != null -> { + ElevatedCard( + modifier = modifier.fillMaxWidth(), + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + onClick = onClick, + shape = RoundedCornerShape(32.dp), + content = { content() }, + ) + } + + else -> { + ElevatedCard( + modifier = modifier.fillMaxWidth(), + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + shape = RoundedCornerShape(32.dp), + content = { content() }, + ) + } } } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 55d5c283f..58fa68386 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Verified import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.OpenInBrowser +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.outlined.Code @@ -28,6 +29,8 @@ import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -39,6 +42,9 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -60,6 +66,7 @@ import zed.rainxch.core.presentation.utils.hasWeekNotPassed import zed.rainxch.core.presentation.utils.toIcons import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.forked_repository +import zed.rainxch.githubstore.core.presentation.res.hide_repository import zed.rainxch.githubstore.core.presentation.res.home_view_details import zed.rainxch.githubstore.core.presentation.res.installed import zed.rainxch.githubstore.core.presentation.res.open_in_browser @@ -76,6 +83,7 @@ fun RepositoryCard( onShareClick: () -> Unit, onDeveloperClick: (String) -> Unit, modifier: Modifier = Modifier, + onHideClick: (() -> Unit)? = null, ) { val uriHandler = LocalUriHandler.current @@ -85,8 +93,11 @@ fun RepositoryCard( label = "seen_content_alpha", ) + var hideMenuExpanded by remember { mutableStateOf(false) } + ExpressiveCard( onClick = onClick, + onLongClick = onHideClick?.let { { hideMenuExpanded = true } }, modifier = modifier, ) { Box(modifier = Modifier.alpha(contentAlpha)) { @@ -336,6 +347,27 @@ fun RepositoryCard( } } } + + if (onHideClick != null) { + DropdownMenu( + expanded = hideMenuExpanded, + onDismissRequest = { hideMenuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.hide_repository)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = null, + ) + }, + onClick = { + hideMenuExpanded = false + onHideClick() + }, + ) + } + } } } } From 555bdd28b81ee251fbb5ff04db7deece918dbac7 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:11:46 +0500 Subject: [PATCH 03/12] feat(home,search): filter hidden repos + hide action wiring --- .../rainxch/home/presentation/HomeAction.kt | 8 +++ .../zed/rainxch/home/presentation/HomeRoot.kt | 3 + .../rainxch/home/presentation/HomeState.kt | 1 + .../home/presentation/HomeViewModel.kt | 43 +++++++++++++- .../search/presentation/SearchAction.kt | 8 +++ .../rainxch/search/presentation/SearchRoot.kt | 3 + .../search/presentation/SearchState.kt | 1 + .../search/presentation/SearchViewModel.kt | 56 +++++++++++++++++-- 8 files changed, 116 insertions(+), 7 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt index 3440b1976..0880cd746 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt @@ -45,4 +45,12 @@ sealed interface HomeAction { data class OnRepositoryDeveloperClick( val username: String, ) : HomeAction + + data class OnHideRepository( + val repo: GithubRepoSummaryUi, + ) : HomeAction + + data class OnUndoHideRepository( + val repoId: Long, + ) : HomeAction } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index c9cc03686..f7006e264 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -444,6 +444,9 @@ private fun MainState( onShareClick = { onAction(HomeAction.OnShareClick(discoveryRepository.repository)) }, + onHideClick = { + onAction(HomeAction.OnHideRepository(discoveryRepository.repository)) + }, modifier = Modifier.animateItem(), ) } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt index 349840406..f0f636e8b 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeState.kt @@ -29,4 +29,5 @@ data class HomeState( val isPlatformPopupVisible: Boolean = false, val isHideSeenEnabled: Boolean = false, val seenRepoIds: Set = emptySet(), + val hiddenRepoIds: Set = emptySet(), ) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index b7e13ecd9..00d1e1082 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -26,6 +26,7 @@ import zed.rainxch.core.domain.model.Platform import zed.rainxch.core.domain.model.hasActualUpdate import zed.rainxch.core.domain.model.isReallyInstalled import zed.rainxch.core.domain.repository.FavouritesRepository +import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.SeenReposRepository import zed.rainxch.core.domain.repository.StarredRepository @@ -52,6 +53,7 @@ class HomeViewModel( private val shareManager: ShareManager, private val tweaksRepository: TweaksRepository, private val seenReposRepository: SeenReposRepository, + private val hiddenReposRepository: HiddenReposRepository, private val profileRepository: ProfileRepository, ) : ViewModel() { private var hasLoadedInitialData = false @@ -79,6 +81,7 @@ class HomeViewModel( observeFavourites() observeStarredRepos() observeSeenRepos() + observeHiddenRepos() observeDiscoveryPlatforms() observeHideSeenEnabled() @@ -374,9 +377,10 @@ class HomeViewModel( .associateBy { it.repoId } val seenIds = _state.value.seenRepoIds + val hiddenIds = _state.value.hiddenRepoIds val currentLogin = currentUserLogin - return repos.map { repo -> + return repos.filter { it.id !in hiddenIds }.map { repo -> val apps = installedAppsMap[repo.id].orEmpty() val favourite = favoritesMap[repo.id] val starred = starredReposMap[repo.id] @@ -520,6 +524,24 @@ class HomeViewModel( // Handled in composable } + is HomeAction.OnHideRepository -> { + val repo = action.repo + viewModelScope.launch { + hiddenReposRepository.hide( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + ) + } + } + + is HomeAction.OnUndoHideRepository -> { + viewModelScope.launch { + hiddenReposRepository.unhide(action.repoId) + } + } + HomeAction.OnSearchClick -> { // Handled in composable } @@ -582,6 +604,25 @@ class HomeViewModel( } } + private fun observeHiddenRepos() { + viewModelScope.launch { + hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> + _state.update { current -> + current.copy( + hiddenRepoIds = ids, + // Drop already-loaded repos that are now hidden so + // the grid reacts immediately to a hide action + // without waiting for the next pagination tick. + repos = + current.repos + .filter { it.repository.id !in ids } + .toImmutableList(), + ) + } + } + } + } + private fun observeHideSeenEnabled() { viewModelScope.launch { tweaksRepository.getHideSeenEnabled().collect { enabled -> diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt index 41a4614c4..db3596a50 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt @@ -75,4 +75,12 @@ sealed interface SearchAction { data object ExploreFromGithub : SearchAction data object OnDisableHideSeenForResults : SearchAction + + data class OnHideRepository( + val repo: GithubRepoSummaryUi, + ) : SearchAction + + data class OnUndoHideRepository( + val repoId: Long, + ) : SearchAction } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 693e1b2eb..677679c66 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -692,6 +692,9 @@ fun SearchScreen( onShareClick = { onAction(SearchAction.OnShareClick(discoveryRepository.repository)) }, + onHideClick = { + onAction(SearchAction.OnHideRepository(discoveryRepository.repository)) + }, modifier = Modifier.animateItem(), ) } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt index 6058b3a33..576551a97 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt @@ -20,6 +20,7 @@ data class SearchState( val isLoading: Boolean = false, val isHideSeenEnabled: Boolean = false, val seenRepoIds: Set = emptySet(), + val hiddenRepoIds: Set = emptySet(), val isLoadingMore: Boolean = false, val errorMessage: String? = null, val hasMorePages: Boolean = true, diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 2c285c463..a60201883 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -24,6 +24,7 @@ import zed.rainxch.core.domain.model.RateLimitException import zed.rainxch.core.domain.model.hasActualUpdate import zed.rainxch.core.domain.model.isReallyInstalled import zed.rainxch.core.domain.repository.FavouritesRepository +import zed.rainxch.core.domain.repository.HiddenReposRepository import zed.rainxch.core.domain.repository.InstalledAppsRepository import zed.rainxch.core.domain.repository.SearchHistoryRepository import zed.rainxch.core.domain.repository.SeenReposRepository @@ -65,6 +66,7 @@ class SearchViewModel( private val searchHistoryRepository: SearchHistoryRepository, private val telemetryRepository: TelemetryRepository, private val profileRepository: ProfileRepository, + private val hiddenReposRepository: HiddenReposRepository, ) : ViewModel() { private var hasLoadedInitialData = false private var currentSearchJob: Job? = null @@ -100,6 +102,7 @@ class SearchViewModel( observeFavouriteApps() observeStarredRepos() observeSeenRepos() + observeHiddenRepos() observeHideSeenEnabled() observeClipboardSetting() observeSearchHistory() @@ -115,12 +118,17 @@ class SearchViewModel( initialValue = SearchState(), ) - private fun computeVisibleRepos(state: SearchState): ImmutableList = - if (state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty()) { - state.repositories.filter { it.repository.id !in state.seenRepoIds }.toImmutableList() - } else { - state.repositories - } + private fun computeVisibleRepos(state: SearchState): ImmutableList { + val hidden = state.hiddenRepoIds + val needsHideSeenFilter = state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty() + if (hidden.isEmpty() && !needsHideSeenFilter) return state.repositories + return state.repositories + .filter { repo -> + repo.repository.id !in hidden && + (!needsHideSeenFilter || repo.repository.id !in state.seenRepoIds) + } + .toImmutableList() + } private fun observeSeenRepos() { viewModelScope.launch { @@ -147,6 +155,24 @@ class SearchViewModel( } } + private fun observeHiddenRepos() { + viewModelScope.launch { + hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> + _state.update { current -> + current.copy( + hiddenRepoIds = ids, + // Drop already-loaded repos that are now hidden so + // the grid reacts immediately to a hide action. + repositories = + current.repositories + .filter { it.repository.id !in ids } + .toImmutableList(), + ) + } + } + } + } + private fun observeCurrentUser() { viewModelScope.launch { profileRepository.getUser().collect { user -> @@ -723,6 +749,24 @@ class SearchViewModel( tweaksRepository.setHideSeenEnabled(false) } } + + is SearchAction.OnHideRepository -> { + val repo = action.repo + viewModelScope.launch { + hiddenReposRepository.hide( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + ) + } + } + + is SearchAction.OnUndoHideRepository -> { + viewModelScope.launch { + hiddenReposRepository.unhide(action.repoId) + } + } } } From d417390353d1596deca8fe8a77d177eddfed1f41 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:11:54 +0500 Subject: [PATCH 04/12] chore(strings): hide-repository strings across 13 locales --- .../src/commonMain/composeResources/values-ar/strings-ar.xml | 3 +++ .../src/commonMain/composeResources/values-bn/strings-bn.xml | 3 +++ .../src/commonMain/composeResources/values-es/strings-es.xml | 3 +++ .../src/commonMain/composeResources/values-fr/strings-fr.xml | 3 +++ .../src/commonMain/composeResources/values-hi/strings-hi.xml | 3 +++ .../src/commonMain/composeResources/values-it/strings-it.xml | 3 +++ .../src/commonMain/composeResources/values-ja/strings-ja.xml | 3 +++ .../src/commonMain/composeResources/values-ko/strings-ko.xml | 3 +++ .../src/commonMain/composeResources/values-pl/strings-pl.xml | 3 +++ .../src/commonMain/composeResources/values-ru/strings-ru.xml | 3 +++ .../src/commonMain/composeResources/values-tr/strings-tr.xml | 3 +++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 3 +++ .../src/commonMain/composeResources/values/strings.xml | 3 +++ 13 files changed, 39 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 6e3eb111e..847da2ca5 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -645,6 +645,9 @@ تم مسح سجل المشاهدة تمت المشاهدة هذا المستودع لك + إخفاء المستودع + تم إخفاء المستودع + تراجع الخصوصية ساعد في تحسين البحث مشاركة بيانات الاستخدام (عمليات البحث والتثبيتات والتفاعلات) المرتبطة بمعرّف تحليلي قابل لإعادة التعيين. لا تتم مشاركة تفاصيل الحساب. diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 7da791d3d..588ff15cd 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -644,6 +644,9 @@ দেখার ইতিহাস মুছে ফেলা হয়েছে দেখা হয়েছে এই রিপো আপনার + রিপোজিটরি লুকান + রিপোজিটরি লুকানো হয়েছে + পূর্বাবস্থা গোপনীয়তা অনুসন্ধান উন্নত করতে সহায়তা করুন পুনরায় সেট করা যায় এমন একটি বিশ্লেষণ আইডির সাথে যুক্ত ব্যবহার ডেটা (অনুসন্ধান, ইনস্টল, ইন্টারঅ্যাকশন) শেয়ার করুন। অ্যাকাউন্ট বিবরণ শেয়ার করা হয় না। diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index b7c0777b4..4012a74e7 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -605,6 +605,9 @@ Historial de vistos borrado Visto Eres dueño de este repo + Ocultar repositorio + Repositorio ocultado + Deshacer Privacidad Ayudar a mejorar la búsqueda Compartir datos de uso (búsquedas, instalaciones, interacciones) asociados a un ID de análisis restablecible. No se comparten detalles de la cuenta. diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index e9a72b942..fbb70b279 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -606,6 +606,9 @@ Historique des consultations effacé Consulté Vous possédez ce dépôt + Masquer le dépôt + Dépôt masqué + Annuler Confidentialité Aider à améliorer la recherche Partager des données d'utilisation (recherches, installations, interactions) associées à un identifiant d'analyse réinitialisable. Aucun détail de compte n'est partagé. diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index a31eaeeeb..408e4c887 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -643,6 +643,9 @@ देखने का इतिहास साफ़ किया गया देखा गया यह रेपो आपका है + रिपॉजिटरी छिपाएं + रिपॉजिटरी छिपाई गई + पूर्ववत् गोपनीयता खोज को बेहतर बनाने में मदद करें रीसेट करने योग्य एनालिटिक्स आईडी से जुड़े उपयोग डेटा (खोज, इंस्टॉल, इंटरैक्शन) साझा करें। खाता विवरण साझा नहीं किए जाते। diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 4f0394421..12f60186c 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -644,6 +644,9 @@ Cronologia visualizzazioni cancellata Visualizzato Sei il proprietario di questo repo + Nascondi repository + Repository nascosto + Annulla Privacy Aiuta a migliorare la ricerca Condividi dati di utilizzo (ricerche, installazioni, interazioni) associati a un ID analitico ripristinabile. Nessun dettaglio dell'account viene condiviso. diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index b19cce2fd..6b767afb6 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -607,6 +607,9 @@ 閲覧履歴をクリアしました 閲覧済み あなたが所有するリポジトリ + リポジトリを非表示 + リポジトリを非表示にしました + 元に戻す プライバシー 検索の改善に協力 リセット可能な分析 ID に関連付けられた使用データ(検索、インストール、操作)を共有します。アカウント情報は共有されません。 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index d6800c755..d039ae381 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -642,6 +642,9 @@ 조회 기록이 삭제되었습니다 확인함 내가 소유한 저장소 + 저장소 숨기기 + 저장소가 숨겨졌습니다 + 실행 취소 개인 정보 보호 검색 개선에 도움 주기 재설정 가능한 분석 ID에 연결된 사용 데이터(검색, 설치, 상호작용)를 공유합니다. 계정 정보는 공유되지 않습니다. diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index dd3c9c502..e702c784b 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -608,6 +608,9 @@ Historia przeglądania wyczyszczona Przeglądane Jesteś właścicielem tego repo + Ukryj repozytorium + Repozytorium ukryte + Cofnij Prywatność Pomóż ulepszyć wyszukiwanie Udostępniaj dane o użyciu (wyszukiwania, instalacje, interakcje) powiązane z resetowalnym identyfikatorem analitycznym. Żadne dane konta nie są udostępniane. diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index 753f382cb..a4605f813 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -608,6 +608,9 @@ История просмотров очищена Просмотрено Ваш репозиторий + Скрыть репозиторий + Репозиторий скрыт + Отменить Конфиденциальность Помочь улучшить поиск Отправлять данные об использовании (поиски, установки, взаимодействия), связанные со сбрасываемым идентификатором аналитики. Данные учётной записи не передаются. diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 09bb56bd1..e039206e1 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -642,6 +642,9 @@ Görüntüleme geçmişi temizlendi Görüntülendi Bu deponun sahibisin + Depoyu gizle + Depo gizlendi + Geri al Gizlilik Aramayı iyileştirmeye yardım et Sıfırlanabilir bir analiz kimliğiyle ilişkilendirilmiş kullanım verilerini (aramalar, yüklemeler, etkileşimler) paylaş. Hesap ayrıntıları paylaşılmaz. diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 17fa993d3..c9854d0e5 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -608,6 +608,9 @@ 浏览记录已清除 已浏览 你拥有此仓库 + 隐藏仓库 + 仓库已隐藏 + 撤销 隐私 帮助改进搜索 分享与可重置分析 ID 关联的使用数据(搜索、安装、交互)。不分享账户详情。 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index c7b4482f0..f2ab0cdcb 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -771,6 +771,9 @@ Seen history cleared Viewed You own this repo + Hide repository + Repository hidden + Undo Privacy From f4016101abc1c5a774e2690fea839cad35234a96 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:11:59 +0500 Subject: [PATCH 05/12] chore(whatsnew): note hide repository --- .../src/commonMain/composeResources/files/whatsnew/17.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json index e39962ce0..24d54daf0 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json @@ -11,7 +11,8 @@ "Skip a specific update — dismiss a single release if it's a false-positive prompt; you'll be re-notified when a newer one lands.", "Silent install via root — Magisk, KernelSU, and APatch users can now install without Shizuku or Dhizuku.", "Search in Starred and Favourites — quickly filter long lists by repo name, owner, description, or language.", - "Self-owned ✓ badge — when you're signed in, repos you own get a verified checkmark on Home and Search cards." + "Self-owned ✓ badge — when you're signed in, repos you own get a verified checkmark on Home and Search cards.", + "Hide repository — long-press any repo card on Home or Search to hide it from discovery. The repo stays in your library if you have it installed." ] }, { From e5f359b4f60b0f8afae073c4201ba0651a7a0b21 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:30:36 +0500 Subject: [PATCH 06/12] fix(hide): keep ids tracked, render-time filter, alpha-safe menu --- .../presentation/components/ExpressiveCard.kt | 6 +++ .../presentation/components/RepositoryCard.kt | 44 +++++++++++-------- .../zed/rainxch/home/presentation/HomeRoot.kt | 18 ++++++-- .../home/presentation/HomeViewModel.kt | 20 +++------ .../search/presentation/SearchViewModel.kt | 16 +++---- 5 files changed, 56 insertions(+), 48 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt index 2825c5061..2960d8df6 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt @@ -19,6 +19,12 @@ fun ExpressiveCard( onLongClick: (() -> Unit)? = null, content: @Composable () -> Unit, ) { + // Long-press without tap leaves the gesture orphaned: the card looks + // tappable but only responds to a hold. Fail loud so the API contract + // is obvious at the call site. + check(onLongClick == null || onClick != null) { + "ExpressiveCard: onLongClick requires onClick" + } when { onClick != null && onLongClick != null -> { // ElevatedCard's built-in `onClick` doesn't expose long-press; diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 58fa68386..0e91fabcb 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -100,6 +100,11 @@ fun RepositoryCard( onLongClick = onHideClick?.let { { hideMenuExpanded = true } }, modifier = modifier, ) { + // Outer Box (no alpha) hosts both the dimmed visual content and the + // DropdownMenu. Putting the menu inside the alpha Box made the + // popup inherit `contentAlpha = 0.55f` whenever the repo was + // already seen, dimming the menu surface. + Box { Box(modifier = Modifier.alpha(contentAlpha)) { if (discoveryRepositoryUi.isFavourite) { Icon( @@ -347,28 +352,29 @@ fun RepositoryCard( } } } + } - if (onHideClick != null) { - DropdownMenu( - expanded = hideMenuExpanded, - onDismissRequest = { hideMenuExpanded = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.hide_repository)) }, - leadingIcon = { - Icon( - imageVector = Icons.Default.VisibilityOff, - contentDescription = null, - ) - }, - onClick = { - hideMenuExpanded = false - onHideClick() - }, - ) - } + if (onHideClick != null) { + DropdownMenu( + expanded = hideMenuExpanded, + onDismissRequest = { hideMenuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.hide_repository)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = null, + ) + }, + onClick = { + hideMenuExpanded = false + onHideClick() + }, + ) } } + } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index f7006e264..88328e44c 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -397,12 +397,22 @@ private fun MainState( onAction: (HomeAction) -> Unit, ) { val bottomNavHeight = LocalBottomNavigationHeight.current - val visibleRepos by remember(state.repos, state.isHideSeenEnabled, state.seenRepoIds) { + val visibleRepos by remember( + state.repos, + state.isHideSeenEnabled, + state.seenRepoIds, + state.hiddenRepoIds, + ) { derivedStateOf { - if (state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty()) { - state.repos.filter { it.repository.id !in state.seenRepoIds } - } else { + val hidden = state.hiddenRepoIds + val needsHideSeen = state.isHideSeenEnabled && state.seenRepoIds.isNotEmpty() + if (hidden.isEmpty() && !needsHideSeen) { state.repos + } else { + state.repos.filter { repo -> + repo.repository.id !in hidden && + (!needsHideSeen || repo.repository.id !in state.seenRepoIds) + } } } } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 00d1e1082..86879b568 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -377,10 +377,9 @@ class HomeViewModel( .associateBy { it.repoId } val seenIds = _state.value.seenRepoIds - val hiddenIds = _state.value.hiddenRepoIds val currentLogin = currentUserLogin - return repos.filter { it.id !in hiddenIds }.map { repo -> + return repos.map { repo -> val apps = installedAppsMap[repo.id].orEmpty() val favourite = favoritesMap[repo.id] val starred = starredReposMap[repo.id] @@ -607,18 +606,11 @@ class HomeViewModel( private fun observeHiddenRepos() { viewModelScope.launch { hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> - _state.update { current -> - current.copy( - hiddenRepoIds = ids, - // Drop already-loaded repos that are now hidden so - // the grid reacts immediately to a hide action - // without waiting for the next pagination tick. - repos = - current.repos - .filter { it.repository.id !in ids } - .toImmutableList(), - ) - } + // Track IDs only — mirror the `seenRepoIds` pattern so + // unhiding restores the repo to the visible list without + // a refresh. `HomeRoot.visibleRepos` filters at render + // time using these IDs. + _state.update { it.copy(hiddenRepoIds = ids) } } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index a60201883..82f9e6f90 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -158,17 +158,11 @@ class SearchViewModel( private fun observeHiddenRepos() { viewModelScope.launch { hiddenReposRepository.getAllHiddenRepoIds().collect { ids -> - _state.update { current -> - current.copy( - hiddenRepoIds = ids, - // Drop already-loaded repos that are now hidden so - // the grid reacts immediately to a hide action. - repositories = - current.repositories - .filter { it.repository.id !in ids } - .toImmutableList(), - ) - } + // Track IDs only — `computeVisibleRepos` already filters + // hidden at render time. Removing from `repositories` + // would break `OnUndoHideRepository`: once the entity is + // gone there's nothing to bring back without re-fetching. + _state.update { it.copy(hiddenRepoIds = ids) } } } } From 6be8888794b5559120536fb8d467d777f71c167a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:37:52 +0500 Subject: [PATCH 07/12] fix(presentation): clip card ripple + bottom sheet for hide action --- .../presentation/components/ExpressiveCard.kt | 17 +-- .../presentation/components/RepositoryCard.kt | 101 ++++++++++++------ 2 files changed, 82 insertions(+), 36 deletions(-) diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt index 2960d8df6..6f365696c 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/ExpressiveCard.kt @@ -9,8 +9,11 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +private val EXPRESSIVE_CARD_SHAPE = RoundedCornerShape(32.dp) + @OptIn(ExperimentalFoundationApi::class) @Composable fun ExpressiveCard( @@ -28,13 +31,15 @@ fun ExpressiveCard( when { onClick != null && onLongClick != null -> { // ElevatedCard's built-in `onClick` doesn't expose long-press; - // route both gestures through `combinedClickable` on the modifier - // instead so callers can attach a hide-menu without sacrificing - // the tap ripple. + // route both gestures through `combinedClickable`. Clip the + // modifier chain to the card shape FIRST so the ripple + // respects the 32.dp rounded corners — without the clip the + // ripple draws as a square overlapping the card edges. ElevatedCard( modifier = modifier .fillMaxWidth() + .clip(EXPRESSIVE_CARD_SHAPE) .combinedClickable( onClick = onClick, onLongClick = onLongClick, @@ -43,7 +48,7 @@ fun ExpressiveCard( CardDefaults.elevatedCardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, ), - shape = RoundedCornerShape(32.dp), + shape = EXPRESSIVE_CARD_SHAPE, content = { content() }, ) } @@ -56,7 +61,7 @@ fun ExpressiveCard( containerColor = MaterialTheme.colorScheme.surfaceContainer, ), onClick = onClick, - shape = RoundedCornerShape(32.dp), + shape = EXPRESSIVE_CARD_SHAPE, content = { content() }, ) } @@ -68,7 +73,7 @@ fun ExpressiveCard( CardDefaults.elevatedCardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer, ), - shape = RoundedCornerShape(32.dp), + shape = EXPRESSIVE_CARD_SHAPE, content = { content() }, ) } diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index 0e91fabcb..b2e71cb52 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -29,10 +29,13 @@ import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme @@ -75,7 +78,11 @@ import zed.rainxch.githubstore.core.presentation.res.self_owned_badge import zed.rainxch.githubstore.core.presentation.res.share_repository import zed.rainxch.githubstore.core.presentation.res.update_available -@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalLayoutApi::class) +@OptIn( + ExperimentalMaterial3ExpressiveApi::class, + ExperimentalLayoutApi::class, + ExperimentalMaterial3Api::class, +) @Composable fun RepositoryCard( discoveryRepositoryUi: DiscoveryRepositoryUi, @@ -93,18 +100,13 @@ fun RepositoryCard( label = "seen_content_alpha", ) - var hideMenuExpanded by remember { mutableStateOf(false) } + var showHideSheet by remember { mutableStateOf(false) } ExpressiveCard( onClick = onClick, - onLongClick = onHideClick?.let { { hideMenuExpanded = true } }, + onLongClick = onHideClick?.let { { showHideSheet = true } }, modifier = modifier, ) { - // Outer Box (no alpha) hosts both the dimmed visual content and the - // DropdownMenu. Putting the menu inside the alpha Box made the - // popup inherit `contentAlpha = 0.55f` whenever the repo was - // already seen, dimming the menu surface. - Box { Box(modifier = Modifier.alpha(contentAlpha)) { if (discoveryRepositoryUi.isFavourite) { Icon( @@ -353,27 +355,66 @@ fun RepositoryCard( } } } + } - if (onHideClick != null) { - DropdownMenu( - expanded = hideMenuExpanded, - onDismissRequest = { hideMenuExpanded = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.hide_repository)) }, - leadingIcon = { - Icon( - imageVector = Icons.Default.VisibilityOff, - contentDescription = null, - ) - }, - onClick = { - hideMenuExpanded = false - onHideClick() - }, - ) - } - } + if (onHideClick != null && showHideSheet) { + HideRepositoryBottomSheet( + repoName = discoveryRepositoryUi.repository.fullName, + onDismiss = { showHideSheet = false }, + onConfirmHide = { + showHideSheet = false + onHideClick() + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HideRepositoryBottomSheet( + repoName: String, + onDismiss: () -> Unit, + onConfirmHide: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + ) { + Text( + text = repoName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 8.dp), + ) + + ListItem( + headlineContent = { + Text(stringResource(Res.string.hide_repository)) + }, + leadingContent = { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = null, + ) + }, + colors = + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ), + modifier = Modifier.clickable(onClick = onConfirmHide), + ) } } } From 9d4951125fc6238a65eafbc5e35c90cc12bb6b0e Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:42:30 +0500 Subject: [PATCH 08/12] fix(hide): catch + log repo errors, rethrow CancellationException --- .../home/presentation/HomeViewModel.kt | 26 ++++++++++++++----- .../search/presentation/SearchViewModel.kt | 26 ++++++++++++++----- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index 86879b568..eade03abf 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -526,18 +526,30 @@ class HomeViewModel( is HomeAction.OnHideRepository -> { val repo = action.repo viewModelScope.launch { - hiddenReposRepository.hide( - repoId = repo.id, - repoName = repo.name, - repoOwner = repo.owner.login, - repoOwnerAvatarUrl = repo.owner.avatarUrl, - ) + try { + hiddenReposRepository.hide( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Hide repository failed for ${repo.id}: ${e.message}") + } } } is HomeAction.OnUndoHideRepository -> { viewModelScope.launch { - hiddenReposRepository.unhide(action.repoId) + try { + hiddenReposRepository.unhide(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Unhide repository failed for ${action.repoId}: ${e.message}") + } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 82f9e6f90..5d7169133 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -747,18 +747,30 @@ class SearchViewModel( is SearchAction.OnHideRepository -> { val repo = action.repo viewModelScope.launch { - hiddenReposRepository.hide( - repoId = repo.id, - repoName = repo.name, - repoOwner = repo.owner.login, - repoOwnerAvatarUrl = repo.owner.avatarUrl, - ) + try { + hiddenReposRepository.hide( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Hide repository failed for ${repo.id}: ${e.message}") + } } } is SearchAction.OnUndoHideRepository -> { viewModelScope.launch { - hiddenReposRepository.unhide(action.repoId) + try { + hiddenReposRepository.unhide(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Unhide repository failed for ${action.repoId}: ${e.message}") + } } } } From 8e7cc0bd858762a64d843ffa0d0bcb364d899f63 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:55:52 +0500 Subject: [PATCH 09/12] feat(tweaks): Hidden Repositories management screen with Unhide --- .../tweaks/presentation/TweaksAction.kt | 2 + .../rainxch/tweaks/presentation/TweaksRoot.kt | 3 + .../tweaks/presentation/TweaksViewModel.kt | 4 + .../components/sections/Installation.kt | 44 ++++ .../hidden/HiddenRepositoriesAction.kt | 7 + .../hidden/HiddenRepositoriesEvent.kt | 9 + .../hidden/HiddenRepositoriesRoot.kt | 238 ++++++++++++++++++ .../hidden/HiddenRepositoriesState.kt | 18 ++ .../hidden/HiddenRepositoriesViewModel.kt | 82 ++++++ 9 files changed, 407 insertions(+) create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesAction.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesEvent.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesState.kt create mode 100644 feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt index adb1d266b..905b558b6 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt @@ -131,6 +131,8 @@ sealed interface TweaksAction { data object OnSkippedUpdatesClick : TweaksAction + data object OnHiddenRepositoriesClick : TweaksAction + data class OnTelemetryToggled( val enabled: Boolean, ) : TweaksAction diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt index 1a0fbf151..19bcfdd7d 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt @@ -46,6 +46,7 @@ import zed.rainxch.tweaks.presentation.feedback.model.FeedbackChannel fun TweaksRoot( onNavigateToMirrorPicker: () -> Unit, onNavigateToSkippedUpdates: () -> Unit, + onNavigateToHiddenRepositories: () -> Unit, viewModel: TweaksViewModel = koinViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -155,6 +156,8 @@ fun TweaksRoot( when (action) { TweaksAction.OnMirrorPickerClick -> onNavigateToMirrorPicker() TweaksAction.OnSkippedUpdatesClick -> onNavigateToSkippedUpdates() + + TweaksAction.OnHiddenRepositoriesClick -> onNavigateToHiddenRepositories() else -> viewModel.onAction(action) } }, diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt index 3a33905c3..376755616 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt @@ -523,6 +523,10 @@ class TweaksViewModel( // Handled in composable (navigates to the skipped-updates screen). } + TweaksAction.OnHiddenRepositoriesClick -> { + // Handled in composable (navigates to the hidden-repositories screen). + } + is TweaksAction.OnThemeColorSelected -> { viewModelScope.launch { tweaksRepository.setThemeColor(action.themeColor) diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt index 769123ac2..b7bfdeb16 100644 --- a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Installation.kt @@ -348,6 +348,50 @@ fun LazyListScope.updatesSection( SkippedUpdatesEntryCard( onClick = { onAction(TweaksAction.OnSkippedUpdatesClick) }, ) + + Spacer(Modifier.height(12.dp)) + + HiddenRepositoriesEntryCard( + onClick = { onAction(TweaksAction.OnHiddenRepositoriesClick) }, + ) + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun HiddenRepositoriesEntryCard( + onClick: () -> Unit, +) { + OutlinedCard( + onClick = onClick, + colors = + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + ), + shape = RoundedCornerShape(32.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(Res.string.hidden_repositories_title), + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = stringResource(Res.string.hidden_repositories_entry_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + ) + } } } diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesAction.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesAction.kt new file mode 100644 index 000000000..40bd36a01 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesAction.kt @@ -0,0 +1,7 @@ +package zed.rainxch.tweaks.presentation.hidden + +sealed interface HiddenRepositoriesAction { + data class OnUnhide(val repoId: Long) : HiddenRepositoriesAction + + data object OnUnhideAll : HiddenRepositoriesAction +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesEvent.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesEvent.kt new file mode 100644 index 000000000..4fda3ff6b --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesEvent.kt @@ -0,0 +1,9 @@ +package zed.rainxch.tweaks.presentation.hidden + +sealed interface HiddenRepositoriesEvent { + data class Unhidden(val repoFullName: String) : HiddenRepositoriesEvent + + data object UnhiddenAll : HiddenRepositoriesEvent + + data class Failure(val message: String) : HiddenRepositoriesEvent +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt new file mode 100644 index 000000000..edbbf0f69 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesRoot.kt @@ -0,0 +1,238 @@ +package zed.rainxch.tweaks.presentation.hidden + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.ArrowBack +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +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.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel +import zed.rainxch.core.presentation.components.GitHubStoreImage +import zed.rainxch.core.presentation.utils.ObserveAsEvents +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_count +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_empty_description +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_empty_title +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_title +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_unhide_action +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_unhide_all +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_unhide_failure +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_unhidden_all_snackbar +import zed.rainxch.githubstore.core.presentation.res.hidden_repositories_unhidden_snackbar +import zed.rainxch.githubstore.core.presentation.res.navigate_back + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HiddenRepositoriesRoot( + onNavigateBack: () -> Unit, + viewModel: HiddenRepositoriesViewModel = koinViewModel(), +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val snackbarState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + ObserveAsEvents(viewModel.events) { event -> + when (event) { + is HiddenRepositoriesEvent.Unhidden -> + scope.launch { + snackbarState.showSnackbar( + getString(Res.string.hidden_repositories_unhidden_snackbar, event.repoFullName), + ) + } + HiddenRepositoriesEvent.UnhiddenAll -> + scope.launch { + snackbarState.showSnackbar( + getString(Res.string.hidden_repositories_unhidden_all_snackbar), + ) + } + is HiddenRepositoriesEvent.Failure -> + scope.launch { + snackbarState.showSnackbar( + getString(Res.string.hidden_repositories_unhide_failure), + ) + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text( + text = stringResource(Res.string.hidden_repositories_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + if (state.items.isNotEmpty()) { + Text( + text = stringResource(Res.string.hidden_repositories_count, state.items.size), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(Res.string.navigate_back), + ) + } + }, + actions = { + if (state.items.isNotEmpty()) { + TextButton( + onClick = { + viewModel.onAction(HiddenRepositoriesAction.OnUnhideAll) + }, + ) { + Text(stringResource(Res.string.hidden_repositories_unhide_all)) + } + } + }, + ) + }, + snackbarHost = { SnackbarHost(snackbarState) }, + ) { padding -> + Box( + modifier = + Modifier + .fillMaxSize() + .padding(padding), + ) { + when { + state.isLoading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + + state.items.isEmpty() -> { + Column( + modifier = + Modifier + .align(Alignment.Center) + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(Res.string.hidden_repositories_empty_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.size(8.dp)) + Text( + text = stringResource(Res.string.hidden_repositories_empty_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(state.items, key = { it.repoId }) { item -> + HiddenRepoRow( + item = item, + onUnhide = { + viewModel.onAction(HiddenRepositoriesAction.OnUnhide(item.repoId)) + }, + ) + } + } + } + } + } + } +} + +@Composable +private fun HiddenRepoRow( + item: HiddenRepoUi, + onUnhide: () -> Unit, +) { + OutlinedCard( + colors = + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + ), + shape = RoundedCornerShape(24.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + GitHubStoreImage( + imageModel = { item.repoOwnerAvatarUrl }, + modifier = + Modifier + .size(36.dp) + .clip(CircleShape), + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = item.repoName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = item.repoOwner, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + TextButton(onClick = onUnhide) { + Text(stringResource(Res.string.hidden_repositories_unhide_action)) + } + } + } +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesState.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesState.kt new file mode 100644 index 000000000..a7d69a421 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesState.kt @@ -0,0 +1,18 @@ +package zed.rainxch.tweaks.presentation.hidden + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class HiddenRepositoriesState( + val isLoading: Boolean = true, + val items: ImmutableList = persistentListOf(), +) + +data class HiddenRepoUi( + val repoId: Long, + val repoName: String, + val repoOwner: String, + val repoOwnerAvatarUrl: String, +) { + val fullName: String get() = "$repoOwner/$repoName" +} diff --git a/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt new file mode 100644 index 000000000..f7f434b04 --- /dev/null +++ b/feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/hidden/HiddenRepositoriesViewModel.kt @@ -0,0 +1,82 @@ +package zed.rainxch.tweaks.presentation.hidden + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import zed.rainxch.core.domain.repository.HiddenReposRepository + +class HiddenRepositoriesViewModel( + private val hiddenReposRepository: HiddenReposRepository, +) : ViewModel() { + private val _state = MutableStateFlow(HiddenRepositoriesState()) + val state = _state.asStateFlow() + + private val _events = Channel() + val events = _events.receiveAsFlow() + + init { + hiddenReposRepository + .getAllHiddenRepos() + .onEach { repos -> + _state.value = + HiddenRepositoriesState( + isLoading = false, + items = + repos.map { repo -> + HiddenRepoUi( + repoId = repo.repoId, + repoName = repo.repoName, + repoOwner = repo.repoOwner, + repoOwnerAvatarUrl = repo.repoOwnerAvatarUrl, + ) + }.toImmutableList(), + ) + } + .launchIn(viewModelScope) + } + + fun onAction(action: HiddenRepositoriesAction) { + when (action) { + is HiddenRepositoriesAction.OnUnhide -> unhide(action.repoId) + HiddenRepositoriesAction.OnUnhideAll -> unhideAll() + } + } + + private fun unhide(repoId: Long) { + // Snapshot the row name BEFORE the flow re-emits without it, so the + // success snackbar can name what the user just acted on. + val fullName = + _state.value.items.firstOrNull { it.repoId == repoId }?.fullName.orEmpty() + viewModelScope.launch { + try { + hiddenReposRepository.unhide(repoId) + _events.send(HiddenRepositoriesEvent.Unhidden(fullName)) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _events.send(HiddenRepositoriesEvent.Failure(e.message.orEmpty())) + } + } + } + + private fun unhideAll() { + viewModelScope.launch { + try { + hiddenReposRepository.clearAll() + _events.send(HiddenRepositoriesEvent.UnhiddenAll) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + _events.send(HiddenRepositoriesEvent.Failure(e.message.orEmpty())) + } + } + } +} From c5f4bb55cb98a569dfdcc44c93845c2dd1a48e1b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:55:58 +0500 Subject: [PATCH 10/12] feat(nav): wire HiddenRepositoriesScreen route + DI --- .../rainxch/githubstore/app/di/ViewModelsModule.kt | 2 ++ .../githubstore/app/navigation/AppNavigation.kt | 12 ++++++++++++ .../githubstore/app/navigation/GithubStoreGraph.kt | 3 +++ 3 files changed, 17 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt index ce5292707..2a6f6ab7b 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/ViewModelsModule.kt @@ -19,6 +19,7 @@ import zed.rainxch.search.presentation.SearchViewModel import zed.rainxch.starred.presentation.StarredReposViewModel import zed.rainxch.tweaks.presentation.TweaksViewModel import zed.rainxch.tweaks.presentation.feedback.FeedbackViewModel +import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesViewModel import zed.rainxch.tweaks.presentation.mirror.AutoSuggestMirrorViewModel import zed.rainxch.tweaks.presentation.mirror.MirrorPickerViewModel import zed.rainxch.tweaks.presentation.skipped.SkippedUpdatesViewModel @@ -76,6 +77,7 @@ val viewModelsModule = viewModelOf(::StarredPickerViewModel) viewModelOf(::AutoSuggestMirrorViewModel) viewModelOf(::SkippedUpdatesViewModel) + viewModelOf(::HiddenRepositoriesViewModel) viewModelOf(::WhatsNewViewModel) viewModelOf(::AnnouncementsViewModel) viewModel { diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt index 297b931b0..eb6ef30bb 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt @@ -46,6 +46,7 @@ import zed.rainxch.recentlyviewed.presentation.RecentlyViewedRoot import zed.rainxch.search.presentation.SearchRoot import zed.rainxch.starred.presentation.StarredReposRoot import zed.rainxch.tweaks.presentation.TweaksRoot +import zed.rainxch.tweaks.presentation.hidden.HiddenRepositoriesRoot import zed.rainxch.tweaks.presentation.mirror.AutoSuggestMirrorViewModel import zed.rainxch.tweaks.presentation.mirror.MirrorPickerRoot import zed.rainxch.tweaks.presentation.mirror.components.AutoSuggestMirrorSheet @@ -382,6 +383,11 @@ fun AppNavigation( launchSingleTop = true } }, + onNavigateToHiddenRepositories = { + navController.navigate(GithubStoreGraph.HiddenRepositoriesScreen) { + launchSingleTop = true + } + }, ) } @@ -391,6 +397,12 @@ fun AppNavigation( ) } + composable { + HiddenRepositoriesRoot( + onNavigateBack = { navController.popBackStack() }, + ) + } + composable { backStackEntry -> // Pick up the "open link sheet" flag set by ExternalImportRoot's // "Add manually" path. We consume the flag once on entry so a diff --git a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt index 4ca177f4e..ebb524d60 100644 --- a/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt +++ b/composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/GithubStoreGraph.kt @@ -56,6 +56,9 @@ sealed interface GithubStoreGraph { @Serializable data object SkippedUpdatesScreen : GithubStoreGraph + @Serializable + data object HiddenRepositoriesScreen : GithubStoreGraph + @Serializable data object WhatsNewHistoryScreen : GithubStoreGraph From 2f31c8b3c106e5e7187dd71fc14e388dd3cf9535 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 21:56:06 +0500 Subject: [PATCH 11/12] chore(strings): hidden repositories management UI 13 locales --- .../composeResources/values-ar/strings-ar.xml | 10 ++++++++++ .../composeResources/values-bn/strings-bn.xml | 10 ++++++++++ .../composeResources/values-es/strings-es.xml | 10 ++++++++++ .../composeResources/values-fr/strings-fr.xml | 10 ++++++++++ .../composeResources/values-hi/strings-hi.xml | 10 ++++++++++ .../composeResources/values-it/strings-it.xml | 10 ++++++++++ .../composeResources/values-ja/strings-ja.xml | 10 ++++++++++ .../composeResources/values-ko/strings-ko.xml | 10 ++++++++++ .../composeResources/values-pl/strings-pl.xml | 10 ++++++++++ .../composeResources/values-ru/strings-ru.xml | 10 ++++++++++ .../composeResources/values-tr/strings-tr.xml | 10 ++++++++++ .../composeResources/values-zh-rCN/strings-zh-rCN.xml | 10 ++++++++++ .../src/commonMain/composeResources/values/strings.xml | 10 ++++++++++ 13 files changed, 130 insertions(+) diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 847da2ca5..75b34f143 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1152,4 +1152,14 @@ إلغاء التخطّي تم تفعيل الإشعار لـ %1$s تعذّر تحديث التفضيل + المستودعات المخفية + إدارة المستودعات التي أخفيتها من الرئيسية والبحث. + %1$d مخفي + لا توجد مستودعات مخفية + اضغط مطولاً على بطاقة مستودع في الرئيسية أو البحث لإخفائه من الاستكشاف. + إظهار + إظهار الكل + تم إظهار %1$s + تم إظهار جميع المستودعات + تعذر الإظهار. حاول مرة أخرى. diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index 588ff15cd..d0802e8a1 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1128,4 +1128,14 @@ এড়িয়ে যাবেন না %1$s এর জন্য আপডেট প্রম্পট আবার চালু পছন্দ আপডেট করা যায়নি + লুকানো রিপোজিটরি + হোম ও সার্চ থেকে লুকানো রিপোজিটরি পরিচালনা করুন। + %1$d লুকানো + কোনো লুকানো রিপোজিটরি নেই + হোম বা সার্চে রিপোজিটরি কার্ডে দীর্ঘক্ষণ চাপ দিয়ে আবিষ্কার থেকে লুকান। + দেখান + সব দেখান + %1$s এখন দৃশ্যমান + সব রিপোজিটরি দৃশ্যমান + দৃশ্যমান করা যায়নি। আবার চেষ্টা করুন। diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 4012a74e7..715951b41 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1100,4 +1100,14 @@ No saltar Aviso de actualización reactivado para %1$s No se pudo actualizar la preferencia + Repositorios ocultos + Gestiona los repositorios que ocultaste de Inicio y Búsqueda. + %1$d oculto(s) + Sin repositorios ocultos + Mantén pulsada una tarjeta de repositorio en Inicio o Búsqueda para ocultarla. + Mostrar + Mostrar todos + %1$s vuelto a mostrar + Todos los repositorios mostrados + No se pudo mostrar. Inténtalo de nuevo. diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index fbb70b279..dbbba2aa5 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1101,4 +1101,14 @@ Ne plus ignorer Avis réactivé pour %1$s Impossible de mettre à jour la préférence + Dépôts masqués + Gérez les dépôts masqués depuis Accueil et Recherche. + %1$d masqué(s) + Aucun dépôt masqué + Appuyez longuement sur une carte de dépôt dans Accueil ou Recherche pour la masquer. + Afficher + Tout afficher + %1$s réaffiché + Tous les dépôts réaffichés + Impossible d\'afficher. Réessayez. diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index 408e4c887..ac9b30b45 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1139,4 +1139,14 @@ मत छोड़ें %1$s के लिए अपडेट प्रॉम्प्ट चालू प्राथमिकता अपडेट नहीं हो सकी + छिपे हुए रिपॉजिटरी + होम और सर्च से छिपाए गए रिपॉजिटरी प्रबंधित करें। + %1$d छिपे हुए + कोई छिपा रिपॉजिटरी नहीं + होम या सर्च में रेपो कार्ड पर लंबा दबाकर इसे डिस्कवरी से छिपाएं। + दिखाएं + सभी दिखाएं + %1$s फिर से दृश्यमान + सभी रिपॉजिटरी अब दृश्यमान + दिखाया नहीं जा सका। पुनः प्रयास करें। diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index 12f60186c..b8ddf8e0a 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1140,4 +1140,14 @@ Non ignorare Avviso riattivato per %1$s Impossibile aggiornare la preferenza + Repository nascosti + Gestisci i repository nascosti da Home e Ricerca. + %1$d nascosti + Nessun repository nascosto + Tieni premuta una scheda repository in Home o Ricerca per nasconderla. + Mostra + Mostra tutti + %1$s di nuovo visibile + Tutti i repository di nuovo visibili + Impossibile mostrare. Riprova. diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 6b767afb6..71cd0af8d 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1095,4 +1095,14 @@ スキップしない %1$s のアップデート通知を再び有効にしました 設定を更新できませんでした + 非表示のリポジトリ + ホームと検索で非表示にしたリポジトリを管理。 + %1$d 件非表示 + 非表示のリポジトリはありません + ホームや検索でリポジトリカードを長押しすると検索結果から非表示にできます。 + 表示する + すべて表示 + %1$s を再表示しました + すべてのリポジトリを再表示しました + 再表示できませんでした。もう一度お試しください。 diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index d039ae381..4dc5e2723 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1130,4 +1130,14 @@ 건너뛰지 않기 %1$s에 대한 업데이트 알림 다시 사용 설정 설정을 업데이트할 수 없습니다 + 숨긴 저장소 + 홈과 검색에서 숨긴 저장소를 관리합니다. + %1$d개 숨김 + 숨긴 저장소 없음 + 홈 또는 검색에서 저장소 카드를 길게 눌러 탐색에서 숨길 수 있습니다. + 표시 + 모두 표시 + %1$s 다시 표시됨 + 모든 저장소가 다시 표시됨 + 표시할 수 없습니다. 다시 시도하세요. diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index e702c784b..1c00bf80d 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1121,4 +1121,14 @@ Nie pomijaj Powiadomienie włączone dla %1$s Nie udało się zmienić preferencji + Ukryte repozytoria + Zarządzaj repozytoriami ukrytymi na Ekranie głównym i w Wyszukiwarce. + %1$d ukryte + Brak ukrytych repozytoriów + Przytrzymaj kartę repozytorium na Ekranie głównym lub w Wyszukiwarce, aby ukryć ją z odkrywania. + Pokaż + Pokaż wszystkie + %1$s znów widoczne + Wszystkie repozytoria znów widoczne + Nie udało się pokazać. Spróbuj ponownie. diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index a4605f813..c055893f4 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1121,4 +1121,14 @@ Не пропускать Уведомление снова включено для %1$s Не удалось обновить настройку + Скрытые репозитории + Управление репозиториями, скрытыми с Главной и Поиска. + %1$d скрыто + Нет скрытых репозиториев + Удерживайте карточку репозитория на Главной или в Поиске, чтобы скрыть её из обзора. + Показать + Показать все + %1$s снова виден + Все репозитории снова видны + Не удалось показать. Повторите попытку. diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index e039206e1..30c94ac6d 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1137,4 +1137,14 @@ Atlamayı kaldır %1$s için güncelleme bildirimi yeniden açıldı Tercih güncellenemedi + Gizli depolar + Ana ekran ve Arama\'da gizlediğin depoları yönet. + %1$d gizli + Gizli depo yok + Ana ekran veya Arama\'da bir depo kartına basılı tutarak keşiften gizleyin. + Göster + Tümünü göster + %1$s yeniden görünür + Tüm depolar yeniden görünür + Gösterilemedi. Tekrar dene. diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index c9854d0e5..02ad2d321 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1097,4 +1097,14 @@ 不要跳过 已为 %1$s 重新启用更新提示 无法更新偏好设置 + 已隐藏的仓库 + 管理从首页和搜索中隐藏的仓库。 + 已隐藏 %1$d 个 + 没有隐藏的仓库 + 在首页或搜索中长按仓库卡片即可从发现中隐藏。 + 取消隐藏 + 全部取消隐藏 + %1$s 已重新显示 + 所有仓库已重新显示 + 无法取消隐藏。请重试。 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index f2ab0cdcb..4ac11c4db 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -774,6 +774,16 @@ Hide repository Repository hidden Undo + Hidden repositories + Manage repositories you hid from Home and Search. + %1$d hidden + No hidden repositories + Long-press a repository card on Home or Search to hide it from discovery. + Unhide + Unhide all + %1$s unhidden + All repositories unhidden + Could not unhide. Try again. Privacy From 9bf22258f50b4a14780283700deb1f2092455a2d Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Tue, 12 May 2026 22:11:44 +0500 Subject: [PATCH 12/12] =?UTF-8?q?feat(presentation):=20contextual=20action?= =?UTF-8?q?s=20bottom=20sheet=20=E2=80=94=20share,=20open,=20seen,=20hide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/SeenReposRepositoryImpl.kt | 34 +++- .../domain/repository/SeenReposRepository.kt | 15 ++ .../composeResources/values-ar/strings-ar.xml | 3 + .../composeResources/values-bn/strings-bn.xml | 3 + .../composeResources/values-es/strings-es.xml | 3 + .../composeResources/values-fr/strings-fr.xml | 3 + .../composeResources/values-hi/strings-hi.xml | 3 + .../composeResources/values-it/strings-it.xml | 3 + .../composeResources/values-ja/strings-ja.xml | 3 + .../composeResources/values-ko/strings-ko.xml | 3 + .../composeResources/values-pl/strings-pl.xml | 3 + .../composeResources/values-ru/strings-ru.xml | 3 + .../composeResources/values-tr/strings-tr.xml | 3 + .../values-zh-rCN/strings-zh-rCN.xml | 3 + .../composeResources/values/strings.xml | 3 + .../presentation/components/RepositoryCard.kt | 175 ++++++++++++++---- .../rainxch/home/presentation/HomeAction.kt | 8 + .../zed/rainxch/home/presentation/HomeRoot.kt | 7 + .../home/presentation/HomeViewModel.kt | 33 ++++ .../search/presentation/SearchAction.kt | 8 + .../rainxch/search/presentation/SearchRoot.kt | 7 + .../search/presentation/SearchViewModel.kt | 33 ++++ 22 files changed, 314 insertions(+), 45 deletions(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SeenReposRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SeenReposRepositoryImpl.kt index 6314e048f..f214bf6cc 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SeenReposRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/SeenReposRepositoryImpl.kt @@ -20,15 +20,35 @@ class SeenReposRepositoryImpl( } override suspend fun markAsSeen(repo: GithubRepoSummary) { + markAsSeen( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + repoDescription = repo.description, + primaryLanguage = repo.language, + repoUrl = repo.htmlUrl, + ) + } + + override suspend fun markAsSeen( + repoId: Long, + repoName: String, + repoOwner: String, + repoOwnerAvatarUrl: String, + repoDescription: String?, + primaryLanguage: String?, + repoUrl: String, + ) { seenRepoDao.insert( SeenRepoEntity( - repoId = repo.id, - repoName = repo.name, - repoOwner = repo.owner.login, - repoOwnerAvatarUrl = repo.owner.avatarUrl, - repoDescription = repo.description, - primaryLanguage = repo.language, - repoUrl = repo.htmlUrl, + repoId = repoId, + repoName = repoName, + repoOwner = repoOwner, + repoOwnerAvatarUrl = repoOwnerAvatarUrl, + repoDescription = repoDescription, + primaryLanguage = primaryLanguage, + repoUrl = repoUrl, seenAt = System.currentTimeMillis(), ), ) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt index 82a038c63..d8f791c6e 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/SeenReposRepository.kt @@ -11,6 +11,21 @@ interface SeenReposRepository { suspend fun markAsSeen(repo: GithubRepoSummary) + /** + * Primitive-arg overload for surfaces that only hold the UI model + * (Home / Search cards) and would otherwise have to reconstitute a + * full [GithubRepoSummary] just to mark a repo seen. + */ + suspend fun markAsSeen( + repoId: Long, + repoName: String, + repoOwner: String, + repoOwnerAvatarUrl: String, + repoDescription: String?, + primaryLanguage: String?, + repoUrl: String, + ) + suspend fun removeFromHistory(repoId: Long) suspend fun clearAll() diff --git a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml index 75b34f143..55b12c82d 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -1162,4 +1162,7 @@ تم إظهار %1$s تم إظهار جميع المستودعات تعذر الإظهار. حاول مرة أخرى. + فتح في GitHub + وضع علامة مشاهد + إلغاء علامة مشاهد diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml index d0802e8a1..9a7bfc34b 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -1138,4 +1138,7 @@ %1$s এখন দৃশ্যমান সব রিপোজিটরি দৃশ্যমান দৃশ্যমান করা যায়নি। আবার চেষ্টা করুন। + GitHub-এ খুলুন + দেখা হিসেবে চিহ্নিত করুন + অদেখা হিসেবে চিহ্নিত করুন diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml index 715951b41..43cfd3fdb 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -1110,4 +1110,7 @@ %1$s vuelto a mostrar Todos los repositorios mostrados No se pudo mostrar. Inténtalo de nuevo. + Abrir en GitHub + Marcar como visto + Marcar como no visto diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml index dbbba2aa5..7d41f1c31 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -1111,4 +1111,7 @@ %1$s réaffiché Tous les dépôts réaffichés Impossible d\'afficher. Réessayez. + Ouvrir sur GitHub + Marquer comme vu + Marquer comme non vu diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml index ac9b30b45..b84d84048 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -1149,4 +1149,7 @@ %1$s फिर से दृश्यमान सभी रिपॉजिटरी अब दृश्यमान दिखाया नहीं जा सका। पुनः प्रयास करें। + GitHub पर खोलें + देखा हुआ चिह्नित करें + अनदेखा चिह्नित करें diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml index b8ddf8e0a..013cc6899 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -1150,4 +1150,7 @@ %1$s di nuovo visibile Tutti i repository di nuovo visibili Impossibile mostrare. Riprova. + Apri su GitHub + Segna come visto + Segna come non visto diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml index 71cd0af8d..4b01ea662 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -1105,4 +1105,7 @@ %1$s を再表示しました すべてのリポジトリを再表示しました 再表示できませんでした。もう一度お試しください。 + GitHub で開く + 既読にする + 未読にする diff --git a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml index 4dc5e2723..11a42a4e1 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -1140,4 +1140,7 @@ %1$s 다시 표시됨 모든 저장소가 다시 표시됨 표시할 수 없습니다. 다시 시도하세요. + GitHub에서 열기 + 본 항목으로 표시 + 본 항목 해제 diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml index 1c00bf80d..2fbfce06d 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -1131,4 +1131,7 @@ %1$s znów widoczne Wszystkie repozytoria znów widoczne Nie udało się pokazać. Spróbuj ponownie. + Otwórz w GitHub + Oznacz jako obejrzane + Oznacz jako nieobejrzane diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml index c055893f4..825cbff54 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -1131,4 +1131,7 @@ %1$s снова виден Все репозитории снова видны Не удалось показать. Повторите попытку. + Открыть на GitHub + Отметить как просмотренный + Снять отметку diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml index 30c94ac6d..1ed48e73c 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -1147,4 +1147,7 @@ %1$s yeniden görünür Tüm depolar yeniden görünür Gösterilemedi. Tekrar dene. + GitHub\'da aç + Görüldü olarak işaretle + Görülmedi olarak işaretle diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml index 02ad2d321..f6c082f1e 100644 --- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml +++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml @@ -1107,4 +1107,7 @@ %1$s 已重新显示 所有仓库已重新显示 无法取消隐藏。请重试。 + 在 GitHub 打开 + 标记为已查看 + 取消已查看标记 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 4ac11c4db..7b19efea3 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -772,6 +772,9 @@ Viewed You own this repo Hide repository + Open on GitHub + Mark as viewed + Mark as unviewed Repository hidden Undo Hidden repositories diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt index b2e71cb52..e39169bc4 100644 --- a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt +++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/RepositoryCard.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Verified import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.outlined.Code @@ -31,6 +32,7 @@ import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults @@ -70,6 +72,9 @@ import zed.rainxch.core.presentation.utils.toIcons import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.forked_repository import zed.rainxch.githubstore.core.presentation.res.hide_repository +import zed.rainxch.githubstore.core.presentation.res.mark_as_unviewed +import zed.rainxch.githubstore.core.presentation.res.mark_as_viewed +import zed.rainxch.githubstore.core.presentation.res.open_on_github import zed.rainxch.githubstore.core.presentation.res.home_view_details import zed.rainxch.githubstore.core.presentation.res.installed import zed.rainxch.githubstore.core.presentation.res.open_in_browser @@ -91,6 +96,7 @@ fun RepositoryCard( onDeveloperClick: (String) -> Unit, modifier: Modifier = Modifier, onHideClick: (() -> Unit)? = null, + onToggleSeen: (() -> Unit)? = null, ) { val uriHandler = LocalUriHandler.current @@ -100,11 +106,16 @@ fun RepositoryCard( label = "seen_content_alpha", ) - var showHideSheet by remember { mutableStateOf(false) } + var showActionsSheet by remember { mutableStateOf(false) } + val sheetEnabled = onHideClick != null ExpressiveCard( onClick = onClick, - onLongClick = onHideClick?.let { { showHideSheet = true } }, + onLongClick = if (sheetEnabled) { + { showActionsSheet = true } + } else { + null + }, modifier = modifier, ) { Box(modifier = Modifier.alpha(contentAlpha)) { @@ -357,13 +368,30 @@ fun RepositoryCard( } } - if (onHideClick != null && showHideSheet) { - HideRepositoryBottomSheet( - repoName = discoveryRepositoryUi.repository.fullName, - onDismiss = { showHideSheet = false }, - onConfirmHide = { - showHideSheet = false - onHideClick() + if (sheetEnabled && showActionsSheet) { + RepositoryActionsBottomSheet( + repository = discoveryRepositoryUi.repository, + isSeen = discoveryRepositoryUi.isSeen, + onDismiss = { showActionsSheet = false }, + onShare = { + showActionsSheet = false + onShareClick() + }, + onOpenOnGithub = { + showActionsSheet = false + uriHandler.openUri(discoveryRepositoryUi.repository.htmlUrl) + }, + onToggleSeen = onToggleSeen?.let { + { + showActionsSheet = false + it() + } + }, + onHide = onHideClick?.let { + { + showActionsSheet = false + it() + } }, ) } @@ -371,10 +399,14 @@ fun RepositoryCard( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun HideRepositoryBottomSheet( - repoName: String, +private fun RepositoryActionsBottomSheet( + repository: GithubRepoSummaryUi, + isSeen: Boolean, onDismiss: () -> Unit, - onConfirmHide: () -> Unit, + onShare: () -> Unit, + onOpenOnGithub: () -> Unit, + onToggleSeen: (() -> Unit)?, + onHide: (() -> Unit)?, ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( @@ -382,43 +414,110 @@ private fun HideRepositoryBottomSheet( sheetState = sheetState, containerColor = MaterialTheme.colorScheme.surfaceContainerLow, ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), - ) { - Text( - text = repoName, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, + Column(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp)) { + // Context header so the user can verify which repo they're + // acting on without the card behind the sheet. + Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + GitHubStoreImage( + imageModel = { repository.owner.avatarUrl }, + modifier = + Modifier + .size(36.dp) + .clip(CircleShape), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = repository.fullName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + repository.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 16.dp), ) - ListItem( - headlineContent = { - Text(stringResource(Res.string.hide_repository)) - }, - leadingContent = { - Icon( - imageVector = Icons.Default.VisibilityOff, - contentDescription = null, - ) - }, - colors = - ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), - modifier = Modifier.clickable(onClick = onConfirmHide), + SheetActionRow( + label = stringResource(Res.string.share_repository), + icon = Icons.Default.Share, + onClick = onShare, + ) + SheetActionRow( + label = stringResource(Res.string.open_on_github), + icon = Icons.Default.OpenInBrowser, + onClick = onOpenOnGithub, ) + if (onToggleSeen != null) { + SheetActionRow( + label = + if (isSeen) { + stringResource(Res.string.mark_as_unviewed) + } else { + stringResource(Res.string.mark_as_viewed) + }, + icon = Icons.Outlined.Visibility, + onClick = onToggleSeen, + ) + } + if (onHide != null) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + SheetActionRow( + label = stringResource(Res.string.hide_repository), + icon = Icons.Default.VisibilityOff, + onClick = onHide, + tint = MaterialTheme.colorScheme.error, + ) + } } } } +@Composable +private fun SheetActionRow( + label: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + onClick: () -> Unit, + tint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface, +) { + ListItem( + headlineContent = { + Text(text = label, color = tint) + }, + leadingContent = { + Icon(imageVector = icon, contentDescription = null, tint = tint) + }, + colors = + ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ), + modifier = Modifier.clickable(onClick = onClick), + ) +} + @Composable fun PlatformChip( platform: DiscoveryPlatform, diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt index 0880cd746..c415a27b4 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeAction.kt @@ -53,4 +53,12 @@ sealed interface HomeAction { data class OnUndoHideRepository( val repoId: Long, ) : HomeAction + + data class OnMarkAsSeen( + val repo: GithubRepoSummaryUi, + ) : HomeAction + + data class OnMarkAsUnseen( + val repoId: Long, + ) : HomeAction } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt index 88328e44c..ec3001f98 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeRoot.kt @@ -457,6 +457,13 @@ private fun MainState( onHideClick = { onAction(HomeAction.OnHideRepository(discoveryRepository.repository)) }, + onToggleSeen = { + if (discoveryRepository.isSeen) { + onAction(HomeAction.OnMarkAsUnseen(discoveryRepository.repository.id)) + } else { + onAction(HomeAction.OnMarkAsSeen(discoveryRepository.repository)) + } + }, modifier = Modifier.animateItem(), ) } diff --git a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt index eade03abf..70d5621fe 100644 --- a/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt +++ b/feature/home/presentation/src/commonMain/kotlin/zed/rainxch/home/presentation/HomeViewModel.kt @@ -553,6 +553,39 @@ class HomeViewModel( } } + is HomeAction.OnMarkAsSeen -> { + val repo = action.repo + viewModelScope.launch { + try { + seenReposRepository.markAsSeen( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + repoDescription = repo.description, + primaryLanguage = repo.language, + repoUrl = repo.htmlUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as seen failed for ${repo.id}: ${e.message}") + } + } + } + + is HomeAction.OnMarkAsUnseen -> { + viewModelScope.launch { + try { + seenReposRepository.removeFromHistory(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as unseen failed for ${action.repoId}: ${e.message}") + } + } + } + HomeAction.OnSearchClick -> { // Handled in composable } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt index db3596a50..2e62a2baa 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt @@ -83,4 +83,12 @@ sealed interface SearchAction { data class OnUndoHideRepository( val repoId: Long, ) : SearchAction + + data class OnMarkAsSeen( + val repo: GithubRepoSummaryUi, + ) : SearchAction + + data class OnMarkAsUnseen( + val repoId: Long, + ) : SearchAction } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 677679c66..80ec4e16d 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -695,6 +695,13 @@ fun SearchScreen( onHideClick = { onAction(SearchAction.OnHideRepository(discoveryRepository.repository)) }, + onToggleSeen = { + if (discoveryRepository.isSeen) { + onAction(SearchAction.OnMarkAsUnseen(discoveryRepository.repository.id)) + } else { + onAction(SearchAction.OnMarkAsSeen(discoveryRepository.repository)) + } + }, modifier = Modifier.animateItem(), ) } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 5d7169133..37139b9f4 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -773,6 +773,39 @@ class SearchViewModel( } } } + + is SearchAction.OnMarkAsSeen -> { + val repo = action.repo + viewModelScope.launch { + try { + seenReposRepository.markAsSeen( + repoId = repo.id, + repoName = repo.name, + repoOwner = repo.owner.login, + repoOwnerAvatarUrl = repo.owner.avatarUrl, + repoDescription = repo.description, + primaryLanguage = repo.language, + repoUrl = repo.htmlUrl, + ) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as seen failed for ${repo.id}: ${e.message}") + } + } + } + + is SearchAction.OnMarkAsUnseen -> { + viewModelScope.launch { + try { + seenReposRepository.removeFromHistory(action.repoId) + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + logger.warn("Mark as unseen failed for ${action.repoId}: ${e.message}") + } + } + } } }