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:
@@ -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)}…",
|
||||
|
||||
Reference in New Issue
Block a user