@momo-kits/native-kits 0.167.1-duo-search.3-debug → 0.167.1-duo-search.22-debug
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +12 -9
- package/README.md +2 -2
- package/compose/build.gradle.kts +1 -1
- package/compose/compose.podspec +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +228 -73
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +23 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +95 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +56 -37
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +28 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +62 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +17 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +39 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +33 -26
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +11 -3
- package/gradle.properties +1 -1
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/Input/InputSearch.swift +5 -2
- package/ios/Layout/GridContext.swift +67 -3
- package/ios/Layout/ScreenLayout.swift +101 -33
- package/ios/Navigation/BottomSheet.swift +83 -36
- package/ios/Navigation/BottomTab/BottomTab.swift +165 -20
- package/ios/Navigation/BottomTab/BottomTabBar.swift +3 -0
- package/ios/Navigation/Component/FloatingButton.swift +29 -51
- package/ios/Navigation/Component/Header.swift +70 -18
- package/ios/Navigation/Component/HeaderBackground.swift +0 -1
- package/ios/Navigation/Component/HeaderRight.swift +107 -68
- package/ios/Navigation/Component/SideBar.swift +15 -8
- package/ios/Navigation/Component/SnackBar.swift +3 -3
- package/ios/Navigation/HostBackGestureBridge.swift +27 -6
- package/ios/Navigation/ModalScreen.swift +2 -2
- package/ios/Navigation/Navigation.swift +14 -0
- package/ios/Navigation/NavigationContainer.swift +29 -58
- package/ios/Navigation/Navigator.swift +132 -181
- package/ios/Navigation/StackScreen.swift +217 -65
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/package.json +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/ComposeApi.swift +0 -23
- package/ios/Application/LegacyHeader.swift +0 -212
- package/ios/Application/Screen.swift +0 -251
package/CLAUDE.md
CHANGED
|
@@ -18,7 +18,7 @@ Guidance for working in this repo. Keep it accurate — update it when structure
|
|
|
18
18
|
## Source layout (`compose/src/commonMain/kotlin/vn/momo/kits/`)
|
|
19
19
|
|
|
20
20
|
- `components/` — ~50 UI components, **one file per component** (`Button.kt`, `Input*.kt`, `PopupNotify.kt`, …).
|
|
21
|
-
- `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `
|
|
21
|
+
- `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `LiteScreen.kt` (standalone lightweight screen with its own header), `WidgetContainer.kt`.
|
|
22
22
|
- `navigation/` — `NavigationContainer.kt` (**canonical entry point**), `Navigator.kt`, `Navigation.kt`, `StackScreen.kt`, `bottomtab/`, `component/`, `tracking/`.
|
|
23
23
|
- `const/` — design tokens: `Theme.kt`, `Colors.kt`, `Typography.kt`, `Spacing.kt`, `Radius.kt`.
|
|
24
24
|
- `layout/`, `modifier/`, `utils/`.
|
|
@@ -46,7 +46,7 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
46
46
|
|
|
47
47
|
## Architecture essentials
|
|
48
48
|
|
|
49
|
-
- **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below.
|
|
49
|
+
- **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. The legacy `Screen` / `ApplicationContainer` / `ComposeApi` stack has been removed on both sides — don't reintroduce it.
|
|
50
50
|
- **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
|
|
51
51
|
- **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
|
|
52
52
|
- **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
|
|
@@ -56,11 +56,13 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
56
56
|
|
|
57
57
|
## SwiftUI kit (`ios/`)
|
|
58
58
|
|
|
59
|
-
- **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize
|
|
60
|
-
- **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders
|
|
59
|
+
- **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `StackTransition`, `SwipeBackHaptic`, `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*` and `MaxApi` (Compose `application/`). Change a navigation piece on both sides together.
|
|
60
|
+
- **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders. New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
|
|
61
61
|
- Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
|
|
62
|
-
-
|
|
63
|
-
- `
|
|
62
|
+
- Everything, navigation shell included, supports the pod's minimum **iOS 15** — no `@available` gates on public types. iOS 16+ APIs only behind `#available` with an iOS 15 path (`Layout`: `BottomSheet` `CapHeight`, `GridFlow` → `_VariadicView` flow row).
|
|
63
|
+
- **Usage is `NavigationContainer` + `Navigator` + `Navigation` only**; the kit owns the stack, transitions, overplays and back gesture. There is no `NavigationStack`: `StackTransition.swift` holds `StackHost` (representable) + `StackController`, the NavHost counterpart. Each entry gets its own `UIHostingController`, kept while on the stack and removed from the window while covered (state kept, onDisappear / onAppear like UINavigationController). Motion is a SwiftUI `offset` per card driven by one `coverage` value on a display link (a UIKit transform would move the safe area); it is Compose `StackTransition` 1:1 (RN `forStackTransitionWithOverlay`: linear 300ms slide, 0.3 parallax, `pink09` scrim to 0.6, 8pt edge shadow to 0.3). Card roots get the container environment via `.environment(\.self, …)` with the kit environment re-applied on top, so host environment objects reach screens.
|
|
64
|
+
- One back stack, root first (`Navigator.entries`, Compose's back stack): `present` appends a dialog entry that rises (FastOutSlowIn 300ms) and later pushes stack on top of it; `dismiss` drops the topmost dialog and everything above it (slides down whole); `popToRoot` returns to the active flow's root. Overplays (modal / bottom sheet / snackbar) render inside the screen they were shown over (`OverplayItem.screenId`, Compose `currentRootId`), so a later push slides over them.
|
|
65
|
+
- Host back gesture: `HostBackGestureBridge.started` / `progressed` / `completed` / `cancelled` scrub the pop (`Navigator.backStarted` …) only for a plain pop; tab back, `onBackHandler`, `preventBack`, an open overplay or a presented top wait for the release and go through `onBackSafe`. `SwipeBackHaptic` ticks at 50pt (`MaxApi.triggerEventVibration`, default = system light impact).
|
|
64
66
|
- Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
|
|
65
67
|
- Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
|
|
66
68
|
|
|
@@ -68,11 +70,12 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
|
|
|
68
70
|
|
|
69
71
|
- `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
|
|
70
72
|
- iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (short side of screen or window > 440, RN `device.ts`) drives popups, modal and bottom sheet.
|
|
71
|
-
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display in portrait
|
|
73
|
+
- Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
|
|
74
|
+
- iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
|
|
72
75
|
- iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
|
|
73
76
|
- Reserved regions (iOS 27.1 `reservedRegionsOfKind:`, reached through the ObjC runtime so older SDKs still build): a probe `UIView` (`platform/ReservedRegions.ios.kt` on the Compose root view; `ReservedRegionsProbe` inside SwiftUI `momoScreenLayout()`) re-reads them in `layoutSubviews`, which UIKit re-runs when they change. They push `sideBar.top` below the status cluster as it grows for calls / Live Activities, and `sideBar.bottom` (≥ 24pt) above a camera at the bottom end of the strip.
|
|
74
77
|
- Tab bar on Duo is `bottomtab/DuoTabBar.kt`: a rail in the side strip (`sideBar.bottom` above the window bottom, QR below it) or, without a side bar, a floating bar spanning 6/8 grid columns. Both overlay the content; `duoFloatingTabBarInset` pads tab content, footer and FAB.
|
|
75
|
-
- Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height so the decision follows each screen's toolkit.
|
|
78
|
+
- Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height (0 when the screen puts nothing there) so the decision follows each screen's toolkit.
|
|
76
79
|
- Glass header: on Duo with a side bar, Default/Extended headers without search and with a `ScrollState` let content scroll under the bar (`usesDuoGlassHeader`); the bar fades in a translucent surface + hairline over 16pt (Compose has no backdrop blur — RN's fallback look), the gradient leaves with the content.
|
|
77
80
|
- Quick return header: glass headers and Animated headers (with a `ScrollState`) on the Duo side bar layout slide out with the content and back in on a reverse scroll; a half-shown header settles in or out once the scroll stops. The side bar is drawn apart from the header, so it stays put.
|
|
78
81
|
- Overlays on Duo: popups cap at 440pt wide and the room between `duoTopInset` and the bottom inset (body scrolls, actions stay); modal centers inside that room; bottom sheet uses the window height, centers at width ≥ 600 and pads by side insets in landscape.
|
|
@@ -127,7 +130,7 @@ Prefer one tight line at the surprising spot over a paragraph. Match the (low) c
|
|
|
127
130
|
## Gotchas
|
|
128
131
|
|
|
129
132
|
- **Resource qualifier drift:** the generated Compose `Res` namespace is `vn.momo.uikits.resources`, but `utils/Resources.kt` and `platform/Platform.ios.kt` reference `vn.momo.compose.resources`. Keep bundled strings/dictionaries as plain Kotlin (the `defaultLanguage` map in `Localize.kt`), not `composeResources/values`, to avoid iOS framework resource-loading issues.
|
|
130
|
-
-
|
|
133
|
+
- `LiteScreen` keeps its own `application.HeaderType` enum (`DEFAULT`/`EXTENDED`/`NONE`), unrelated to the sealed `navigation.component.HeaderType`; its toolkit is the navigation `Toolkit` (through `LocalMaxApi`).
|
|
131
134
|
- iOS strings/maps must survive Kotlin/Native framework packaging — keep them compiled-in, not loaded from resource bundles.
|
|
132
135
|
|
|
133
136
|
## Code comments
|
package/README.md
CHANGED
|
@@ -150,8 +150,8 @@ themeController(defaultTheme.copy(colors = defaultTheme.colors.copy(primary = Co
|
|
|
150
150
|
---
|
|
151
151
|
|
|
152
152
|
### Lưu ý khác
|
|
153
|
-
- `Screen`
|
|
154
|
-
- `
|
|
153
|
+
- `Screen` / `ApplicationContainer` cũ (Compose và SwiftUI) đã bị gỡ; dùng `NavigationContainer + StackScreen`.
|
|
154
|
+
- `KitDesignSystem.update(isWhiteList = …)` (`const/KitDesignSystem.kt`) bật font design system mới cho host trong whitelist; đừng kích hoạt nếu không cần.
|
|
155
155
|
- Khi thêm tài nguyên mới, đặt trong `shared/src/commonMain/composeResources` để Compose Multiplatform plugin sinh accessor tự động.
|
|
156
156
|
- Repo không bundle SDK bên thứ ba ngoại trừ `vn.momo.corex.maxapi`, Compose/Coil/Ktor **và Compottie** cho animation.
|
|
157
157
|
|
package/compose/build.gradle.kts
CHANGED
package/compose/compose.podspec
CHANGED
|
@@ -20,6 +20,7 @@ import androidx.compose.foundation.text.KeyboardActions
|
|
|
20
20
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
21
21
|
import androidx.compose.foundation.verticalScroll
|
|
22
22
|
import androidx.compose.runtime.Composable
|
|
23
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
23
24
|
import androidx.compose.runtime.LaunchedEffect
|
|
24
25
|
import androidx.compose.runtime.MutableState
|
|
25
26
|
import androidx.compose.runtime.Stable
|
|
@@ -27,16 +28,14 @@ import androidx.compose.runtime.State
|
|
|
27
28
|
import androidx.compose.runtime.derivedStateOf
|
|
28
29
|
import androidx.compose.runtime.getValue
|
|
29
30
|
import androidx.compose.runtime.mutableStateOf
|
|
30
|
-
import androidx.compose.runtime.produceState
|
|
31
31
|
import androidx.compose.runtime.remember
|
|
32
32
|
import androidx.compose.runtime.rememberUpdatedState
|
|
33
|
-
import androidx.compose.runtime.snapshotFlow
|
|
34
33
|
import androidx.compose.runtime.staticCompositionLocalOf
|
|
35
34
|
import androidx.compose.ui.Alignment
|
|
36
35
|
import androidx.compose.ui.Modifier
|
|
37
36
|
import androidx.compose.ui.composed
|
|
38
37
|
import androidx.compose.ui.draw.clip
|
|
39
|
-
import androidx.compose.ui.draw.
|
|
38
|
+
import androidx.compose.ui.draw.drawWithCache
|
|
40
39
|
import androidx.compose.ui.draw.drawWithContent
|
|
41
40
|
import androidx.compose.ui.geometry.Offset
|
|
42
41
|
import androidx.compose.ui.geometry.Rect
|
|
@@ -77,13 +76,20 @@ import vn.momo.kits.layout.DuoMetrics
|
|
|
77
76
|
import vn.momo.kits.layout.ScreenLayout
|
|
78
77
|
import vn.momo.kits.layout.SideBarEdge
|
|
79
78
|
import vn.momo.kits.layout.getScreenLayout
|
|
79
|
+
import vn.momo.kits.modifier.hideKeyboardOnTap
|
|
80
80
|
import vn.momo.kits.utils.horizontalSafeAreaPadding
|
|
81
81
|
import vn.momo.kits.modifier.kitsAutomationId
|
|
82
82
|
import vn.momo.kits.modifier.noFeedbackClickable
|
|
83
83
|
import vn.momo.kits.modifier.setAutomationId
|
|
84
|
-
import vn.momo.kits.navigation.component.
|
|
84
|
+
import vn.momo.kits.navigation.component.headerFontFamily
|
|
85
85
|
import vn.momo.kits.navigation.component.HEADER_HEIGHT
|
|
86
|
+
import vn.momo.kits.navigation.component.HeaderColor
|
|
87
|
+
import vn.momo.kits.navigation.component.LocalHeaderColor
|
|
88
|
+
import vn.momo.kits.navigation.component.HeaderRight
|
|
86
89
|
import vn.momo.kits.navigation.component.SideBarButton
|
|
90
|
+
import vn.momo.kits.navigation.component.TitlePosition
|
|
91
|
+
import vn.momo.kits.navigation.component.ToolGroup
|
|
92
|
+
import vn.momo.kits.navigation.component.Toolkit
|
|
87
93
|
import vn.momo.kits.utils.getAppStatusBarHeight
|
|
88
94
|
import kotlin.math.max
|
|
89
95
|
import kotlin.math.roundToInt
|
|
@@ -112,6 +118,30 @@ data class AnimationOption(
|
|
|
112
118
|
|
|
113
119
|
val LocalAnimationOption = staticCompositionLocalOf<AnimationOption?> { null }
|
|
114
120
|
|
|
121
|
+
// LiteScreen's own header kinds; StackScreen uses the sealed navigation.component.HeaderType.
|
|
122
|
+
enum class HeaderType {
|
|
123
|
+
DEFAULT,
|
|
124
|
+
EXTENDED,
|
|
125
|
+
NONE
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Stable
|
|
129
|
+
data class HeaderRightData(
|
|
130
|
+
val useShortcut: Boolean = false,
|
|
131
|
+
val useMore: Boolean = false,
|
|
132
|
+
val useSystemTools: Boolean = true,
|
|
133
|
+
val tools: List<ToolGroup> = emptyList(),
|
|
134
|
+
val toolCallback: ((String) -> Unit)? = { _ -> }
|
|
135
|
+
) {
|
|
136
|
+
internal fun toToolkit() = HeaderRight.Toolkit(
|
|
137
|
+
useShortcut = useShortcut,
|
|
138
|
+
useMore = useMore,
|
|
139
|
+
useSystemTools = useSystemTools,
|
|
140
|
+
tools = tools,
|
|
141
|
+
toolCallback = toolCallback,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
@Composable
|
|
116
146
|
fun LiteScreen(
|
|
117
147
|
scrollable: Boolean = true,
|
|
@@ -245,18 +275,16 @@ private fun LiteScreenLayout(
|
|
|
245
275
|
borderColor = Colors.black_20.copy(alpha = 0.2f)
|
|
246
276
|
)
|
|
247
277
|
}
|
|
278
|
+
val toolkit = remember(headerRightData) { (headerRightData ?: HeaderRightData()).toToolkit() }
|
|
248
279
|
|
|
249
|
-
val scrollPercentage =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
val rangePx = with(density) { backgroundHeight.toPx() }
|
|
258
|
-
snapshotFlow { scrollState.value }.collectLatest {
|
|
259
|
-
value = (it / rangePx).coerceIn(0f, 1f)
|
|
280
|
+
val scrollPercentage = remember(useAnimationSearch, scrollState, backgroundHeight, density) {
|
|
281
|
+
derivedStateOf {
|
|
282
|
+
if (!useAnimationSearch || scrollState == null) {
|
|
283
|
+
0f
|
|
284
|
+
} else {
|
|
285
|
+
val rangePx = with(density) { backgroundHeight.toPx() }
|
|
286
|
+
(scrollState.value / rangePx).coerceIn(0f, 1f)
|
|
287
|
+
}
|
|
260
288
|
}
|
|
261
289
|
}
|
|
262
290
|
|
|
@@ -335,25 +363,27 @@ private fun LiteScreenLayout(
|
|
|
335
363
|
modifier = Modifier
|
|
336
364
|
.layoutId(HeaderId.BACKGROUND_ID)
|
|
337
365
|
.then(animationOpacityModifier)
|
|
338
|
-
.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
),
|
|
355
|
-
)
|
|
366
|
+
.drawWithCache {
|
|
367
|
+
if (headerBackgroundColor != null) {
|
|
368
|
+
val backgroundColor = headerBackgroundColor
|
|
369
|
+
onDrawBehind { drawRect(color = backgroundColor) }
|
|
370
|
+
} else {
|
|
371
|
+
val headerHeight = max(
|
|
372
|
+
HeaderId.EXTENDED_HEADER_HEIGHT.toPx(),
|
|
373
|
+
size.height,
|
|
374
|
+
)
|
|
375
|
+
val gradientBrush = Brush.linearGradient(
|
|
376
|
+
colors = listGradientColors,
|
|
377
|
+
start = Offset.Zero,
|
|
378
|
+
end = Offset(
|
|
379
|
+
x = 0f,
|
|
380
|
+
y = headerHeight * (1 - scrollPercentage.value),
|
|
381
|
+
),
|
|
356
382
|
)
|
|
383
|
+
onDrawBehind {
|
|
384
|
+
drawRect(color = Colors.black_01)
|
|
385
|
+
drawRect(brush = gradientBrush)
|
|
386
|
+
}
|
|
357
387
|
}
|
|
358
388
|
},
|
|
359
389
|
)
|
|
@@ -408,11 +438,14 @@ private fun LiteScreenLayout(
|
|
|
408
438
|
.then(animationOpacityModifier)
|
|
409
439
|
) {
|
|
410
440
|
if (headerRight != null) {
|
|
411
|
-
|
|
441
|
+
CompositionLocalProvider(LocalHeaderColor provides headerColor) {
|
|
442
|
+
headerRight()
|
|
443
|
+
}
|
|
412
444
|
} else {
|
|
413
|
-
|
|
414
|
-
headerRight =
|
|
445
|
+
Toolkit(
|
|
446
|
+
headerRight = toolkit,
|
|
415
447
|
tintColor = tintColor,
|
|
448
|
+
headerColor = headerColor,
|
|
416
449
|
)
|
|
417
450
|
}
|
|
418
451
|
}
|
|
@@ -429,7 +462,7 @@ private fun LiteScreenLayout(
|
|
|
429
462
|
text = title,
|
|
430
463
|
color = headerColor.tintIconColor,
|
|
431
464
|
style = titleStyle,
|
|
432
|
-
fontFamily =
|
|
465
|
+
fontFamily = headerFontFamily,
|
|
433
466
|
modifier = titleModifier,
|
|
434
467
|
maxLines = 1,
|
|
435
468
|
overflow = TextOverflow.Ellipsis
|
|
@@ -440,6 +473,125 @@ private fun LiteScreenLayout(
|
|
|
440
473
|
)
|
|
441
474
|
}
|
|
442
475
|
|
|
476
|
+
private interface LiteScreenGeometry {
|
|
477
|
+
fun MeasureScope.safeWidth(
|
|
478
|
+
screenLayout: ScreenLayout,
|
|
479
|
+
width: Int,
|
|
480
|
+
insetLeft: Int,
|
|
481
|
+
insetRight: Int,
|
|
482
|
+
): Int
|
|
483
|
+
|
|
484
|
+
fun MeasureScope.searchStartX(
|
|
485
|
+
screenLayout: ScreenLayout,
|
|
486
|
+
insetLeft: Int,
|
|
487
|
+
spacing: Int,
|
|
488
|
+
): Int
|
|
489
|
+
|
|
490
|
+
fun MeasureScope.headerHeight(screenLayout: ScreenLayout, statusBar: Int, rowHeight: Int): Int
|
|
491
|
+
fun MeasureScope.headerRightStartX(
|
|
492
|
+
screenLayout: ScreenLayout,
|
|
493
|
+
width: Int,
|
|
494
|
+
insetRight: Int,
|
|
495
|
+
spacing: Int,
|
|
496
|
+
rightWidth: Int,
|
|
497
|
+
): Int
|
|
498
|
+
|
|
499
|
+
fun MeasureScope.collapsedHeaderHeight(screenLayout: ScreenLayout, statusBar: Int, rowHeight: Int): Int
|
|
500
|
+
|
|
501
|
+
fun MeasureScope.searchStartY(
|
|
502
|
+
screenLayout: ScreenLayout,
|
|
503
|
+
headerY: Int,
|
|
504
|
+
statusBar: Int,
|
|
505
|
+
rowHeight: Int,
|
|
506
|
+
): Int
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private class StandardPhoneLiteScreenGeometry : LiteScreenGeometry {
|
|
510
|
+
override fun MeasureScope.safeWidth(
|
|
511
|
+
screenLayout: ScreenLayout,
|
|
512
|
+
width: Int,
|
|
513
|
+
insetLeft: Int,
|
|
514
|
+
insetRight: Int,
|
|
515
|
+
) = (width - insetLeft - insetRight).coerceAtLeast(0)
|
|
516
|
+
|
|
517
|
+
override fun MeasureScope.searchStartX(screenLayout: ScreenLayout, insetLeft: Int, spacing: Int) =
|
|
518
|
+
insetLeft + spacing
|
|
519
|
+
|
|
520
|
+
override fun MeasureScope.headerHeight(screenLayout: ScreenLayout, statusBar: Int, rowHeight: Int) =
|
|
521
|
+
statusBar + rowHeight
|
|
522
|
+
override fun MeasureScope.collapsedHeaderHeight(
|
|
523
|
+
screenLayout: ScreenLayout,
|
|
524
|
+
statusBar: Int,
|
|
525
|
+
rowHeight: Int,
|
|
526
|
+
) = statusBar + rowHeight
|
|
527
|
+
override fun MeasureScope.headerRightStartX(
|
|
528
|
+
screenLayout: ScreenLayout,
|
|
529
|
+
width: Int,
|
|
530
|
+
insetRight: Int,
|
|
531
|
+
spacing: Int,
|
|
532
|
+
rightWidth: Int,
|
|
533
|
+
) = width - insetRight - spacing - rightWidth
|
|
534
|
+
|
|
535
|
+
override fun MeasureScope.searchStartY(
|
|
536
|
+
screenLayout: ScreenLayout,
|
|
537
|
+
headerY: Int,
|
|
538
|
+
statusBar: Int,
|
|
539
|
+
rowHeight: Int,
|
|
540
|
+
) = headerY + statusBar
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private class IPhoneDuoLiteScreenGeometry : LiteScreenGeometry {
|
|
544
|
+
override fun MeasureScope.safeWidth(
|
|
545
|
+
screenLayout: ScreenLayout,
|
|
546
|
+
width: Int,
|
|
547
|
+
insetLeft: Int,
|
|
548
|
+
insetRight: Int,
|
|
549
|
+
): Int {
|
|
550
|
+
val clearance = screenLayout.statusBarClearance
|
|
551
|
+
return (
|
|
552
|
+
width - insetLeft - insetRight -
|
|
553
|
+
(clearance?.left?.roundToPx() ?: 0) -
|
|
554
|
+
(clearance?.right?.roundToPx() ?: 0)
|
|
555
|
+
).coerceAtLeast(0)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
override fun MeasureScope.searchStartX(screenLayout: ScreenLayout, insetLeft: Int, spacing: Int) =
|
|
559
|
+
insetLeft + (screenLayout.statusBarClearance?.left?.roundToPx() ?: 0) + spacing
|
|
560
|
+
|
|
561
|
+
override fun MeasureScope.headerHeight(screenLayout: ScreenLayout, statusBar: Int, rowHeight: Int): Int {
|
|
562
|
+
val rowTop = screenLayout.headerStatusBarHeight +
|
|
563
|
+
(screenLayout.headerBarHeight - rowHeight.toDp()) / 2
|
|
564
|
+
return max(screenLayout.headerHeight.roundToPx(), rowTop.roundToPx() + rowHeight)
|
|
565
|
+
}
|
|
566
|
+
override fun MeasureScope.collapsedHeaderHeight(
|
|
567
|
+
screenLayout: ScreenLayout,
|
|
568
|
+
statusBar: Int,
|
|
569
|
+
rowHeight: Int,
|
|
570
|
+
) = (
|
|
571
|
+
screenLayout.headerStatusBarHeight +
|
|
572
|
+
(screenLayout.headerBarHeight - rowHeight.toDp()) / 2
|
|
573
|
+
).roundToPx() + rowHeight
|
|
574
|
+
override fun MeasureScope.headerRightStartX(
|
|
575
|
+
screenLayout: ScreenLayout,
|
|
576
|
+
width: Int,
|
|
577
|
+
insetRight: Int,
|
|
578
|
+
spacing: Int,
|
|
579
|
+
rightWidth: Int,
|
|
580
|
+
) = width - insetRight -
|
|
581
|
+
(screenLayout.statusBarClearance?.right?.roundToPx() ?: 0) -
|
|
582
|
+
spacing - rightWidth
|
|
583
|
+
|
|
584
|
+
override fun MeasureScope.searchStartY(
|
|
585
|
+
screenLayout: ScreenLayout,
|
|
586
|
+
headerY: Int,
|
|
587
|
+
statusBar: Int,
|
|
588
|
+
rowHeight: Int,
|
|
589
|
+
) = headerY + (
|
|
590
|
+
screenLayout.headerStatusBarHeight +
|
|
591
|
+
(screenLayout.headerBarHeight - rowHeight.toDp()) / 2
|
|
592
|
+
).roundToPx()
|
|
593
|
+
}
|
|
594
|
+
|
|
443
595
|
private class LiteScreenLayoutPolicy(
|
|
444
596
|
private val screenLayout: ScreenLayout,
|
|
445
597
|
private val applySafeAreaPadding: Boolean,
|
|
@@ -454,6 +606,11 @@ private class LiteScreenLayoutPolicy(
|
|
|
454
606
|
private val verticalArrangement: Arrangement.Vertical,
|
|
455
607
|
private val horizontalAlignment: Alignment.Horizontal,
|
|
456
608
|
) : MeasurePolicy {
|
|
609
|
+
private val geometry = if (screenLayout.isIPhoneDuo) {
|
|
610
|
+
IPhoneDuoLiteScreenGeometry()
|
|
611
|
+
} else {
|
|
612
|
+
StandardPhoneLiteScreenGeometry()
|
|
613
|
+
}
|
|
457
614
|
|
|
458
615
|
val searchStartPosition: IntOffset by lazy {
|
|
459
616
|
if (animationOption == null) return@lazy IntOffset.Zero
|
|
@@ -478,12 +635,14 @@ private class LiteScreenLayoutPolicy(
|
|
|
478
635
|
|
|
479
636
|
val insetLeft = if (applySafeAreaPadding) screenLayout.insetLeft.roundToPx() else 0
|
|
480
637
|
val insetRight = if (applySafeAreaPadding) screenLayout.insetRight.roundToPx() else 0
|
|
481
|
-
val
|
|
638
|
+
val safeLayoutWidth = with(geometry) {
|
|
639
|
+
safeWidth(screenLayout, constraints.maxWidth, insetLeft, insetRight)
|
|
640
|
+
}
|
|
641
|
+
val searchStartX = with(geometry) { searchStartX(screenLayout, insetLeft, spacing12) }
|
|
482
642
|
val realConstraints = constraints.copy(
|
|
483
643
|
minWidth = 0,
|
|
484
644
|
minHeight = 0,
|
|
485
|
-
maxWidth = (
|
|
486
|
-
.coerceAtLeast(0),
|
|
645
|
+
maxWidth = (safeLayoutWidth - spacing12 * 2).coerceAtLeast(0),
|
|
487
646
|
)
|
|
488
647
|
val backIconPlaceable =
|
|
489
648
|
measurables.find { it.layoutId == HeaderId.BACK_ID }?.measure(realConstraints)
|
|
@@ -522,15 +681,20 @@ private class LiteScreenLayoutPolicy(
|
|
|
522
681
|
/**
|
|
523
682
|
* replicate logic from [vn.momo.kits.navigation.component.HeaderBackground]
|
|
524
683
|
* */
|
|
525
|
-
var defaultHeight = statusBarPx
|
|
684
|
+
var defaultHeight = with(geometry) { headerHeight(screenLayout, statusBarPx, headerRowDefault) }
|
|
526
685
|
if (isHeaderExtend) {
|
|
527
|
-
defaultHeight =
|
|
686
|
+
defaultHeight = if (screenLayout.isIPhoneDuo) {
|
|
687
|
+
max(defaultHeight, 154.dp.roundToPx())
|
|
688
|
+
} else {
|
|
689
|
+
154.dp.roundToPx()
|
|
690
|
+
}
|
|
528
691
|
}
|
|
529
692
|
val headerHeight = when {
|
|
530
693
|
isHeaderNone -> statusBarPx
|
|
531
694
|
!useAnimationSearch && !isHeaderExtend -> defaultHeight
|
|
532
695
|
else -> {
|
|
533
|
-
val collapsableSpace = defaultHeight -
|
|
696
|
+
val collapsableSpace = defaultHeight -
|
|
697
|
+
with(geometry) { collapsedHeaderHeight(screenLayout, statusBarPx, headerRowDefault) }
|
|
534
698
|
(defaultHeight - scrollPercent * collapsableSpace).toInt()
|
|
535
699
|
}
|
|
536
700
|
}
|
|
@@ -574,8 +738,10 @@ private class LiteScreenLayoutPolicy(
|
|
|
574
738
|
) {
|
|
575
739
|
val progress = animationOption?.progress?.value ?: 1f
|
|
576
740
|
|
|
577
|
-
val startX =
|
|
578
|
-
val startY =
|
|
741
|
+
val startX = searchStartX
|
|
742
|
+
val startY = with(geometry) {
|
|
743
|
+
searchStartY(screenLayout, headerY, statusBarPx, headerRowDefault)
|
|
744
|
+
}
|
|
579
745
|
var curX = startX
|
|
580
746
|
var curY = startY
|
|
581
747
|
|
|
@@ -613,14 +779,22 @@ private class LiteScreenLayoutPolicy(
|
|
|
613
779
|
}
|
|
614
780
|
}
|
|
615
781
|
headerRightPlaceable?.place(
|
|
616
|
-
x =
|
|
782
|
+
x = with(geometry) {
|
|
783
|
+
headerRightStartX(
|
|
784
|
+
screenLayout,
|
|
785
|
+
layoutWidth,
|
|
786
|
+
insetRight,
|
|
787
|
+
spacing12,
|
|
788
|
+
headerRightPlaceable.safeWidth,
|
|
789
|
+
)
|
|
790
|
+
},
|
|
617
791
|
y = startY + headerRightPlaceable.verticalCenterOffset(headerRowDefault),
|
|
618
792
|
)
|
|
619
793
|
|
|
620
794
|
val titleOffset = IntOffset(
|
|
621
795
|
x = if (titlePosition == TitlePosition.LEFT) curX
|
|
622
|
-
else
|
|
623
|
-
space =
|
|
796
|
+
else searchStartX - spacing12 + titlePlaceable.horizontalCenterOffset(
|
|
797
|
+
space = safeLayoutWidth - spacing12 * 2,
|
|
624
798
|
layoutDirection = layoutDirection,
|
|
625
799
|
),
|
|
626
800
|
y = startY + titlePlaceable.verticalCenterOffset(headerRowDefault),
|
|
@@ -771,11 +945,8 @@ private fun LiteInputSearch(
|
|
|
771
945
|
val interactionSource = remember { MutableInteractionSource() }
|
|
772
946
|
val textState by remember { inputSearchProps.textFieldState }
|
|
773
947
|
|
|
774
|
-
val isShowBtnText
|
|
775
|
-
|
|
776
|
-
inputSearchProps.isShowBtnText && !inputSearchProps.btnText.isNullOrEmpty()
|
|
777
|
-
}
|
|
778
|
-
}
|
|
948
|
+
val isShowBtnText = inputSearchProps.isShowBtnText &&
|
|
949
|
+
!inputSearchProps.btnText.isNullOrEmpty()
|
|
779
950
|
val inputFieldFontFamily = getFontFamily(theme.font, Typography.bodyDefaultRegular.fontWeight)
|
|
780
951
|
val inputFieldStyle = remember(theme, inputFieldFontFamily) {
|
|
781
952
|
Typography.bodyDefaultRegular.copy(
|
|
@@ -817,12 +988,8 @@ private fun LiteInputSearch(
|
|
|
817
988
|
inputSearchProps.clearCondition?.invoke() == true || (isFocused.value && textState.isNotEmpty())
|
|
818
989
|
}
|
|
819
990
|
}
|
|
820
|
-
val placeHolder
|
|
821
|
-
|
|
822
|
-
snapshotFlow { textState }.collectLatest {
|
|
823
|
-
value = if (it.isEmpty()) inputSearchProps.placeHolder else null
|
|
824
|
-
}
|
|
825
|
-
}
|
|
991
|
+
val placeHolder = inputSearchProps.placeHolder
|
|
992
|
+
?.takeIf { textState.isEmpty() }
|
|
826
993
|
val iconRightModifier = remember {
|
|
827
994
|
Modifier
|
|
828
995
|
.padding(start = Spacing.L)
|
|
@@ -865,7 +1032,7 @@ private fun LiteInputSearch(
|
|
|
865
1032
|
) {
|
|
866
1033
|
if (!placeHolder.isNullOrEmpty()) {
|
|
867
1034
|
inputSearchProps.customPlaceHolder?.invoke() ?: Text(
|
|
868
|
-
text = placeHolder
|
|
1035
|
+
text = placeHolder,
|
|
869
1036
|
style = inputSearchProps.customTextStyle
|
|
870
1037
|
?: Typography.bodyDefaultRegular,
|
|
871
1038
|
maxLines = 1,
|
|
@@ -893,7 +1060,7 @@ private fun LiteInputSearch(
|
|
|
893
1060
|
|
|
894
1061
|
if (isShowBtnText) {
|
|
895
1062
|
Text(
|
|
896
|
-
text = inputSearchProps.btnText
|
|
1063
|
+
text = inputSearchProps.btnText,
|
|
897
1064
|
style = Typography.actionDefaultBold,
|
|
898
1065
|
color = theme.colors.text.default,
|
|
899
1066
|
modifier = Modifier.padding(start = Spacing.L)
|
|
@@ -902,15 +1069,3 @@ private fun LiteInputSearch(
|
|
|
902
1069
|
}
|
|
903
1070
|
}
|
|
904
1071
|
}
|
|
905
|
-
|
|
906
|
-
fun Modifier.hideKeyboardOnTap() = composed {
|
|
907
|
-
val focusManager = LocalFocusManager.current
|
|
908
|
-
val keyboardManager = LocalSoftwareKeyboardController.current
|
|
909
|
-
|
|
910
|
-
pointerInput(Unit) {
|
|
911
|
-
detectTapGestures {
|
|
912
|
-
keyboardManager?.hide()
|
|
913
|
-
focusManager.clearFocus()
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
@@ -19,6 +19,7 @@ import coil3.request.ImageRequest
|
|
|
19
19
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
20
20
|
import vn.momo.kits.const.AppTheme
|
|
21
21
|
import vn.momo.kits.const.Colors
|
|
22
|
+
import vn.momo.kits.gifimage.GifImage
|
|
22
23
|
import vn.momo.kits.modifier.conditional
|
|
23
24
|
import vn.momo.kits.utils.Icons
|
|
24
25
|
import vn.momo.kits.utils.noThemeIcons
|
|
@@ -59,18 +60,27 @@ fun Icon(
|
|
|
59
60
|
}
|
|
60
61
|
.semantics { contentDescription = contentDesc }
|
|
61
62
|
) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
if (isGifUrl(iconUrl)) {
|
|
64
|
+
GifImage(
|
|
65
|
+
modifier = Modifier.matchParentSize(),
|
|
66
|
+
url = iconUrl,
|
|
67
|
+
contentDescription = null,
|
|
68
|
+
contentScale = ContentScale.Fit,
|
|
69
|
+
)
|
|
70
|
+
} else {
|
|
71
|
+
AsyncImage(
|
|
72
|
+
modifier = Modifier.matchParentSize(),
|
|
73
|
+
model = ImageRequest.Builder(context)
|
|
74
|
+
.data(iconUrl)
|
|
75
|
+
.size(
|
|
76
|
+
coil3.size.Dimension.Undefined,
|
|
77
|
+
coil3.size.Dimension.Undefined
|
|
78
|
+
)
|
|
79
|
+
.build(),
|
|
80
|
+
contentDescription = null,
|
|
81
|
+
contentScale = ContentScale.Fit,
|
|
82
|
+
colorFilter = colorFilter,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
75
85
|
}
|
|
76
86
|
}
|
|
@@ -77,7 +77,7 @@ private fun getHostFromUrl(url: String): String? {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
internal fun isGifUrl(url: String): Boolean {
|
|
81
81
|
val path = url.substringBefore('#').substringBefore('?')
|
|
82
82
|
return path.endsWith(".gif", ignoreCase = true)
|
|
83
83
|
}
|
|
@@ -10,6 +10,10 @@ import androidx.compose.ui.text.font.FontFamily
|
|
|
10
10
|
* - Android: `assets/fonts/<fileName>` (gradle task `syncMomoFonts`)
|
|
11
11
|
* - iOS: file rời ở bundle root (pbxproj fileRef được `post_install.rb` trỏ về font-source)
|
|
12
12
|
*
|
|
13
|
+
* AI-GENERATED START: note SF Pro -> iOS system font (ITMS-91182)
|
|
14
|
+
* Ngoại lệ: SF Pro trên iOS dùng system font (chính là SF Pro) — Apple cấm app nhúng font của Apple.
|
|
15
|
+
* AI-GENERATED END: note SF Pro -> iOS system font (ITMS-91182)
|
|
16
|
+
*
|
|
13
17
|
* Trả về `null` nếu host không có font đó — caller tự fallback [FontFamily.Default].
|
|
14
18
|
*/
|
|
15
19
|
@Composable
|