VerifyScreen: top + bottom scroll shadows on results list

Top shadow: fades in as you scroll down from the top (content hidden above).
Bottom shadow: present when last item is not yet visible (content below).
Both use drawWithContent overlay at 8% black opacity, 24dp strip.
This commit is contained in:
Kyle Bolen
2026-07-28 05:36:55 +00:00
parent 87241ba1d4
commit 7aa7e4dae1

View File

@@ -3,6 +3,7 @@ package com.bolenpad.photophetch.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
@@ -11,12 +12,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.bolenpad.photophetch.model.CopyResult
/**
* Shown during active import (IMPORTING screen) and after import completes (VERIFY screen).
* Shown during active import (IMPORTING screen).
*/
@Composable
fun ImportingScreen(state: AppState) {
@@ -49,6 +53,9 @@ fun ImportingScreen(state: AppState) {
}
}
/**
* Shown after import completes (VERIFY screen).
*/
@Composable
fun VerifyScreen(state: AppState) {
val result by state.importResult.collectAsState()
@@ -59,6 +66,7 @@ fun VerifyScreen(state: AppState) {
val r = result ?: return
Column(modifier = Modifier.fillMaxSize().padding(24.dp)) {
// Summary header
ResultSummaryHeader(
success = r.successCount,
@@ -67,16 +75,70 @@ fun VerifyScreen(state: AppState) {
)
Spacer(Modifier.height(16.dp))
// File-by-file results
Text("Results", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.height(8.dp))
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
// Scrollable results list with top + bottom shadows
val listState = rememberLazyListState()
val topShadowAlpha by remember {
derivedStateOf {
val scrolled = listState.firstVisibleItemScrollOffset +
listState.firstVisibleItemIndex * 80 // approximate
(scrolled / 60f).coerceIn(0f, 1f)
}
}
val bottomShadowAlpha by remember {
derivedStateOf {
val info = listState.layoutInfo
val lastVisible = info.visibleItemsInfo.lastOrNull()
val total = info.totalItemsCount
if (lastVisible == null || lastVisible.index >= total - 1) 0f
else 1f // snap off at bottom is fine here since it's item-based not pixel
}
}
Box(
modifier = Modifier
.weight(1f)
.drawWithContent {
drawContent()
// Top shadow — appears when scrolled down
if (topShadowAlpha > 0f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Black.copy(alpha = 0.08f * topShadowAlpha),
Color.Transparent,
),
startY = 0f,
endY = 24.dp.toPx(),
)
)
}
// Bottom shadow — fades as last item approaches view
if (bottomShadowAlpha > 0f) {
drawRect(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.08f * bottomShadowAlpha),
),
startY = size.height - 24.dp.toPx(),
endY = size.height,
)
)
}
}
) {
items(r.results, key = { it.photo.devicePath }) { copyResult ->
CopyResultRow(copyResult)
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
items(r.results, key = { it.photo.devicePath }) { copyResult ->
CopyResultRow(copyResult)
}
}
}
@@ -165,20 +227,17 @@ private fun CopyResultRow(result: CopyResult) {
if (result.verified && result.error == null) Icons.Default.CheckCircle else Icons.Default.Error,
contentDescription = null,
tint = if (result.verified && result.error == null)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error,
MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
modifier = Modifier.size(18.dp),
)
Column(modifier = Modifier.weight(1f)) {
val displayName = if (result.convertedFromHeic) {
val displayName = if (result.convertedFromHeic)
"${result.photo.filename}${result.destinationPath.fileName}"
} else {
result.photo.filename
}
else result.photo.filename
Text(displayName, style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium)
if (result.error != null) {
Text(result.error, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.error)
Text(result.error, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.error)
} else {
Text(
"${result.photo.sizeBytes / 1024} KB • SHA-256: ${result.destSha256.take(12)}",