diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/VerifyScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/VerifyScreen.kt index da5b222..a1bc614 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/VerifyScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/VerifyScreen.kt @@ -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)}…",