@@ -24,6 +24,7 @@ package ss.document.segment.components.blocks
2424
2525import androidx.compose.foundation.background
2626import androidx.compose.foundation.layout.Arrangement
27+ import androidx.compose.foundation.layout.Box
2728import androidx.compose.foundation.layout.Column
2829import androidx.compose.foundation.layout.Spacer
2930import androidx.compose.foundation.layout.WindowInsets
@@ -46,8 +47,10 @@ import androidx.compose.ui.Modifier
4647import androidx.compose.ui.draw.drawWithContent
4748import androidx.compose.ui.graphics.Color
4849import androidx.compose.ui.graphics.graphicsLayer
50+ import androidx.compose.ui.layout.ContentScale
4951import androidx.compose.ui.unit.dp
5052import app.ss.design.compose.widget.scaffold.LocalNavbarController
53+ import coil.compose.AsyncImage
5154import io.adventech.blockkit.model.BlockData
5255import io.adventech.blockkit.model.resource.ReferenceModel
5356import io.adventech.blockkit.model.resource.Segment
@@ -57,6 +60,7 @@ import io.adventech.blockkit.ui.style.LocalReaderStyle
5760import io.adventech.blockkit.ui.style.LocalSegmentStyle
5861import io.adventech.blockkit.ui.style.background
5962import io.adventech.blockkit.ui.style.primaryForeground
63+ import io.adventech.blockkit.ui.style.showBackground
6064import io.adventech.blockkit.ui.style.thenIf
6165import ss.document.segment.components.SegmentCover
6266import ss.document.segment.components.SegmentHeader
@@ -81,140 +85,154 @@ internal fun SegmentBlocksContent(
8185
8286 val hasCoverParallax by remember(segment) { derivedStateOf { segment.cover != null && ! (segment.titleBelowCover ? : titleBelowCover) } }
8387
84- LazyColumn (
88+ Box (
8589 modifier = modifier
8690 .fillMaxSize()
87- .background(readerStyle.theme.background())
88- .imePadding(),
89- state = listState,
91+ .background(readerStyle.theme.background()),
9092 ) {
91- item(key = " cover-${segment.id} " ) {
92- SegmentCover (
93- cover = segment.cover,
94- modifier = Modifier
95- .animateItem()
96- .thenIf(hasCoverParallax) {
97- // Parallax
98- graphicsLayer {
99- // Check if the cover is the first item visible
100- val firstVisibleIndex = listState.firstVisibleItemIndex
101- val firstVisibleOffset = listState.firstVisibleItemScrollOffset
102-
103- translationY = if (firstVisibleIndex == 0 ) {
104- // Move the cover down by 50% of the scroll distance.
105- // This makes it look like it's moving up at half speed.
106- firstVisibleOffset * 0.5f
107- } else {
108- 0f
109- }
110- }
111- // Fade to Black (Draw Overlay)
112- .drawWithContent {
113- drawContent() // Draw the original image first
93+ segment.background?.takeIf { readerStyle.theme.showBackground() }?.let {
94+ AsyncImage (
95+ model = it,
96+ contentDescription = null ,
97+ modifier = modifier.fillMaxSize(),
98+ contentScale = ContentScale .Crop ,
99+ )
100+ }
114101
102+ LazyColumn (
103+ modifier = Modifier
104+ .fillMaxSize()
105+ .imePadding(),
106+ state = listState,
107+ ) {
108+ item(key = " cover-${segment.id} " ) {
109+ SegmentCover (
110+ cover = segment.cover,
111+ modifier = Modifier
112+ .animateItem()
113+ .thenIf(hasCoverParallax) {
114+ // Parallax
115+ graphicsLayer {
116+ // Check if the cover is the first item visible
115117 val firstVisibleIndex = listState.firstVisibleItemIndex
116- val firstVisibleOffset = listState.firstVisibleItemScrollOffset.toFloat()
118+ val firstVisibleOffset = listState.firstVisibleItemScrollOffset
119+
120+ translationY = if (firstVisibleIndex == 0 ) {
121+ // Move the cover down by 50% of the scroll distance.
122+ // This makes it look like it's moving up at half speed.
123+ firstVisibleOffset * 0.5f
124+ } else {
125+ 0f
126+ }
127+ }
128+ // Fade to Black (Draw Overlay)
129+ .drawWithContent {
130+ drawContent() // Draw the original image first
117131
118- if (firstVisibleIndex == 0 ) {
119- // Calculate opacity: 0f (clear) to 0.7f (dark)
120- // We use size.height to scale the fade relative to the cover's size
121- val fadeAlpha = (firstVisibleOffset / size.height)
122- .coerceIn(0f , 0.7f ) // Cap at 0.7 so it doesn't go pitch black
132+ val firstVisibleIndex = listState.firstVisibleItemIndex
133+ val firstVisibleOffset = listState.firstVisibleItemScrollOffset.toFloat()
123134
124- drawRect(Color .Black , alpha = fadeAlpha)
135+ if (firstVisibleIndex == 0 ) {
136+ // Calculate opacity: 0f (clear) to 0.7f (dark)
137+ // We use size.height to scale the fade relative to the cover's size
138+ val fadeAlpha = (firstVisibleOffset / size.height)
139+ .coerceIn(0f , 0.7f ) // Cap at 0.7 so it doesn't go pitch black
140+
141+ drawRect(Color .Black , alpha = fadeAlpha)
142+ }
125143 }
126- }
127- },
128- headerContent = {
129- if ( ! (segment.titleBelowCover ? : titleBelowCover)) {
130- SegmentHeader (
131- title = segment.markdownTitle ? : segment.title ,
132- subtitle = segment.markdownSubtitle ? : segment.subtitle ,
133- date = segment.date ,
134- contentColor = if ( segment.cover != null ) Color . White else contentColor ,
135- style = segmentStyle. takeIf { segment.cover == null } ,
136- hasCover = hasCoverParallax,
137- modifier = Modifier
138- .fillMaxWidth()
139- .thenIf(hasCoverParallax) {
140- graphicsLayer {
141- val firstVisibleIndex = listState.firstVisibleItemIndex
142- val firstVisibleOffset = listState.firstVisibleItemScrollOffset.toFloat()
143-
144- translationY = if (firstVisibleIndex == 0 ) {
145- // Inverse translation keeps text locked to original scroll position
146- - (firstVisibleOffset * 0.5f )
147- } else {
148- 0f
144+ },
145+ headerContent = {
146+ if ( ! (segment.titleBelowCover ? : titleBelowCover)) {
147+ SegmentHeader (
148+ title = segment.markdownTitle ? : segment.title,
149+ subtitle = segment.markdownSubtitle ? : segment.subtitle ,
150+ date = segment.date ,
151+ contentColor = if ( segment.cover != null ) Color . White else contentColor ,
152+ style = segmentStyle. takeIf { segment.cover == null } ,
153+ hasCover = hasCoverParallax ,
154+ modifier = Modifier
155+ .fillMaxWidth()
156+ .thenIf(hasCoverParallax) {
157+ graphicsLayer {
158+ val firstVisibleIndex = listState.firstVisibleItemIndex
159+ val firstVisibleOffset = listState.firstVisibleItemScrollOffset.toFloat()
160+
161+ translationY = if (firstVisibleIndex == 0 ) {
162+ // Inverse translation keeps text locked to original scroll position
163+ - (firstVisibleOffset * 0.5f )
164+ } else {
165+ 0f
166+ }
149167 }
150- }
151- },
152- )
168+ },
169+ )
170+ }
153171 }
172+ )
173+ }
174+
175+ if ((segment.titleBelowCover ? : titleBelowCover)) {
176+ item(key = " header-${segment.id} " ) {
177+ SegmentHeader (
178+ title = segment.markdownTitle ? : segment.title,
179+ subtitle = segment.markdownSubtitle ? : segment.subtitle,
180+ date = segment.date,
181+ contentColor = contentColor,
182+ style = segmentStyle,
183+ hasCover = false ,
184+ modifier = Modifier
185+ .fillMaxWidth()
186+ .animateItem()
187+ .thenIf(hasCoverParallax) {
188+ background(readerStyle.theme.background())
189+ }
190+ .padding(top = 16 .dp),
191+ )
154192 }
155- )
156- }
193+ }
157194
158- if ((segment.titleBelowCover ? : titleBelowCover)) {
159- item(key = " header-${segment.id} " ) {
160- SegmentHeader (
161- title = segment.markdownTitle ? : segment.title,
162- subtitle = segment.markdownSubtitle ? : segment.subtitle,
163- date = segment.date,
164- contentColor = contentColor,
165- style = segmentStyle,
166- hasCover = false ,
195+
196+ // Using a single `item` with a `Column` to wrap the blocks avoids LazyColumn's
197+ // per-item recycling overhead. This prevents scroll jitter caused by the high
198+ // composition cost of individual `BlockContent` items.
199+ item(key = " blocks-container" , contentType = " blocks-container" ) {
200+ Column (
167201 modifier = Modifier
168- .fillMaxWidth()
169202 .animateItem()
170203 .thenIf(hasCoverParallax) {
171204 background(readerStyle.theme.background())
172205 }
173206 .padding(top = 16 .dp),
174- )
175- }
176- }
177-
178-
179- // Using a single `item` with a `Column` to wrap the blocks avoids LazyColumn's
180- // per-item recycling overhead. This prevents scroll jitter caused by the high
181- // composition cost of individual `BlockContent` items.
182- item(key = " blocks-container" , contentType = " blocks-container" ) {
183- Column (
184- modifier = Modifier
185- .animateItem()
186- .thenIf(hasCoverParallax) {
187- background(readerStyle.theme.background())
188- }
189- .padding(top = 16 .dp),
190- verticalArrangement = Arrangement .spacedBy(20 .dp),
191- ) {
192- segment.blocks.orEmpty().forEach { block ->
193- // We manually provide a key here for stability within the Column
194- key(block.id) {
195- BlockContent (
196- blockItem = block,
197- modifier = Modifier ,
198- userInputState = userInputState,
199- onHandleUri = stableOnHandleUri,
200- onHandleReference = stableOnHandleReference,
201- )
207+ verticalArrangement = Arrangement .spacedBy(20 .dp),
208+ ) {
209+ segment.blocks.orEmpty().forEach { block ->
210+ // We manually provide a key here for stability within the Column
211+ key(block.id) {
212+ BlockContent (
213+ blockItem = block,
214+ modifier = Modifier ,
215+ userInputState = userInputState,
216+ onHandleUri = stableOnHandleUri,
217+ onHandleReference = stableOnHandleReference,
218+ )
219+ }
202220 }
203221 }
204222 }
205- }
206223
207- item(key = " spacer" ) { Spacer (Modifier .height(64 .dp)) }
224+ item(key = " spacer" ) { Spacer (Modifier .height(64 .dp)) }
208225
209- item(key = " spacer-system" ) { Spacer (Modifier .windowInsetsBottomHeight(WindowInsets .systemBars)) }
226+ item(key = " spacer-system" ) { Spacer (Modifier .windowInsetsBottomHeight(WindowInsets .systemBars)) }
210227
211- item(" spacer-navbar" ) {
212- if (LocalNavbarController .current.enabled) {
213- Spacer (
214- modifier = Modifier
215- .fillMaxWidth()
216- .height(80 .dp)
217- )
228+ item(" spacer-navbar" ) {
229+ if (LocalNavbarController .current.enabled) {
230+ Spacer (
231+ modifier = Modifier
232+ .fillMaxWidth()
233+ .height(80 .dp)
234+ )
235+ }
218236 }
219237 }
220238 }
0 commit comments