diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/18.json index 182a8c484..1790bed2a 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/18.json @@ -22,7 +22,8 @@ "Sign-in opens the device-code URL prefilled and copies a paste-friendly code.", "Android 16 / custom-ROM crash on Settings + download failure — falls back to internal storage when external is blocked.", "Magisk / KernelSU / APatch detection on Android 13+ — previous probe was masked by SELinux.", - "Pinned-variant label on Details refreshes across releases — old beta/rc qualifier numbers no longer linger in the chip." + "Pinned-variant label on Details refreshes across releases — old beta/rc qualifier numbers no longer linger in the chip.", + "README and release-notes no longer snap back to the top when you scroll past and return — measured height now persists across viewport disposal." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json index bbe9ecff2..bb4df0f27 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/18.json @@ -22,7 +22,8 @@ "تسجيل الدخول يفتح رابط الرمز مع تعبئته مسبقاً وينسخ نسخة مناسبة للصق.", "إصلاح تعطل الإعدادات وفشل التنزيلات على Android 16 / ROMات مخصصة — يستخدم التخزين الداخلي عند حجب الخارجي.", "اكتشاف Magisk / KernelSU / APatch على Android 13+ — كان الفحص السابق محجوبًا بـ SELinux.", - "تسمية المتغير المثبت في التفاصيل تتحدث عبر الإصدارات — أرقام beta/rc القديمة لم تعد تظهر في الشارة." + "تسمية المتغير المثبت في التفاصيل تتحدث عبر الإصدارات — أرقام beta/rc القديمة لم تعد تظهر في الشارة.", + "لم تعد ملاحظات README والإصدار ترتد إلى الأعلى عند التمرير بعيداً والعودة — يتم الآن الاحتفاظ بالارتفاع المُقاس عبر إعادة إنشاء العرض." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json index 42116af3f..fe5e2c47a 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/18.json @@ -22,7 +22,8 @@ "সাইন-ইন এখন কোড URL আগে থেকে পূরণ করে খোলে এবং পেস্ট-বান্ধব কোড কপি করে।", "Android 16 / কাস্টম ROM-এ সেটিংস ক্র্যাশ ও ডাউনলোড ব্যর্থতা — বহিরাগত স্টোরেজ ব্লক হলে অভ্যন্তরীণ স্টোরেজে ফলব্যাক।", "Android 13+-এ Magisk / KernelSU / APatch শনাক্তকরণ — পূর্ববর্তী যাচাই SELinux দ্বারা ঢাকা পড়েছিল।", - "Details-এ পিনড ভ্যারিয়েন্ট লেবেল প্রতি রিলিজে রিফ্রেশ হয় — পুরোনো beta/rc নম্বর আর চিপে থাকে না।" + "Details-এ পিনড ভ্যারিয়েন্ট লেবেল প্রতি রিলিজে রিফ্রেশ হয় — পুরোনো beta/rc নম্বর আর চিপে থাকে না।", + "README ও রিলিজ নোট স্ক্রল করে ফিরে এলে আর শুরুতে ফিরে যায় না — মাপা উচ্চতা এখন ভিউপোর্ট ডিসপোজাল জুড়ে থাকে।" ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json index 3183f2822..5482ca4a1 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/es/18.json @@ -22,7 +22,8 @@ "El inicio de sesión abre la URL con el código ya rellenado y copia un código compatible con pegado.", "Crash en Android 16 / ROMs personalizadas en Ajustes y descargas — usa almacenamiento interno cuando el externo está bloqueado.", "Detección de Magisk / KernelSU / APatch en Android 13+ — la prueba anterior la ocultaba SELinux.", - "La etiqueta de variante fijada en Detalles se refresca entre versiones — los números beta/rc antiguos ya no se quedan en el chip." + "La etiqueta de variante fijada en Detalles se refresca entre versiones — los números beta/rc antiguos ya no se quedan en el chip.", + "El README y las notas de versión ya no vuelven al principio al desplazarte y regresar — la altura medida ahora se conserva entre desasignaciones de viewport." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json index f8dd1ee31..3586eaf88 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/18.json @@ -22,7 +22,8 @@ "La connexion ouvre l'URL du code pré-remplie et copie un code adapté au collage.", "Crash sur Android 16 / ROMs personnalisées dans Paramètres et téléchargements — bascule vers stockage interne quand l'externe est bloqué.", "Détection de Magisk / KernelSU / APatch sur Android 13+ — la sonde précédente était masquée par SELinux.", - "L'étiquette de variante épinglée dans Détails se rafraîchit entre versions — les anciens numéros beta/rc ne persistent plus dans la puce." + "L'étiquette de variante épinglée dans Détails se rafraîchit entre versions — les anciens numéros beta/rc ne persistent plus dans la puce.", + "Le README et les notes de version ne reviennent plus en haut quand tu fais défiler puis reviens — la hauteur mesurée est désormais conservée au-delà des disposals du viewport." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json index d234ae64b..9fff6e706 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/18.json @@ -22,7 +22,8 @@ "साइन-इन अब प्री-फिल्ड कोड URL खोलता है और पेस्ट-फ्रेंडली कोड कॉपी करता है।", "Android 16 / कस्टम ROM में सेटिंग्स क्रैश और डाउनलोड विफलता — बाहरी स्टोरेज ब्लॉक होने पर आंतरिक स्टोरेज पर फॉलबैक।", "Android 13+ पर Magisk / KernelSU / APatch का पता लगाना — पिछली जांच SELinux से छिप गई थी।", - "Details पर पिन किया गया वेरिएंट लेबल रिलीज़ के साथ रिफ्रेश होता है — पुराने beta/rc नंबर अब चिप में नहीं रहते।" + "Details पर पिन किया गया वेरिएंट लेबल रिलीज़ के साथ रिफ्रेश होता है — पुराने beta/rc नंबर अब चिप में नहीं रहते।", + "README और रिलीज़ नोट्स अब स्क्रॉल करके वापस आने पर शुरुआत में नहीं जाते — मापी गई ऊँचाई अब व्यूपोर्ट डिस्पोज़ल के पार बनी रहती है।" ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json index 887e99b5d..1f3f7f17e 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/it/18.json @@ -22,7 +22,8 @@ "Il login apre l'URL del codice precompilato e copia un codice facile da incollare.", "Crash su Android 16 / ROM personalizzate in Impostazioni e download — ripiega su archiviazione interna quando quella esterna è bloccata.", "Rilevamento di Magisk / KernelSU / APatch su Android 13+ — la sonda precedente era mascherata da SELinux.", - "L'etichetta della variante fissata nei Dettagli si aggiorna tra le release — i vecchi numeri beta/rc non restano più nel chip." + "L'etichetta della variante fissata nei Dettagli si aggiorna tra le release — i vecchi numeri beta/rc non restano più nel chip.", + "README e note di rilascio non tornano più in cima quando scorri via e poi torni — l'altezza misurata ora persiste tra le rimozioni dal viewport." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json index 9f2495ada..b39713bbb 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/18.json @@ -22,7 +22,8 @@ "サインインがデバイスコード入りの URL を開き、貼り付けやすい形式でコードをコピーするように。", "Android 16 / カスタム ROM での設定クラッシュとダウンロード失敗を修正 — 外部ストレージがブロックされたら内部にフォールバック。", "Android 13+ で Magisk / KernelSU / APatch を検出 — 以前のプローブは SELinux で見えなかった。", - "詳細のピン留めバリアントラベルがリリースごとに更新されるように — 古い beta/rc 番号がチップに残らなくなりました。" + "詳細のピン留めバリアントラベルがリリースごとに更新されるように — 古い beta/rc 番号がチップに残らなくなりました。", + "README やリリースノートをスクロールで通り過ぎて戻っても先頭に巻き戻らなくなりました — 測定済みの高さがビューポート破棄をまたいで保持されます。" ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json index be96edb5e..8ced37b1b 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/18.json @@ -22,7 +22,8 @@ "로그인이 기기 코드가 채워진 URL을 열고 붙여넣기 친화적인 코드를 복사합니다.", "Android 16 / 커스텀 ROM에서 설정 크래시 및 다운로드 실패 수정 — 외부 저장소가 차단되면 내부 저장소로 폴백.", "Android 13+ 에서 Magisk / KernelSU / APatch 감지 — 이전 검사는 SELinux 로 가려졌었습니다.", - "상세의 고정된 변형 라벨이 릴리스마다 새로고침됩니다 — 오래된 beta/rc 번호가 더 이상 칩에 남지 않습니다." + "상세의 고정된 변형 라벨이 릴리스마다 새로고침됩니다 — 오래된 beta/rc 번호가 더 이상 칩에 남지 않습니다.", + "README와 릴리스 노트가 스크롤로 지나갔다가 돌아와도 더 이상 처음으로 튀지 않습니다 — 측정된 높이가 뷰포트 폐기 사이에도 유지됩니다." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json index fd2e86f64..837f6ab64 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/18.json @@ -22,7 +22,8 @@ "Logowanie otwiera URL kodu z wstępnie wypełnionym kodem i kopiuje wersję przyjazną do wklejania.", "Crash na Android 16 / niestandardowych ROM-ach w Ustawieniach i pobieraniu — przełącza na pamięć wewnętrzną, gdy zewnętrzna jest zablokowana.", "Wykrywanie Magisk / KernelSU / APatch na Android 13+ — poprzednia sonda była maskowana przez SELinux.", - "Etykieta przypiętego wariantu w Szczegółach odświeża się między wydaniami — stare numery beta/rc nie utrzymują się już w chipie." + "Etykieta przypiętego wariantu w Szczegółach odświeża się między wydaniami — stare numery beta/rc nie utrzymują się już w chipie.", + "README i notatki wydania nie wracają już na początek po przewinięciu i powrocie — zmierzona wysokość jest teraz zachowywana między usunięciami z viewportu." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json index d227407bf..dbb82e6e6 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/18.json @@ -22,7 +22,8 @@ "Вход открывает URL с уже заполненным кодом и копирует версию, удобную для вставки.", "Сбой на Android 16 / кастомных ROM в Настройках и загрузках — переключается на внутреннее хранилище, когда внешнее заблокировано.", "Обнаружение Magisk / KernelSU / APatch на Android 13+ — предыдущая проверка скрывалась SELinux.", - "Шильдик закреплённого варианта в Деталях обновляется между релизами — старые номера beta/rc больше не залипают." + "Шильдик закреплённого варианта в Деталях обновляется между релизами — старые номера beta/rc больше не залипают.", + "README и заметки релиза больше не отскакивают к началу при возврате прокрутки — измеренная высота теперь сохраняется при пересоздании элементов viewport." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json index 7fa0c3cdc..ebaa5471a 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/18.json @@ -22,7 +22,8 @@ "Oturum açma, kod URL'sini önceden doldurulmuş açar ve yapıştırmaya uygun kodu kopyalar.", "Android 16 / özel ROM'larda Ayarlar çökmesi ve indirme hatası — harici depolama engellendiğinde dahili depolamaya geçer.", "Android 13+ üzerinde Magisk / KernelSU / APatch algılama — önceki sonda SELinux tarafından maskeleniyordu.", - "Detaylar'daki sabitlenmiş varyant etiketi sürümler arasında yenileniyor — eski beta/rc numaraları artık çipte takılı kalmıyor." + "Detaylar'daki sabitlenmiş varyant etiketi sürümler arasında yenileniyor — eski beta/rc numaraları artık çipte takılı kalmıyor.", + "README ve sürüm notları kaydırıp dönünce artık başa atlamıyor — ölçülen yükseklik viewport temizliklerinin ardından da korunuyor." ] }, { diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json index cbd820fac..8f820b35c 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/18.json @@ -22,7 +22,8 @@ "登录现在打开预填了设备代码的 URL 并复制粘贴友好的代码。", "修复 Android 16 / 定制 ROM 上设置闪退与下载失败 — 外部存储被阻止时回退到内部存储。", "Android 13+ 上检测 Magisk / KernelSU / APatch — 之前的探测被 SELinux 屏蔽。", - "详情页的已固定变体标签会随版本刷新 — 旧的 beta/rc 编号不再残留在标签中。" + "详情页的已固定变体标签会随版本刷新 — 旧的 beta/rc 编号不再残留在标签中。", + "README 与发布说明在滚出再回看时不再回到顶部 — 测量到的高度现在会跨视口销毁保留。" ] }, { 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 8aafa1162..06d3f81db 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 @@ -91,6 +91,10 @@ sealed interface DetailsAction { data object ToggleWhatsNewExpanded : DetailsAction + data class OnAboutMeasured(val heightPx: Float) : DetailsAction + + data class OnWhatsNewMeasured(val heightPx: Float) : DetailsAction + data class TranslateAbout( val targetLanguageCode: String, ) : DetailsAction diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt index 4034bc689..088d028d2 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt @@ -562,6 +562,8 @@ fun DetailsScreen( isExpanded = state.isWhatsNewExpanded, onToggleExpanded = { onAction(DetailsAction.ToggleWhatsNewExpanded) }, collapsedHeight = collapsedSectionHeight, + measuredHeightPx = state.whatsNewMeasuredHeightPx, + onMeasured = { onAction(DetailsAction.OnWhatsNewMeasured(it)) }, translationState = state.whatsNewTranslation, onTranslateClick = { onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode)) @@ -582,6 +584,8 @@ fun DetailsScreen( isExpanded = state.isAboutExpanded, onToggleExpanded = { onAction(DetailsAction.ToggleAboutExpanded) }, collapsedHeight = collapsedSectionHeight, + measuredHeightPx = state.aboutMeasuredHeightPx, + onMeasured = { onAction(DetailsAction.OnAboutMeasured(it)) }, translationState = state.aboutTranslation, onTranslateClick = { onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode)) @@ -602,6 +606,8 @@ fun DetailsScreen( isExpanded = state.isAboutExpanded, onToggleExpanded = { onAction(DetailsAction.ToggleAboutExpanded) }, collapsedHeight = collapsedSectionHeight, + measuredHeightPx = state.aboutMeasuredHeightPx, + onMeasured = { onAction(DetailsAction.OnAboutMeasured(it)) }, translationState = state.aboutTranslation, onTranslateClick = { onAction(DetailsAction.TranslateAbout(state.deviceLanguageCode)) @@ -621,6 +627,8 @@ fun DetailsScreen( isExpanded = state.isWhatsNewExpanded, onToggleExpanded = { onAction(DetailsAction.ToggleWhatsNewExpanded) }, collapsedHeight = collapsedSectionHeight, + measuredHeightPx = state.whatsNewMeasuredHeightPx, + onMeasured = { onAction(DetailsAction.OnWhatsNewMeasured(it)) }, translationState = state.whatsNewTranslation, onTranslateClick = { onAction(DetailsAction.TranslateWhatsNew(state.deviceLanguageCode)) 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 108efa98f..aa6142c84 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 @@ -70,6 +70,12 @@ data class DetailsState( val isTrackingApp: Boolean = false, val isAboutExpanded: Boolean = false, val isWhatsNewExpanded: Boolean = false, + // Measured intrinsic heights of the rendered markdown blocks, hoisted + // out of the composable so LazyColumn item disposal/recompose doesn't + // re-trigger the measure → clip → reflow loop that snapped scroll + // position to the section start. + val aboutMeasuredHeightPx: Float? = null, + val whatsNewMeasuredHeightPx: Float? = null, val aboutTranslation: TranslationState = TranslationState(), val whatsNewTranslation: TranslationState = TranslationState(), val isLanguagePickerVisible: Boolean = false, 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 0e6c8b8eb..506a6d863 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 @@ -348,6 +348,7 @@ class DetailsViewModel( primaryAsset = primary, isVersionPickerVisible = false, whatsNewTranslation = TranslationState(), + whatsNewMeasuredHeightPx = null, ) } } @@ -370,6 +371,20 @@ class DetailsViewModel( } } + is DetailsAction.OnAboutMeasured -> { + val current = _state.value.aboutMeasuredHeightPx + if (current == null || action.heightPx > current) { + _state.update { it.copy(aboutMeasuredHeightPx = action.heightPx) } + } + } + + is DetailsAction.OnWhatsNewMeasured -> { + val current = _state.value.whatsNewMeasuredHeightPx + if (current == null || action.heightPx > current) { + _state.update { it.copy(whatsNewMeasuredHeightPx = action.heightPx) } + } + } + is DetailsAction.TranslateAbout -> { val readme = _state.value.readmeMarkdown ?: return aboutTranslationJob?.cancel() diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt index 561a2fda3..02acc32d7 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/About.kt @@ -1,12 +1,9 @@ package zed.rainxch.details.presentation.components.sections -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,16 +19,14 @@ 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.runtime.mutableStateOf +import androidx.compose.runtime.LaunchedEffect 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.clipToBounds import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -53,6 +48,8 @@ fun LazyListScope.about( isExpanded: Boolean, onToggleExpanded: () -> Unit, collapsedHeight: Dp, + measuredHeightPx: Float?, + onMeasured: (Float) -> Unit, translationState: TranslationState, onTranslateClick: () -> Unit, onLanguagePickerClick: () -> Unit, @@ -100,7 +97,7 @@ fun LazyListScope.about( } } - item { + item(key = "about_markdown") { val raw = if (translationState.isShowingTranslation && translationState.translatedText != null) { translationState.translatedText @@ -113,24 +110,19 @@ fun LazyListScope.about( zed.rainxch.core.domain.util.applyThemeAwareImages(raw, isDark) } - AnimatedContent( - targetState = displayContent, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - label = "about_content", - ) { content -> - ExpandableMarkdownContent( - content = content, - isExpanded = isExpanded, - onToggleExpanded = onToggleExpanded, - imageTransformer = MarkdownImageTransformer, - collapsedHeight = collapsedHeight, - fadeColor = MaterialTheme.colorScheme.background, - modifier = - Modifier - .fillMaxWidth() - .animateContentSize(), - ) - } + ExpandableMarkdownContent( + content = displayContent, + isExpanded = isExpanded, + onToggleExpanded = onToggleExpanded, + imageTransformer = MarkdownImageTransformer, + collapsedHeight = collapsedHeight, + measuredHeightPx = measuredHeightPx, + onMeasured = onMeasured, + fadeColor = MaterialTheme.colorScheme.background, + modifier = + Modifier + .fillMaxWidth(), + ) } } @@ -141,6 +133,8 @@ fun ExpandableMarkdownContent( onToggleExpanded: () -> Unit, imageTransformer: ImageTransformer, collapsedHeight: Dp, + measuredHeightPx: Float?, + onMeasured: (Float) -> Unit, fadeColor: Color, modifier: Modifier = Modifier, ) { @@ -150,21 +144,34 @@ fun ExpandableMarkdownContent( val flavour = remember { GFMFlavourDescriptor() } val collapsedHeightPx = with(density) { collapsedHeight.toPx() } - var contentHeightPx by remember(content, collapsedHeightPx) { mutableStateOf(0f) } - val needsExpansion = contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f + val effectiveHeight = measuredHeightPx ?: 0f + val needsExpansion = effectiveHeight > collapsedHeightPx && collapsedHeightPx > 0f + val measuredDp = + measuredHeightPx?.let { with(density) { it.toDp() } } + + val bringIntoViewRequester = remember { BringIntoViewRequester() } + LaunchedEffect(isExpanded) { + if (isExpanded) { + bringIntoViewRequester.bringIntoView() + } + } Column( - modifier = modifier.animateContentSize(), + modifier = modifier.bringIntoViewRequester(bringIntoViewRequester), ) { Box { Surface( color = Color.Transparent, contentColor = MaterialTheme.colorScheme.onBackground, modifier = - if (!isExpanded && needsExpansion) { - Modifier.heightIn(max = collapsedHeight).clipToBounds() - } else { - Modifier + when { + !isExpanded && needsExpansion -> + Modifier + .height(collapsedHeight) + .clipToBounds() + isExpanded && measuredDp != null -> + Modifier.heightIn(min = measuredDp) + else -> Modifier }, ) { val isDark = androidx.compose.foundation.isSystemInDarkTheme() @@ -179,10 +186,11 @@ fun ExpandableMarkdownContent( modifier = Modifier .fillMaxWidth() - .onGloballyPositioned { coordinates -> - val measured = coordinates.size.height.toFloat() - if (measured > contentHeightPx) { - contentHeightPx = measured + .onSizeChanged { size -> + val measured = size.height.toFloat() + val decisive = effectiveHeight > collapsedHeightPx + if (!decisive && measured > effectiveHeight) { + onMeasured(measured) } }, ) diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt index e52ddffdc..7d22ec271 100644 --- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt +++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/sections/WhatsNew.kt @@ -1,12 +1,9 @@ package zed.rainxch.details.presentation.components.sections -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -23,15 +20,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.LaunchedEffect 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.clipToBounds import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -52,6 +47,8 @@ fun LazyListScope.whatsNew( isExpanded: Boolean, onToggleExpanded: () -> Unit, collapsedHeight: Dp, + measuredHeightPx: Float?, + onMeasured: (Float) -> Unit, translationState: TranslationState, onTranslateClick: () -> Unit, onLanguagePickerClick: () -> Unit, @@ -121,7 +118,7 @@ fun LazyListScope.whatsNew( } } - item { + item(key = "whats_new_markdown") { Spacer(Modifier.height(12.dp)) ExpandableMarkdownContent( @@ -130,6 +127,8 @@ fun LazyListScope.whatsNew( collapsedHeight = collapsedHeight, isExpanded = isExpanded, onToggleExpanded = onToggleExpanded, + measuredHeightPx = measuredHeightPx, + onMeasured = onMeasured, ) } } @@ -141,6 +140,8 @@ private fun ExpandableMarkdownContent( collapsedHeight: Dp, isExpanded: Boolean, onToggleExpanded: () -> Unit, + measuredHeightPx: Float?, + onMeasured: (Float) -> Unit, ) { val raw = if (translationState.isShowingTranslation && translationState.translatedText != null) { @@ -160,87 +161,86 @@ private fun ExpandableMarkdownContent( val flavour = remember { GFMFlavourDescriptor() } val cardColor = MaterialTheme.colorScheme.surfaceContainerLow - AnimatedContent( - targetState = displayContent, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - label = "whats_new_content", - ) { content -> + val collapsedHeightPx = with(density) { collapsedHeight.toPx() } + val effectiveHeight = measuredHeightPx ?: 0f + val needsExpansion = effectiveHeight > collapsedHeightPx && collapsedHeightPx > 0f + val measuredDp = + measuredHeightPx?.let { with(density) { it.toDp() } } - val collapsedHeightPx = with(density) { collapsedHeight.toPx() } - var contentHeightPx by remember(content, collapsedHeightPx) { - mutableFloatStateOf(0f) + val bringIntoViewRequester = remember { BringIntoViewRequester() } + LaunchedEffect(isExpanded) { + if (isExpanded) { + bringIntoViewRequester.bringIntoView() } - val needsExpansion = - remember(contentHeightPx, collapsedHeightPx) { - contentHeightPx > collapsedHeightPx && collapsedHeightPx > 0f + } + + Column(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)) { + Box { + Box( + modifier = + when { + !isExpanded && needsExpansion -> + Modifier + .height(collapsedHeight) + .clipToBounds() + isExpanded && measuredDp != null -> + Modifier.heightIn(min = measuredDp) + else -> Modifier + }, + ) { + Markdown( + content = displayContent, + colors = colors, + typography = typography, + flavour = flavour, + imageTransformer = MarkdownImageTransformer, + components = zed.rainxch.details.presentation.markdown + .githubStoreMarkdownComponents(MarkdownImageTransformer, isDark), + modifier = + Modifier + .fillMaxWidth() + .onSizeChanged { size -> + val measured = size.height.toFloat() + val decisive = effectiveHeight > collapsedHeightPx + if (!decisive && measured > effectiveHeight) { + onMeasured(measured) + } + }, + ) } - Column( - modifier = Modifier.animateContentSize(), - ) { - Box { + if (!isExpanded && needsExpansion) { Box( modifier = - if (!isExpanded && needsExpansion) { - Modifier.heightIn(max = collapsedHeight).clipToBounds() - } else { - Modifier - }, - ) { - val isDark = androidx.compose.foundation.isSystemInDarkTheme() - Markdown( - content = content, - colors = colors, - typography = typography, - flavour = flavour, - imageTransformer = MarkdownImageTransformer, - components = zed.rainxch.details.presentation.markdown - .githubStoreMarkdownComponents(MarkdownImageTransformer, isDark), - modifier = - Modifier - .fillMaxWidth() - .onGloballyPositioned { coordinates -> - val measured = coordinates.size.height.toFloat() - if (measured > contentHeightPx) { - contentHeightPx = measured - } - }, - ) - } - - if (!isExpanded && needsExpansion) { - Box( - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(80.dp) - .background( - Brush.verticalGradient( - 0f to cardColor.copy(alpha = 0f), - 1f to cardColor, - ), + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(80.dp) + .background( + Brush.verticalGradient( + 0f to cardColor.copy(alpha = 0f), + 1f to cardColor, ), - ) - } + ), + ) } + } - if (needsExpansion) { - TextButton( - onClick = onToggleExpanded, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) { - Text( - text = - if (isExpanded) { - stringResource(Res.string.show_less) - } else { - stringResource(Res.string.read_more) - }, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - } + if (needsExpansion) { + TextButton( + onClick = onToggleExpanded, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Text( + text = + if (isExpanded) { + stringResource(Res.string.show_less) + } else { + stringResource(Res.string.read_more) + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) } } }