Skip to content

Commit 72da06f

Browse files
Segment and Block Background (#1528)
1 parent be4337a commit 72da06f

13 files changed

Lines changed: 1529 additions & 133 deletions

File tree

features/document/src/main/kotlin/ss/document/DocumentPresenter.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,12 @@ class DocumentPresenter @AssistedInject constructor(
266266
if (value == persistentListOf<Segment>()) {
267267
delay(150)
268268
}
269-
value = (document?.segments?.map { it.copy(cover = it.cover ?: document.cover) } ?: emptyList())
269+
value = (document?.segments?.map {
270+
it.copy(
271+
cover = it.cover ?: document.cover,
272+
background = it.background ?: document.background,
273+
)
274+
} ?: emptyList())
270275
.toImmutableList()
271276
}
272277

features/document/src/main/kotlin/ss/document/segment/components/blocks/SegmentBlocksContent.kt

Lines changed: 128 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package ss.document.segment.components.blocks
2424

2525
import androidx.compose.foundation.background
2626
import androidx.compose.foundation.layout.Arrangement
27+
import androidx.compose.foundation.layout.Box
2728
import androidx.compose.foundation.layout.Column
2829
import androidx.compose.foundation.layout.Spacer
2930
import androidx.compose.foundation.layout.WindowInsets
@@ -46,8 +47,10 @@ import androidx.compose.ui.Modifier
4647
import androidx.compose.ui.draw.drawWithContent
4748
import androidx.compose.ui.graphics.Color
4849
import androidx.compose.ui.graphics.graphicsLayer
50+
import androidx.compose.ui.layout.ContentScale
4951
import androidx.compose.ui.unit.dp
5052
import app.ss.design.compose.widget.scaffold.LocalNavbarController
53+
import coil.compose.AsyncImage
5154
import io.adventech.blockkit.model.BlockData
5255
import io.adventech.blockkit.model.resource.ReferenceModel
5356
import io.adventech.blockkit.model.resource.Segment
@@ -57,6 +60,7 @@ import io.adventech.blockkit.ui.style.LocalReaderStyle
5760
import io.adventech.blockkit.ui.style.LocalSegmentStyle
5861
import io.adventech.blockkit.ui.style.background
5962
import io.adventech.blockkit.ui.style.primaryForeground
63+
import io.adventech.blockkit.ui.style.showBackground
6064
import io.adventech.blockkit.ui.style.thenIf
6165
import ss.document.segment.components.SegmentCover
6266
import 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
}

features/resource/src/main/kotlin/ss/resource/components/content/Placeholder.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ internal object Placeholder {
4747
segmentChipsStyle = null,
4848
style = null,
4949
share = null,
50+
background = null,
5051
)
5152

5253
val resourcesWithMenu: List<ResourceSection> = listOf(
@@ -87,6 +88,7 @@ internal object Placeholder {
8788
segmentChipsStyle = null,
8889
style = null,
8990
share = null,
91+
background = null,
9092
),
9193
ResourceDocument(
9294
id = "doc2",
@@ -117,7 +119,8 @@ internal object Placeholder {
117119
externalURL = null,
118120
segmentChipsStyle = null,
119121
style = null,
120-
share = null
122+
share = null,
123+
background = null,
121124
)
122125
)
123126
),
@@ -158,6 +161,7 @@ internal object Placeholder {
158161
segmentChipsStyle = null,
159162
style = null,
160163
share = null,
164+
background = null,
161165
)
162166
)
163167
)

libraries/block-kit/model/src/main/kotlin/io/adventech/blockkit/model/resource/ResourceDocument.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ data class ResourceDocument(
4747
val segmentChipsStyle: SegmentChipsStyle?,
4848
val style: Style?,
4949
val share: ShareOptions?,
50+
val background: String?,
5051
)

0 commit comments

Comments
 (0)