From 8b120438b1528e499ea8ea73bff1f850d35ddc62 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Sat, 23 May 2026 18:57:29 +0700 Subject: [PATCH 01/10] feat: make language selection bottom sheet scrollable - Refactored `AppearanceLanguageBottomSheet` in `AppearanceSettingsPage.kt` to use a `LazyColumn` instead of a standard `Column`. - Wrapped the sheet title inside an `item` block and used `itemsIndexed` to render the language options. - This resolves an issue where the language list was truncated and unscrollable, particularly in landscape mode or when the list of translations grows. --- .../settings/AppearanceSettingsPage.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt index bc3129824..abbf58e7f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppearanceSettingsPage.kt @@ -13,6 +13,8 @@ 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -213,21 +215,22 @@ private fun AppearanceLanguageBottomSheet( }, sheetState = sheetState, ) { - Column( + LazyColumn( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(0.dp), ) { - Text( - text = stringResource(Res.string.settings_appearance_app_language_sheet_title), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), - ) + item { + Text( + text = stringResource(Res.string.settings_appearance_app_language_sheet_title), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + ) + } - options.forEachIndexed { index, option -> + itemsIndexed(options) { index, option -> if (index > 0) { NuvioBottomSheetDivider() } From 452b3bb17307fee4126e586e60dd3e1532533d9c Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 10:13:32 +0700 Subject: [PATCH 02/10] fix(ios): enable animated GIF avatars on Profile Selection Screen - Replaced Coil's `AsyncImage` with `CollectionCardRemoteImage` inside `ProfileAvatarCard`. - Passed `animateIfPossible = true` to allow iOS to utilize the existing custom native GIF decoding engine used in collection poster. - Ensured zero behavioral impact on Android as the component maps back to Coil under the hood. --- .../nuvio/app/features/profiles/ProfileSelectionScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt index 195ba6748..272054d77 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt @@ -59,6 +59,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.nuvio.app.core.auth.AuthRepository import com.nuvio.app.core.auth.AuthState +import com.nuvio.app.features.home.components.CollectionCardRemoteImage import kotlinx.coroutines.delay import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* @@ -373,11 +374,12 @@ private fun ProfileAvatarCard( contentAlignment = Alignment.Center, ) { if (avatarImageUrl != null) { - AsyncImage( - model = avatarImageUrl, + CollectionCardRemoteImage( + imageUrl = avatarImageUrl, contentDescription = avatarItem?.displayName ?: profile.name, modifier = Modifier.size(100.dp).clip(CircleShape), contentScale = ContentScale.Crop, + animateIfPossible = true, ) } else if (profile.name.isNotBlank()) { Text( From 0a6807a80405c0f6ff4204e734940f8c6e9ba60d Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 10:17:49 +0700 Subject: [PATCH 03/10] fix(ios): enable animated GIF avatars in Profile Switcher Tab - Integrated `CollectionCardRemoteImage` into `PopupProfileBubble` and `ActiveProfileMiniAvatar`. - Set `animateIfPossible = true` to ensure consistent GIF playback within the bottom navigation tab and popup switcher on iOS. - Maintained existing layout sizes and clip shapes for visual consistency. --- .../nuvio/app/features/profiles/ProfileSwitcherTab.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index a399c8220..3facba80f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -69,6 +69,7 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage +import com.nuvio.app.features.home.components.CollectionCardRemoteImage import com.nuvio.app.isIos import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -504,11 +505,12 @@ private fun PopupProfileBubble( contentAlignment = Alignment.Center, ) { if (avatarImageUrl != null) { - AsyncImage( - model = avatarImageUrl, + CollectionCardRemoteImage( + imageUrl = avatarImageUrl, contentDescription = profile.name, modifier = Modifier.size(48.dp).clip(CircleShape), contentScale = ContentScale.Crop, + animateIfPossible = true, ) } else if (profile.name.isNotBlank()) { Text( @@ -818,11 +820,12 @@ fun ActiveProfileMiniAvatar( contentAlignment = Alignment.Center, ) { if (avatarImageUrl != null) { - AsyncImage( - model = avatarImageUrl, + CollectionCardRemoteImage( + imageUrl = avatarImageUrl, contentDescription = profile.name, modifier = Modifier.size(size.dp).clip(CircleShape), contentScale = ContentScale.Crop, + animateIfPossible = true, ) } else if (profile.name.isNotBlank()) { Text( From d6c44b3c071da2f02df5fde82346b0c7a388ea0f Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 10:22:41 +0700 Subject: [PATCH 04/10] fix(ios): enable animated GIF preview on Profile Edit Screen - Updated `ProfileIdentityCard` to use `CollectionCardRemoteImage`. - Configured `animateIfPossible = true` to provide real-time animated previews during profile creation or customization on iOS. --- .../com/nuvio/app/features/profiles/ProfileEditScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt index 5f00697d7..0dab3a422 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt @@ -54,6 +54,7 @@ import com.nuvio.app.core.ui.NuvioScreen import com.nuvio.app.core.ui.NuvioScreenHeader import com.nuvio.app.core.ui.NuvioStatusModal import com.nuvio.app.core.ui.NuvioSurfaceCard +import com.nuvio.app.features.home.components.CollectionCardRemoteImage import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -408,11 +409,12 @@ private fun ProfileIdentityCard( contentAlignment = Alignment.Center, ) { if (customAvatarUrl != null) { - AsyncImage( - model = customAvatarUrl, + CollectionCardRemoteImage( + imageUrl = customAvatarUrl, contentDescription = name, modifier = Modifier.size(88.dp).clip(CircleShape), contentScale = ContentScale.Crop, + animateIfPossible = true, ) } else if (selectedAvatar != null) { AsyncImage( From ee9a63f27b4987b78228185d6ea5a102ea037fed Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 13:58:29 +0700 Subject: [PATCH 05/10] fix: correct touch area offset on Profile Edit Screen - Removed redundant `clip` and `size` modifiers from `CollectionCardRemoteImage` in `ProfileIdentityCard` and `AvatarChoiceItem`. - Used `Modifier.fillMaxSize()` to inherit parent shape constraints, fixing the touch offset issue on iOS devices without affecting Android. --- .../kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt index 0dab3a422..428ac271a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt @@ -420,7 +420,7 @@ private fun ProfileIdentityCard( AsyncImage( model = avatarStorageUrl(selectedAvatar.storagePath), contentDescription = selectedAvatar.displayName, - modifier = Modifier.size(88.dp).clip(CircleShape), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, ) } else if (name.isNotBlank()) { From 6f5c865a552353c0261f132103260e8c4a23f328 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 14:00:11 +0700 Subject: [PATCH 06/10] fix: correct touch area offset on Profile Selection Screen - Replaced explicit `size` and `clip` modifiers with `Modifier.fillMaxSize()` on `CollectionCardRemoteImage`. - Resolved an iOS-specific bug where extra Compose clipping layers caused the UIKitView hit-box to shift away from its visual bounds. - Restored accurate touch targets by relying on the parent Box's constraints. --- .../com/nuvio/app/features/profiles/ProfileSelectionScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt index 272054d77..ec8a99a8a 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSelectionScreen.kt @@ -377,7 +377,7 @@ private fun ProfileAvatarCard( CollectionCardRemoteImage( imageUrl = avatarImageUrl, contentDescription = avatarItem?.displayName ?: profile.name, - modifier = Modifier.size(100.dp).clip(CircleShape), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, animateIfPossible = true, ) From 2eaab2f5455930780b822a86449dc6f05c44127a Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 14:01:49 +0700 Subject: [PATCH 07/10] fix: correct touch area offset in Profile Switcher Tab - Applied `Modifier.fillMaxSize()` to `CollectionCardRemoteImage` inside `PopupProfileBubble` and `ActiveProfileMiniAvatar`. - Removed redundant clipping layers that displaced the touch area for Native iOS views within the bottom navigation and popup switcher. --- .../com/nuvio/app/features/profiles/ProfileSwitcherTab.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index 3facba80f..a51ac840f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -508,7 +508,7 @@ private fun PopupProfileBubble( CollectionCardRemoteImage( imageUrl = avatarImageUrl, contentDescription = profile.name, - modifier = Modifier.size(48.dp).clip(CircleShape), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, animateIfPossible = true, ) @@ -823,7 +823,7 @@ fun ActiveProfileMiniAvatar( CollectionCardRemoteImage( imageUrl = avatarImageUrl, contentDescription = profile.name, - modifier = Modifier.size(size.dp).clip(CircleShape), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, animateIfPossible = true, ) From fd1cbf45ff385af0ed2631eda9ea946ce5543d6e Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 14:06:25 +0700 Subject: [PATCH 08/10] fix: correct touch area offset on Profile Edit Screen - Removed redundant `clip` and `size` modifiers from `CollectionCardRemoteImage` in `ProfileIdentityCard` and `AvatarChoiceItem`. - Used `Modifier.fillMaxSize()` to inherit parent shape constraints, fixing the touch offset issue on iOS devices without affecting Android. --- .../com/nuvio/app/features/profiles/ProfileEditScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt index 428ac271a..f8e0ca9b3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileEditScreen.kt @@ -412,7 +412,7 @@ private fun ProfileIdentityCard( CollectionCardRemoteImage( imageUrl = customAvatarUrl, contentDescription = name, - modifier = Modifier.size(88.dp).clip(CircleShape), + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, animateIfPossible = true, ) @@ -420,7 +420,7 @@ private fun ProfileIdentityCard( AsyncImage( model = avatarStorageUrl(selectedAvatar.storagePath), contentDescription = selectedAvatar.displayName, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.size(88.dp).clip(CircleShape), contentScale = ContentScale.Crop, ) } else if (name.isNotBlank()) { From 78bf7ffe103956ea5a1c38a42cbc625f2848dec7 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 14:36:02 +0700 Subject: [PATCH 09/10] fix: add missing layout import in ProfileSwitcherTab - Added the missing `androidx.compose.foundation.layout.fillMaxSize` import statement. - Resolves the "Unresolved reference 'fillMaxSize'" compilation failure during the iOS build task (`compileKotlinIosArm64`). --- .../kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index a51ac840f..46c38992f 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding From 2a06b8e9182c776380c2aa182ae334958de9f2e4 Mon Sep 17 00:00:00 2001 From: Luqman Fadlli <75630095+luqmanfadlli@users.noreply.github.com> Date: Tue, 26 May 2026 15:46:19 +0700 Subject: [PATCH 10/10] fix(ios): fix touch area offset for animated Native UI components - Added `interactive = false` to the `UIKitView` in `CollectionCardRemoteImage.ios.kt`. - Resolved an iOS-specific Compose Multiplatform bug where the native touch wrapper becomes misaligned from its visual bounds during `graphicsLayer` animations (e.g., scale or translation). - By disabling the native interactive wrapper, Compose safely takes over the hit-testing, ensuring that the touch targets are perfectly synced and accurate. --- .../features/home/components/CollectionCardRemoteImage.ios.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/components/CollectionCardRemoteImage.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/components/CollectionCardRemoteImage.ios.kt index 7f1e5c69e..54cb6af12 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/components/CollectionCardRemoteImage.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/features/home/components/CollectionCardRemoteImage.ios.kt @@ -96,6 +96,7 @@ internal actual fun CollectionCardRemoteImage( UIKitView( modifier = modifier, + interactive = false, factory = { UIImageView().apply { contentMode = UIViewContentMode.UIViewContentModeScaleAspectFill