diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt index 5938a2c57..6331b2d9c 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt @@ -341,6 +341,17 @@ class TweaksRepositoryImpl( } } + override fun getChannelChipCoachmarkShown(): Flow = + preferences.data.map { prefs -> + prefs[CHANNEL_CHIP_COACHMARK_SHOWN_KEY] ?: false + } + + override suspend fun setChannelChipCoachmarkShown(shown: Boolean) { + preferences.edit { prefs -> + prefs[CHANNEL_CHIP_COACHMARK_SHOWN_KEY] = shown + } + } + override fun getBatteryOptimizationPromptDismissed(): Flow = preferences.data.map { prefs -> prefs[BATTERY_OPT_PROMPT_DISMISSED_KEY] ?: false @@ -445,6 +456,7 @@ class TweaksRepositoryImpl( private val EXTERNAL_MATCH_SEARCH_ENABLED_KEY = booleanPreferencesKey("external_match_search_enabled") private val EXTERNAL_IMPORT_BANNER_DISMISSED_AT_KEY = intPreferencesKey("external_import_banner_dismissed_at") private val APK_INSPECT_COACHMARK_SHOWN_KEY = booleanPreferencesKey("apk_inspect_coachmark_shown") + private val CHANNEL_CHIP_COACHMARK_SHOWN_KEY = booleanPreferencesKey("channel_chip_coachmark_shown") private val BATTERY_OPT_PROMPT_DISMISSED_KEY = booleanPreferencesKey("battery_opt_prompt_dismissed") private val LAST_SEEN_WHATS_NEW_VERSION_CODE_KEY = intPreferencesKey("last_seen_whats_new_version_code") private val ANNOUNCEMENTS_DISMISSED_IDS_KEY = stringSetPreferencesKey("announcements_dismissed_ids") diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt index a8a989eb2..2051e6075 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt @@ -113,6 +113,16 @@ interface TweaksRepository { suspend fun setApkInspectCoachmarkShown(shown: Boolean) + /** + * One-shot flag for the release-channel coachmark on the Details + * screen. Survey signal — users don't realise the per-app channel + * chip toggles betas. `false` until shown at least once; permanent + * `true` after dismissal. + */ + fun getChannelChipCoachmarkShown(): Flow + + suspend fun setChannelChipCoachmarkShown(shown: Boolean) + /** * One-shot watermark for the battery-optimization prompt on * aggressive-OEM ROMs (Oppo / OnePlus / Realme / Xiaomi / vivo / diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json index 63021991f..e39962ce0 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/17.json @@ -28,7 +28,8 @@ "type": "IMPROVED", "bullets": [ "More reliable background updates on Oppo, OnePlus, Realme, Xiaomi, vivo, and Honor — Tweaks now offers a one-tap battery-optimization shortcut and update workers run with expedited priority.", - "Dhizuku silent install on Android 14+ — the app now retries automatically without installer attribution when the system would otherwise demand a confirmation tap." + "Dhizuku silent install on Android 14+ — the app now retries automatically without installer attribution when the system would otherwise demand a confirmation tap.", + "First-time coachmark on the per-app release-channel chip — explains how to switch between stable and beta builds, surfaces the toggle users were missing." ] } ] 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 5de26be2d..6e3eb111e 100644 --- a/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml +++ b/core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml @@ -602,8 +602,8 @@ استيراد التطبيقات الصق ملف JSON المُصدَّر لاستعادة التطبيقات المتتبعة الصق JSON المُصدَّر هنا… - تضمين الإصدارات التجريبية - تتبع الإصدارات التجريبية عند التحقق من التحديثات. عند التعطيل، يتم اعتبار الإصدارات المستقرة فقط. + قناة البيتا الافتراضية + تشمل التطبيقات التي تتعقبها حديثاً إصدارات البيتا افتراضياً. التطبيقات المتعقَّبة مسبقاً تحتفظ بإعدادها الخاص (بدّله من شاشة التفاصيل). إلغاء تثبيت التطبيق؟ هل أنت متأكد من إلغاء تثبيت %1$s؟ لا يمكن التراجع عن هذا الإجراء وقد تُفقد بيانات التطبيق. رابط GitHub غير صالح. استخدم التنسيق: github.com/owner/repo @@ -767,6 +767,9 @@ تضمين الإصدارات التجريبية الإصدار المستقر فقط + اختر قناة الإصدار + اضغط للتبديل بين الإصدارات المستقرة وإصدارات البيتا لهذا التطبيق. + حسناً تبديل الإصدارات التجريبية لهذا التطبيق التبديل إلى الإصدار المستقر %1$s لا يوجد إصدار مستقر منذ %1$d أشهر 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 e1c9dbc9c..7da791d3d 100644 --- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml +++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml @@ -601,8 +601,8 @@ অ্যাপ আমদানি করুন ট্র্যাক করা অ্যাপ পুনরুদ্ধার করতে রপ্তানি করা JSON পেস্ট করুন রপ্তানি করা JSON এখানে পেস্ট করুন… - প্রি-রিলিজ অন্তর্ভুক্ত করুন - আপডেট পরীক্ষার সময় প্রি-রিলিজ সংস্করণ ট্র্যাক করুন। নিষ্ক্রিয় থাকলে, শুধুমাত্র স্থিতিশীল রিলিজ বিবেচনা করা হয়। + ডিফল্ট বেটা চ্যানেল + নতুন ট্র্যাক করা অ্যাপ ডিফল্টভাবে বেটা বিল্ড অন্তর্ভুক্ত করবে। ইতিমধ্যে ট্র্যাক করা অ্যাপ তাদের নিজস্ব সেটিং রাখবে (অ্যাপের বিস্তারিত স্ক্রিনে পরিবর্তন করুন)। অ্যাপ আনইনস্টল করবেন? আপনি কি নিশ্চিত যে %1$s আনইনস্টল করতে চান? এই ক্রিয়া পূর্বাবস্থায় ফেরানো যাবে না এবং অ্যাপের ডেটা হারিয়ে যেতে পারে। অবৈধ GitHub URL। ফর্ম্যাট ব্যবহার করুন: github.com/owner/repo @@ -767,6 +767,9 @@ বেটা অন্তর্ভুক্ত করুন শুধুমাত্র স্থিতিশীল + আপনার রিলিজ চ্যানেল বাছুন + এই অ্যাপের স্থিতিশীল রিলিজ এবং বেটা বিল্ডের মধ্যে স্যুইচ করতে আলতো চাপুন। + বুঝেছি এই অ্যাপের জন্য বেটা রিলিজ টগল করুন স্থিতিশীল %1$s-এ যান %1$d মাসে কোনো স্থিতিশীল রিলিজ নেই 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 e0bcdd638..b7c0777b4 100644 --- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml +++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml @@ -562,8 +562,8 @@ Importar apps Pega el JSON exportado para restaurar tus apps rastreadas Pega el JSON exportado aquí… - Incluir pre-lanzamientos - Rastrear versiones pre-lanzamiento al buscar actualizaciones. Si está desactivado, solo se consideran las versiones estables. + Canal beta predeterminado + Las apps recién seguidas incluyen compilaciones beta por defecto. Las ya seguidas conservan su propio canal (cámbialo en la pantalla de Detalles de la app). ¿Desinstalar app? ¿Estás seguro de que quieres desinstalar %1$s? Esta acción no se puede deshacer y los datos de la app podrían perderse. URL de GitHub no válida. Usa el formato: github.com/owner/repo @@ -731,6 +731,9 @@ ═══════════════════════════════════════════════════════════════ --> Incluir betas Solo estable + Elige tu canal de lanzamiento + Toca para alternar entre lanzamientos estables y compilaciones beta de esta app. + Entendido Alternar versiones beta de esta aplicación Cambiar a %1$s estable Sin versión estable en %1$d meses 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 5f078c060..e9a72b942 100644 --- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml +++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml @@ -563,8 +563,8 @@ Importer des apps Collez le JSON exporté pour restaurer vos apps suivies Collez le JSON exporté ici… - Inclure les pré-versions - Suivre les versions pré-release lors de la vérification des mises à jour. Désactivé, seules les versions stables sont prises en compte. + Canal bêta par défaut + Les apps nouvellement suivies incluent les versions bêta par défaut. Les apps déjà suivies conservent leur propre paramètre (modifiable dans Détails de l\'app). Désinstaller l'app ? Êtes-vous sûr de vouloir désinstaller %1$s ? Cette action est irréversible et les données de l'app pourraient être perdues. URL GitHub invalide. Utilisez le format : github.com/owner/repo @@ -732,6 +732,9 @@ ═══════════════════════════════════════════════════════════════ --> Inclure les bêtas Stable uniquement + Choisissez votre canal de version + Touchez pour basculer entre les versions stables et les bêtas pour cette app. + Compris Activer/désactiver les versions bêta de cette application Passer à %1$s stable Aucune version stable depuis %1$d mois 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 04b1cb63f..a31eaeeeb 100644 --- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml +++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml @@ -600,8 +600,8 @@ ऐप्स आयात करें अपने ट्रैक किए गए ऐप्स को पुनर्स्थापित करने के लिए निर्यात किया गया JSON पेस्ट करें निर्यात किया गया JSON यहाँ पेस्ट करें… - प्री-रिलीज़ शामिल करें - अपडेट की जाँच करते समय प्री-रिलीज़ संस्करणों को ट्रैक करें। अक्षम होने पर, केवल स्थिर रिलीज़ पर विचार किया जाता है। + डिफ़ॉल्ट बीटा चैनल + नए ट्रैक किए गए ऐप डिफ़ॉल्ट रूप से बीटा बिल्ड शामिल करते हैं। पहले से ट्रैक किए गए ऐप अपनी सेटिंग रखते हैं (ऐप के विवरण स्क्रीन से बदलें)। ऐप अनइंस्टॉल करें? क्या आप वाकई %1$s को अनइंस्टॉल करना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती और ऐप डेटा खो सकता है। अमान्य GitHub URL। प्रारूप का उपयोग करें: github.com/owner/repo @@ -770,6 +770,9 @@ ═══════════════════════════════════════════════════════════════ --> बीटा शामिल करें केवल स्थिर + अपना रिलीज़ चैनल चुनें + इस ऐप के स्थिर रिलीज़ और बीटा बिल्ड के बीच स्विच करने के लिए टैप करें। + समझ गया इस ऐप के बीटा रिलीज़ टॉगल करें %1$s स्थिर पर स्विच करें %1$d महीनों में कोई स्थिर रिलीज़ नहीं 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 7731991d0..4f0394421 100644 --- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml +++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml @@ -601,8 +601,8 @@ Importa app Incolla il JSON esportato per ripristinare le app monitorate Incolla il JSON esportato qui… - Includi pre-release - Monitora le versioni pre-release durante il controllo aggiornamenti. Se disabilitato, vengono considerate solo le versioni stabili. + Canale beta predefinito + Le app appena tracciate includono build beta per impostazione predefinita. Le app già tracciate mantengono il proprio canale (modificalo nella schermata Dettagli). Disinstallare l'app? Sei sicuro di voler disinstallare %1$s? Questa azione non può essere annullata e i dati dell'app potrebbero andare persi. URL GitHub non valido. Usa il formato: github.com/owner/repo @@ -771,6 +771,9 @@ ═══════════════════════════════════════════════════════════════ --> Includi beta Solo stabile + Scegli il canale di rilascio + Tocca per passare tra rilasci stabili e build beta per questa app. + Ho capito Attiva/disattiva le versioni beta per questa app Passa a %1$s stabile Nessuna versione stabile da %1$d mesi 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 f79352d59..b19cce2fd 100644 --- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml +++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml @@ -564,8 +564,8 @@ アプリをインポート エクスポートしたJSONを貼り付けて追跡中のアプリを復元 エクスポートしたJSONをここに貼り付け… - プレリリースを含める - アップデート確認時にプレリリース版を追跡します。無効の場合、安定版リリースのみが対象となります。 + デフォルトのベータチャンネル + 新しく追跡したアプリはデフォルトでベータビルドを含みます。既に追跡中のアプリは独自の設定を保持します(アプリの詳細画面で変更可能)。 アプリをアンインストールしますか? %1$sをアンインストールしてよろしいですか?この操作は元に戻せず、アプリのデータが失われる可能性があります。 無効なGitHub URL。形式を使用してください:github.com/owner/repo @@ -731,6 +731,9 @@ ═══════════════════════════════════════════════════════════════ --> ベータを含む 安定版のみ + リリースチャンネルを選択 + このアプリの安定版とベータビルドを切り替えるにはタップ。 + 了解 このアプリのベータリリースを切り替え %1$s の安定版に切り替え %1$d か月間、安定版リリースなし 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 2106bdabc..d6800c755 100644 --- a/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml +++ b/core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml @@ -599,8 +599,8 @@ 앱 가져오기 내보낸 JSON을 붙여넣어 추적 중인 앱을 복원하세요 내보낸 JSON을 여기에 붙여넣기… - 사전 릴리스 포함 - 업데이트 확인 시 사전 릴리스 버전을 추적합니다. 비활성화하면 안정적인 릴리스만 고려됩니다. + 기본 베타 채널 + 새로 추적한 앱은 기본적으로 베타 빌드를 포함합니다. 이미 추적 중인 앱은 자체 설정을 유지합니다(앱 세부 정보 화면에서 전환). 앱을 제거하시겠습니까? %1$s을(를) 제거하시겠습니까? 이 작업은 취소할 수 없으며 앱 데이터가 손실될 수 있습니다. 잘못된 GitHub URL입니다. 형식: github.com/owner/repo @@ -766,6 +766,9 @@ ═══════════════════════════════════════════════════════════════ --> 베타 포함 안정 버전만 + 릴리스 채널 선택 + 이 앱의 안정 릴리스와 베타 빌드를 전환하려면 탭하세요. + 확인 이 앱의 베타 릴리스 전환 %1$s 안정 버전으로 전환 %1$d개월간 안정 릴리스 없음 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 de0505269..dd3c9c502 100644 --- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml +++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml @@ -565,8 +565,8 @@ Importuj aplikacje Wklej wyeksportowany JSON, aby przywrócić śledzone aplikacje Wklej wyeksportowany JSON tutaj… - Uwzględnij wersje wstępne - Śledź wersje wstępne podczas sprawdzania aktualizacji. Po wyłączeniu uwzględniane są tylko stabilne wydania. + Domyślny kanał beta + Nowo śledzone aplikacje domyślnie zawierają wersje beta. Już śledzone aplikacje zachowują własne ustawienie (zmień je na ekranie Szczegółów aplikacji). Odinstalować aplikację? Czy na pewno chcesz odinstalować %1$s? Tej czynności nie można cofnąć, a dane aplikacji mogą zostać utracone. Nieprawidłowy URL GitHub. Użyj formatu: github.com/owner/repo @@ -739,6 +739,9 @@ ═══════════════════════════════════════════════════════════════ --> Uwzględnij wersje beta Tylko stabilne + Wybierz kanał wydań + Dotknij, aby przełączać między stabilnymi wydaniami a wersjami beta tej aplikacji. + OK Przełącz wydania beta dla tej aplikacji Przejdź do stabilnej wersji %1$s Brak stabilnego wydania od %1$d miesięcy 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 cb7489a11..753f382cb 100644 --- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml +++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml @@ -565,8 +565,8 @@ Импорт приложений Вставьте экспортированный JSON для восстановления отслеживаемых приложений Вставьте экспортированный JSON… - Включить пре-релизы - Отслеживать пре-релизные версии при проверке обновлений. При отключении учитываются только стабильные релизы. + Канал бета по умолчанию + Вновь отслеживаемые приложения по умолчанию включают бета-сборки. Ранее отслеживаемые приложения сохраняют своё значение (переключите на экране Подробностей приложения). Удалить приложение? Вы уверены, что хотите удалить %1$s? Это действие нельзя отменить, данные приложения могут быть утеряны. Неверный URL GitHub. Используйте формат: github.com/owner/repo @@ -739,6 +739,9 @@ ═══════════════════════════════════════════════════════════════ --> Включить бета-версии Только стабильные + Выберите канал релизов + Нажмите, чтобы переключаться между стабильными релизами и бета-сборками этого приложения. + Понятно Переключить бета-релизы для этого приложения Перейти на стабильную %1$s Нет стабильного релиза уже %1$d месяцев 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 e35f40f8e..09bb56bd1 100644 --- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml +++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml @@ -599,8 +599,8 @@ Uygulamaları içe aktar Takip edilen uygulamaları geri yüklemek için dışa aktarılan JSON'u yapıştırın Dışa aktarılan JSON'u buraya yapıştırın… - Ön sürümleri dahil et - Güncelleme kontrolünde ön sürümleri takip edin. Devre dışı bırakıldığında, yalnızca kararlı sürümler dikkate alınır. + Varsayılan beta kanalı + Yeni takip edilen uygulamalar varsayılan olarak beta sürümleri içerir. Önceden takip edilen uygulamalar kendi ayarını korur (uygulamanın Ayrıntılar ekranından değiştirin). Uygulama kaldırılsın mı? %1$s uygulamasını kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz ve uygulama verileri kaybolabilir. Geçersiz GitHub URL'si. Biçim: github.com/owner/repo @@ -768,6 +768,9 @@ ═══════════════════════════════════════════════════════════════ --> Beta sürümleri dahil et Yalnızca kararlı + Sürüm kanalını seçin + Bu uygulamanın kararlı sürümleri ile beta sürümleri arasında geçiş yapmak için dokunun. + Anladım Bu uygulama için beta sürümlerini aç/kapat %1$s kararlı sürümüne geç %1$d aydır kararlı sürüm yok 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 0469dd1d9..17fa993d3 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 @@ -565,8 +565,8 @@ 导入应用 粘贴导出的JSON以恢复跟踪的应用 在此粘贴导出的JSON… - 包含预发布版本 - 检查更新时跟踪预发布版本。禁用后,仅考虑稳定版本。 + 默认测试版渠道 + 新追踪的应用默认包含测试版。已追踪的应用保留各自设置(在应用详情页切换)。 卸载应用? 确定要卸载%1$s吗?此操作无法撤销,应用数据可能会丢失。 无效的GitHub URL。请使用格式:github.com/owner/repo @@ -733,6 +733,9 @@ ═══════════════════════════════════════════════════════════════ --> 包含测试版 仅稳定版 + 选择发布渠道 + 点按以在此应用的稳定版和测试版之间切换。 + 知道了 切换此应用的测试版发布 切换到 %1$s 稳定版 %1$d 个月内无稳定版发布 diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index e79b38b92..c7b4482f0 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -481,6 +481,9 @@ Include betas Stable only + Choose your release channel + Tap to switch between stable releases and beta builds for this app. + Got it Toggle beta releases for this app Switch to stable %1$s No stable release in %1$d months @@ -736,8 +739,8 @@ Paste the exported JSON to restore your tracked apps Paste exported JSON here… - Include pre-releases - Track pre-release versions when checking for updates. When disabled, only stable releases are considered. + Default beta channel + Newly tracked apps include beta builds by default. Already-tracked apps keep their own per-app channel setting (toggle it on the app\'s Details screen). Uninstall app? Are you sure you want to uninstall %1$s? This action cannot be undone and app data may be lost. Discard pending install? diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt index 9365a984a..2e419c5e8 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt @@ -152,4 +152,11 @@ sealed interface DetailsAction { * coachmark only ever shows once. */ data object OnAcknowledgeApkInspectCoachmark : DetailsAction + + /** + * Acknowledges the release-channel chip coachmark. Fired on + * tap/dismiss or when the user toggles the channel chip itself. + * Persists so the coachmark only ever shows once. + */ + data object OnAcknowledgeChannelChipCoachmark : DetailsAction } diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt index 290fd1824..b0991ec24 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.kt @@ -128,6 +128,12 @@ data class DetailsState( * pulse + tooltip animation in the install button row. */ val isApkInspectCoachmarkPending: Boolean = false, + /** + * One-shot flag — `false` until the user has seen the + * release-channel coachmark. Drives the pulse + tooltip on the + * `ChannelChip` so users discover the per-app channel toggle. + */ + val isChannelChipCoachmarkPending: Boolean = false, ) { val filteredReleases: List get() = diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt index 1bff29975..96b4e4e05 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt @@ -145,6 +145,7 @@ class DetailsViewModel( if (!hasLoadedInitialData) { loadInitial() observeApkInspectCoachmark() + observeChannelChipCoachmark() observeCurrentUserForBadge() hasLoadedInitialData = true @@ -463,6 +464,10 @@ class DetailsViewModel( } DetailsAction.ToggleIncludeBetas -> { + // Tapping the chip is the strongest possible signal that + // the user has discovered the toggle. No need to keep + // pulsing it after that. + acknowledgeChannelChipCoachmark() toggleIncludeBetas() } @@ -483,6 +488,10 @@ class DetailsViewModel( DetailsAction.OnAcknowledgeApkInspectCoachmark -> { acknowledgeApkInspectCoachmark() } + + DetailsAction.OnAcknowledgeChannelChipCoachmark -> { + acknowledgeChannelChipCoachmark() + } } } @@ -546,6 +555,22 @@ class DetailsViewModel( } } + private fun acknowledgeChannelChipCoachmark() { + // Persist unconditionally — `state.update` with the same value is a + // no-op, and `setChannelChipCoachmarkShown(true)` is idempotent. + // Skipping persistence when in-memory `pending` is already false + // (e.g., toggle + dismiss race) leaves the DataStore flag at false + // if the very first persist failed, so the coachmark would + // re-appear on next launch. + _state.update { it.copy(isChannelChipCoachmarkPending = false) } + viewModelScope.launch { + runCatching { tweaksRepository.setChannelChipCoachmarkShown(true) } + .onFailure { t -> + logger.warn("Failed to persist channel chip coachmark flag: ${t.message}") + } + } + } + /** * Derived signals surfaced in the Details UX for pre-release * handling (release UX #4 and #6). Computed once per release-list @@ -848,6 +873,22 @@ class DetailsViewModel( } } + private fun observeChannelChipCoachmark() { + viewModelScope.launch { + val alreadyShown = + runCatching { tweaksRepository.getChannelChipCoachmarkShown().first() } + .getOrDefault(true) + if (alreadyShown) return@launch + // ChannelChip only renders when the app is tracked + // (`installedApp != null` in `ReleaseChannel.releaseChannel`). + // No point pulsing a non-existent chip — defer to a future + // visit where the user actually has the app installed. + val firstStable = _state.first { !it.isLoading } + if (firstStable.installedApp == null) return@launch + _state.update { it.copy(isChannelChipCoachmarkPending = true) } + } + } + private fun retryReleases() { val repo = _state.value.repository ?: return if (_state.value.isRetryingReleases) return diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt index 1f422ec5c..0a116801c 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/ReleaseChannel.kt @@ -1,7 +1,13 @@ package zed.rainxch.details.presentation.components.sections +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable 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 @@ -10,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -23,17 +30,26 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import org.jetbrains.compose.resources.stringResource import zed.rainxch.details.presentation.DetailsAction import zed.rainxch.details.presentation.DetailsState import zed.rainxch.githubstore.core.presentation.res.Res import zed.rainxch.githubstore.core.presentation.res.action_switch_to_stable +import zed.rainxch.githubstore.core.presentation.res.channel_chip_coachmark_body +import zed.rainxch.githubstore.core.presentation.res.channel_chip_coachmark_dismiss +import zed.rainxch.githubstore.core.presentation.res.channel_chip_coachmark_title import zed.rainxch.githubstore.core.presentation.res.channel_chip_include_betas import zed.rainxch.githubstore.core.presentation.res.channel_chip_stable_only import zed.rainxch.githubstore.core.presentation.res.merged_whats_changed_title @@ -86,27 +102,37 @@ fun LazyListScope.releaseChannel( } else { stringResource(Res.string.channel_chip_stable_only) } - ChannelChip( - label = channelLabel, - icon = Icons.Default.Science, - // Visually signal the "hot" channel when the user - // has opted into betas; keep it muted when they're - // on the default stable-only track. - tint = - if (includeBetas) { - MaterialTheme.colorScheme.tertiary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - onClick = { onAction(DetailsAction.ToggleIncludeBetas) }, - // Mirror the visible label so screen readers hear - // the current channel ("Include betas" / "Stable - // only") instead of the previous static - // "Toggle beta releases for this app" string, - // which gave no indication of which side the - // toggle is currently on. - contentDescriptionText = channelLabel, - ) + val pulse by rememberChipPulse(active = state.isChannelChipCoachmarkPending) + Box(modifier = Modifier.scale(pulse)) { + ChannelChip( + label = channelLabel, + icon = Icons.Default.Science, + // Visually signal the "hot" channel when the user + // has opted into betas; keep it muted when they're + // on the default stable-only track. + tint = + if (includeBetas) { + MaterialTheme.colorScheme.tertiary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + onClick = { onAction(DetailsAction.ToggleIncludeBetas) }, + // Mirror the visible label so screen readers hear + // the current channel ("Include betas" / "Stable + // only") instead of the previous static + // "Toggle beta releases for this app" string, + // which gave no indication of which side the + // toggle is currently on. + contentDescriptionText = channelLabel, + ) + if (state.isChannelChipCoachmarkPending) { + ChannelChipCoachmark( + onDismiss = { + onAction(DetailsAction.OnAcknowledgeChannelChipCoachmark) + }, + ) + } + } } if (showSwitchToStable) { @@ -256,3 +282,79 @@ private fun ChannelChip( } } } + +@Composable +private fun rememberChipPulse(active: Boolean) = + rememberInfiniteTransition(label = "channel-chip-pulse") + .animateFloat( + initialValue = 1f, + targetValue = if (active) 1.06f else 1f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1100), + repeatMode = RepeatMode.Reverse, + ), + label = "channel-chip-pulse-scale", + ) + +@Composable +private fun ChannelChipCoachmark(onDismiss: () -> Unit) { + Popup( + alignment = Alignment.TopStart, + offset = androidx.compose.ui.unit.IntOffset(x = 0, y = -220), + properties = + PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = false, + ), + onDismissRequest = onDismiss, + ) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.primary, + shadowElevation = 6.dp, + modifier = Modifier.width(280.dp), + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = Icons.Default.Science, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(16.dp), + ) + Text( + text = stringResource(Res.string.channel_chip_coachmark_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.Bold, + ) + } + Text( + text = stringResource(Res.string.channel_chip_coachmark_body), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.9f), + ) + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onDismiss) { + Text( + text = stringResource(Res.string.channel_chip_coachmark_dismiss), + color = MaterialTheme.colorScheme.onPrimary, + fontWeight = FontWeight.SemiBold, + ) + } + } + } + } + } +}