diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 10f6bbe0..7dfef775 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -69,6 +69,8 @@ kotlin { implementation(libs.liquid) implementation(libs.jetbrains.compose.material.icons.extended) + implementation(libs.touchlab.kermit) + implementation(compose.runtime) implementation(compose.foundation) implementation(libs.jetbrains.compose.material3) diff --git a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt index e2c9d8ca..a8b33d47 100644 --- a/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt +++ b/composeApp/src/androidMain/kotlin/zed/rainxch/githubstore/app/GithubStoreApp.kt @@ -4,16 +4,26 @@ import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.os.Build +import co.touchlab.kermit.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import zed.rainxch.core.data.services.PackageEventReceiver import zed.rainxch.core.data.services.UpdateScheduler +import zed.rainxch.core.domain.model.InstallSource +import zed.rainxch.core.domain.model.InstalledApp import zed.rainxch.core.domain.repository.InstalledAppsRepository +import zed.rainxch.core.domain.repository.ThemesRepository import zed.rainxch.core.domain.system.PackageMonitor import zed.rainxch.githubstore.app.di.initKoin class GithubStoreApp : Application() { private var packageEventReceiver: PackageEventReceiver? = null + private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onCreate() { super.onCreate() @@ -25,6 +35,7 @@ class GithubStoreApp : Application() { createNotificationChannels() registerPackageEventReceiver() scheduleBackgroundUpdateChecks() + registerSelfAsInstalledApp() } private fun createNotificationChannels() { @@ -70,10 +81,86 @@ class GithubStoreApp : Application() { } private fun scheduleBackgroundUpdateChecks() { - UpdateScheduler.schedule(context = this) + appScope.launch { + try { + val intervalHours = get().getUpdateCheckInterval().first() + UpdateScheduler.schedule( + context = this@GithubStoreApp, + intervalHours = intervalHours, + ) + } catch (e: Exception) { + Logger.e(e) { "Failed to schedule background update checks" } + } + } + } + + private fun registerSelfAsInstalledApp() { + appScope.launch { + try { + val repo = get() + val selfPackageName = packageName + val existing = repo.getAppByPackage(selfPackageName) + + if (existing != null) return@launch + + val packageMonitor = get() + val systemInfo = packageMonitor.getInstalledPackageInfo(selfPackageName) + if (systemInfo == null) { + Logger.w { "GithubStoreApp: Skip self-registration, package info missing for $selfPackageName" } + return@launch + } + + val now = System.currentTimeMillis() + val versionName = systemInfo.versionName + val versionCode = systemInfo.versionCode + + val selfApp = + InstalledApp( + packageName = selfPackageName, + repoId = SELF_REPO_ID, + repoName = SELF_REPO_NAME, + repoOwner = SELF_REPO_OWNER, + repoOwnerAvatarUrl = SELF_AVATAR_URL, + repoDescription = "A cross-platform app store for GitHub releases", + primaryLanguage = "Kotlin", + repoUrl = "https://github.com/$SELF_REPO_OWNER/$SELF_REPO_NAME", + installedVersion = versionName, + installedAssetName = null, + installedAssetUrl = null, + latestVersion = null, + latestAssetName = null, + latestAssetUrl = null, + latestAssetSize = null, + appName = "GitHub Store", + installSource = InstallSource.THIS_APP, + installedAt = now, + lastCheckedAt = 0L, + lastUpdatedAt = now, + isUpdateAvailable = false, + updateCheckEnabled = true, + releaseNotes = null, + systemArchitecture = "", + fileExtension = "apk", + isPendingInstall = false, + installedVersionName = versionName, + installedVersionCode = versionCode, + ) + + repo.saveInstalledApp(selfApp) + Logger.i("GitHub Store App: App added") + } catch (e: Exception) { + Logger.e(e) { "GitHub Store App: Failed to register self as installed app" } + } + } } companion object { + private const val SELF_REPO_ID = 1101281251L + private const val SELF_REPO_OWNER = "OpenHub-Store" + private const val SELF_REPO_NAME = "GitHub-Store" + private const val SELF_AVATAR_URL = + @Suppress("ktlint:standard:max-line-length") + "https://raw.githubusercontent.com/OpenHub-Store/GitHub-Store/refs/heads/main/media-resources/app_icon.png" const val UPDATES_CHANNEL_ID = "app_updates" const val UPDATE_SERVICE_CHANNEL_ID = "update_service" } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt index edc1985a..ba00fa8f 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/di/PlatformModule.android.kt @@ -24,9 +24,11 @@ import zed.rainxch.core.data.utils.AndroidBrowserHelper import zed.rainxch.core.data.utils.AndroidClipboardHelper import zed.rainxch.core.data.utils.AndroidShareManager import zed.rainxch.core.domain.network.Downloader +import zed.rainxch.core.data.services.AndroidUpdateScheduleManager import zed.rainxch.core.domain.system.Installer import zed.rainxch.core.domain.system.InstallerStatusProvider import zed.rainxch.core.domain.system.PackageMonitor +import zed.rainxch.core.domain.system.UpdateScheduleManager import zed.rainxch.core.domain.utils.AppLauncher import zed.rainxch.core.domain.utils.BrowserHelper import zed.rainxch.core.domain.utils.ClipboardHelper @@ -122,4 +124,10 @@ actual val corePlatformModule = module { ) } + single { + AndroidUpdateScheduleManager( + context = androidContext() + ) + } + } diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidUpdateScheduleManager.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidUpdateScheduleManager.kt new file mode 100644 index 00000000..17a61497 --- /dev/null +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidUpdateScheduleManager.kt @@ -0,0 +1,12 @@ +package zed.rainxch.core.data.services + +import android.content.Context +import zed.rainxch.core.domain.system.UpdateScheduleManager + +class AndroidUpdateScheduleManager( + private val context: Context, +) : UpdateScheduleManager { + override fun reschedule(intervalHours: Long) { + UpdateScheduler.reschedule(context, intervalHours) + } +} diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt index e7603468..8895e55e 100644 --- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt +++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/UpdateScheduler.kt @@ -68,6 +68,43 @@ object UpdateScheduler { Logger.i { "UpdateScheduler: Scheduled periodic update check every ${intervalHours}h + immediate check" } } + /** + * Force-reschedules the periodic update check with a new interval. + * Uses UPDATE policy to replace the existing schedule immediately. + * Call this when the user changes the update check interval in settings. + */ + fun reschedule( + context: Context, + intervalHours: Long, + ) { + val constraints = + Constraints + .Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = + PeriodicWorkRequestBuilder( + repeatInterval = intervalHours, + repeatIntervalTimeUnit = TimeUnit.HOURS, + ).setConstraints(constraints) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + 30, + TimeUnit.MINUTES, + ).build() + + WorkManager + .getInstance(context) + .enqueueUniquePeriodicWork( + uniqueWorkName = UpdateCheckWorker.WORK_NAME, + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE, + request = request, + ) + + Logger.i { "UpdateScheduler: Rescheduled periodic update check to every ${intervalHours}h" } + } + /** * Enqueues a one-time [AutoUpdateWorker] to download and silently install * all available updates via Shizuku. Uses KEEP policy to avoid duplicate runs. diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt index 8b7aa481..68eb2d64 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ThemesRepositoryImpl.kt @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -22,6 +23,7 @@ class ThemesRepositoryImpl( private val AUTO_DETECT_CLIPBOARD_KEY = booleanPreferencesKey("auto_detect_clipboard_links") private val INSTALLER_TYPE_KEY = stringPreferencesKey("installer_type") private val AUTO_UPDATE_KEY = booleanPreferencesKey("auto_update_enabled") + private val UPDATE_CHECK_INTERVAL_KEY = longPreferencesKey("update_check_interval_hours") override fun getThemeColor(): Flow = preferences.data.map { prefs -> @@ -107,4 +109,19 @@ class ThemesRepositoryImpl( prefs[AUTO_UPDATE_KEY] = enabled } } + + override fun getUpdateCheckInterval(): Flow = + preferences.data.map { prefs -> + prefs[UPDATE_CHECK_INTERVAL_KEY] ?: DEFAULT_UPDATE_CHECK_INTERVAL_HOURS + } + + override suspend fun setUpdateCheckInterval(hours: Long) { + preferences.edit { prefs -> + prefs[UPDATE_CHECK_INTERVAL_KEY] = hours + } + } + + companion object { + const val DEFAULT_UPDATE_CHECK_INTERVAL_HOURS = 6L + } } diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt index 07693ba2..25195211 100644 --- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/di/PlatformModule.jvm.kt @@ -15,9 +15,11 @@ import zed.rainxch.core.data.services.DesktopFileLocationsProvider import zed.rainxch.core.data.services.DesktopInstaller import zed.rainxch.core.data.services.DesktopLocalizationManager import zed.rainxch.core.data.services.DesktopPackageMonitor +import zed.rainxch.core.data.services.DesktopUpdateScheduleManager import zed.rainxch.core.data.services.FileLocationsProvider import zed.rainxch.core.domain.system.Installer import zed.rainxch.core.domain.system.InstallerStatusProvider +import zed.rainxch.core.domain.system.UpdateScheduleManager import zed.rainxch.core.data.services.LocalizationManager import zed.rainxch.core.data.services.DesktopInstallerStatusProvider import zed.rainxch.core.data.utils.DesktopShareManager @@ -92,4 +94,8 @@ actual val corePlatformModule = module { single { DesktopInstallerStatusProvider() } + + single { + DesktopUpdateScheduleManager() + } } \ No newline at end of file diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt new file mode 100644 index 00000000..a0989f7f --- /dev/null +++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopUpdateScheduleManager.kt @@ -0,0 +1,12 @@ +package zed.rainxch.core.data.services + +import zed.rainxch.core.domain.system.UpdateScheduleManager + +/** + * No-op implementation for Desktop — WorkManager is Android-only. + */ +class DesktopUpdateScheduleManager : UpdateScheduleManager { + override fun reschedule(intervalHours: Long) { + // No background scheduler on Desktop + } +} diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt index 92eaab94..aad26498 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ThemesRepository.kt @@ -20,4 +20,6 @@ interface ThemesRepository { suspend fun setInstallerType(type: InstallerType) fun getAutoUpdateEnabled(): Flow suspend fun setAutoUpdateEnabled(enabled: Boolean) + fun getUpdateCheckInterval(): Flow + suspend fun setUpdateCheckInterval(hours: Long) } \ No newline at end of file diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt new file mode 100644 index 00000000..135fe13c --- /dev/null +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/UpdateScheduleManager.kt @@ -0,0 +1,13 @@ +package zed.rainxch.core.domain.system + +/** + * Abstraction for rescheduling background update checks. + * Android implementation delegates to WorkManager; Desktop is a no-op. + */ +interface UpdateScheduleManager { + /** + * Reschedules the periodic update check with a new interval. + * Takes effect immediately (replaces existing schedule). + */ + fun reschedule(intervalHours: Long) +} 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 5176e5b3..0312f8b9 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -521,4 +521,11 @@ تحديث التطبيقات تلقائيًا تنزيل التحديثات وتثبيتها تلقائيًا في الخلفية عبر Shizuku + التحديثات + فترة التحقق من التحديثات + عدد مرات التحقق من تحديثات التطبيق في الخلفية + ٣ ساعات + ٦ ساعات + ١٢ ساعة + ٢٤ ساعة 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 e7c474ff..c0166d85 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -520,4 +520,11 @@ স্বয়ংক্রিয়ভাবে অ্যাপ আপডেট করুন Shizuku এর মাধ্যমে ব্যাকগ্রাউন্ডে স্বয়ংক্রিয়ভাবে আপডেট ডাউনলোড এবং ইনস্টল করুন + আপডেট + আপডেট চেক করার ব্যবধান + ব্যাকগ্রাউন্ডে কতক্ষণ পর পর অ্যাপ আপডেট খোঁজা হবে + ৩ঘ + ৬ঘ + ১২ঘ + ২৪ঘ 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 1e1df512..618067e8 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -481,4 +481,11 @@ Actualizar apps automáticamente Descargar e instalar actualizaciones en segundo plano a través de Shizuku + Actualizaciones + Intervalo de verificación + Con qué frecuencia buscar actualizaciones en segundo plano + 3h + 6h + 12h + 24h \ No newline at end of file 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 37d8781a..221c7236 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -482,4 +482,11 @@ Mise à jour automatique Télécharger et installer automatiquement les mises à jour en arrière-plan via Shizuku + Mises à jour + Intervalle de vérification + Fréquence de vérification des mises à jour en arrière-plan + 3h + 6h + 12h + 24h \ No newline at end of file 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 501df890..6a52cd3f 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -520,4 +520,11 @@ ऐप्स ऑटो-अपडेट करें Shizuku के माध्यम से पृष्ठभूमि में स्वचालित रूप से अपडेट डाउनलोड और इंस्टॉल करें + अपडेट + अपडेट जाँच अंतराल + पृष्ठभूमि में ऐप अपडेट कितनी बार जाँचें + 3घ + 6घ + 12घ + 24घ 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 df4121ed..9bc5809d 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -520,4 +520,11 @@ Aggiornamento automatico Scarica e installa automaticamente gli aggiornamenti in background tramite Shizuku + Aggiornamenti + Intervallo di controllo + Ogni quanto verificare gli aggiornamenti in background + 3h + 6h + 12h + 24h \ No newline at end of file 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 9dee03aa..ebfd58b7 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -482,4 +482,11 @@ アプリを自動更新 Shizukuを使用してバックグラウンドで自動的にアップデートをダウンロードしてインストール + アップデート + アップデート確認間隔 + バックグラウンドでアプリのアップデートを確認する頻度 + 3時間 + 6時間 + 12時間 + 24時間 \ No newline at end of file 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 3dbe360b..a17f1b82 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -517,4 +517,11 @@ 앱 자동 업데이트 Shizuku를 통해 백그라운드에서 자동으로 업데이트 다운로드 및 설치 + 업데이트 + 업데이트 확인 주기 + 백그라운드에서 앱 업데이트를 확인하는 빈도 + 3시간 + 6시간 + 12시간 + 24시간 \ No newline at end of file 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 acb9c66e..b2dcb959 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -484,4 +484,11 @@ Automatyczna aktualizacja Automatycznie pobieraj i instaluj aktualizacje w tle przez Shizuku + Aktualizacje + Częstotliwość sprawdzania + Jak często sprawdzać aktualizacje aplikacji w tle + 3g + 6g + 12g + 24g \ No newline at end of file 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 993311a9..9894bfd2 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -484,4 +484,11 @@ Автообновление приложений Автоматически загружать и устанавливать обновления в фоне через Shizuku + Обновления + Интервал проверки обновлений + Как часто проверять обновления приложения в фоне + + + 12ч + 24ч \ No newline at end of file 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 b4e03773..aa44eea6 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -518,4 +518,11 @@ Uygulamaları otomatik güncelle Shizuku aracılığıyla arka planda otomatik olarak güncellemeleri indirin ve yükleyin + Güncellemeler + Güncelleme kontrol aralığı + Arka planda uygulama güncellemelerinin ne sıklıkla kontrol edileceği + 3s + 6s + 12s + 24s 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 d7b4e821..4a8476ab 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 @@ -483,4 +483,11 @@ 自动更新应用 通过 Shizuku 在后台自动下载并安装更新 + 更新 + 更新检查间隔 + 在后台检查应用更新的频率 + 3小时 + 6小时 + 12小时 + 24小时 \ No newline at end of file diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index 5f1097ec..886071dd 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -525,4 +525,12 @@ Shizuku install failed, using standard installer Auto-update apps Automatically download and install updates in background via Shizuku + + Updates + Update check interval + How often to check for app updates in background + 3h + 6h + 12h + 24h \ No newline at end of file diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt index aba02175..5306e43a 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt @@ -70,6 +70,7 @@ sealed interface ProfileAction { data class OnInstallerTypeSelected(val type: InstallerType) : ProfileAction data object OnRequestShizukuPermission : ProfileAction data class OnAutoUpdateToggled(val enabled: Boolean) : ProfileAction + data class OnUpdateCheckIntervalChanged(val hours: Long) : ProfileAction data class OnAutoDetectClipboardToggled( val enabled: Boolean, diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt index 925d12d0..c9e0b68e 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt @@ -35,6 +35,7 @@ import zed.rainxch.githubstore.core.presentation.res.* import zed.rainxch.profile.presentation.components.LogoutDialog import zed.rainxch.profile.presentation.components.sections.about import zed.rainxch.profile.presentation.components.sections.installationSection +import zed.rainxch.profile.presentation.components.sections.updatesSection import zed.rainxch.profile.presentation.components.sections.logout import zed.rainxch.profile.presentation.components.sections.networkSection import zed.rainxch.profile.presentation.components.sections.othersSection @@ -206,6 +207,11 @@ fun ProfileScreen( onAction = onAction ) + updatesSection( + state = state, + onAction = onAction + ) + item { Spacer(Modifier.height(16.dp)) } diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt index 1e6e8a6b..647a184a 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt @@ -27,4 +27,5 @@ data class ProfileState( val installerType: InstallerType = InstallerType.DEFAULT, val shizukuAvailability: ShizukuAvailability = ShizukuAvailability.UNAVAILABLE, val autoUpdateEnabled: Boolean = false, + val updateCheckIntervalHours: Long = 6L, ) diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt index 7bedaf9c..2d2e9d2e 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt @@ -16,6 +16,7 @@ import zed.rainxch.core.domain.model.ProxyConfig import zed.rainxch.core.domain.repository.ProxyRepository import zed.rainxch.core.domain.repository.ThemesRepository import zed.rainxch.core.domain.system.InstallerStatusProvider +import zed.rainxch.core.domain.system.UpdateScheduleManager import zed.rainxch.core.domain.utils.BrowserHelper import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings @@ -30,6 +31,7 @@ class ProfileViewModel( private val profileRepository: ProfileRepository, private val installerStatusProvider: InstallerStatusProvider, private val proxyRepository: ProxyRepository, + private val updateScheduleManager: UpdateScheduleManager, ) : ViewModel() { private var userProfileJob: Job? = null @@ -48,6 +50,7 @@ class ProfileViewModel( loadInstallerPreference() observeShizukuStatus() loadAutoUpdatePreference() + loadUpdateCheckInterval() hasLoadedInitialData = true } @@ -229,6 +232,16 @@ class ProfileViewModel( } } + private fun loadUpdateCheckInterval() { + viewModelScope.launch { + themesRepository.getUpdateCheckInterval().collect { hours -> + _state.update { + it.copy(updateCheckIntervalHours = hours) + } + } + } + } + fun onAction(action: ProfileAction) { when (action) { ProfileAction.OnHelpClick -> { @@ -402,6 +415,13 @@ class ProfileViewModel( } } + is ProfileAction.OnUpdateCheckIntervalChanged -> { + viewModelScope.launch { + themesRepository.setUpdateCheckInterval(action.hours) + updateScheduleManager.reschedule(action.hours) + } + } + ProfileAction.OnProxySave -> { val currentState = _state.value val port = diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Installation.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Installation.kt index 9ecc7e80..f11cea95 100644 --- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Installation.kt +++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Installation.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -20,9 +21,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.InstallMobile +import androidx.compose.material.icons.outlined.Schedule import androidx.compose.material.icons.outlined.Speed import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -92,6 +96,36 @@ fun LazyListScope.installationSection( } } +/** + * Updates section — always visible on Android (not gated on Shizuku). + * Shows the update check interval picker so all users can configure + * how often background update checks run. + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun LazyListScope.updatesSection( + state: ProfileState, + onAction: (ProfileAction) -> Unit, +) { + if (getPlatform() != Platform.ANDROID) return + + item { + Spacer(Modifier.height(32.dp)) + + SectionHeader( + text = stringResource(Res.string.section_updates).uppercase() + ) + + Spacer(Modifier.height(8.dp)) + + UpdateCheckIntervalCard( + selectedIntervalHours = state.updateCheckIntervalHours, + onIntervalSelected = { hours -> + onAction(ProfileAction.OnUpdateCheckIntervalChanged(hours)) + } + ) + } +} + @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun InstallerTypeCard( @@ -335,6 +369,86 @@ private fun AutoUpdateCard( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun UpdateCheckIntervalCard( + selectedIntervalHours: Long, + onIntervalSelected: (Long) -> Unit, +) { + val intervals = listOf( + 3L to Res.string.interval_3h, + 6L to Res.string.interval_6h, + 12L to Res.string.interval_12h, + 24L to Res.string.interval_24h, + ) + + ExpressiveCard { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Outlined.Schedule, + contentDescription = null, + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(8.dp), + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + text = stringResource(Res.string.update_check_interval_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(Res.string.update_check_interval_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + intervals.forEach { (hours, labelRes) -> + val isSelected = selectedIntervalHours == hours + + FilterChip( + selected = isSelected, + onClick = { onIntervalSelected(hours) }, + label = { + Text( + text = stringResource(labelRes), + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal + ) + }, + shape = RoundedCornerShape(12.dp), + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, + selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + ) + } + } + } + } +} + @Composable private fun HintText(text: String) { Text(