Compare commits

..

5 Commits

Author SHA1 Message Date
xfy
ffafee550e Merge branch 'feat/record-detail-image-lightbox' 2026-07-22 15:23:53 +08:00
xfy
70c97ed9d5 revert(photo-editor): 恢复点击裁剪 icon 进入裁剪的交互
撤销"切到裁剪 tab 自动进入裁剪"的改动,保留原来的两步操作:
点击下方裁剪 icon 进入裁剪模式,再次点击 ✓ 确认裁剪。

ViewModel 恢复 toggleCrop,删除无用的 enterCrop/exitCrop;
保留裁剪框拖动的 bug 修复(本地累积值 + rememberUpdatedState)。
2026-07-22 15:20:13 +08:00
xfy
d3ebd6829b fix(photo-editor): 修复裁剪框拖动的闭包快照陷阱
pointerInput(Unit) 的手势 lambda 只在首次组合捕获一次 state,
后续 onDragStart 读的是过期快照,导致再次拖动从中心开始。

改用本地累积值绕开 StateFlow 往返:onDragStart 把 state 快照到
mutableFloatStateOf,onDrag 每帧在累积值上叠加增量,onDragEnd
一次性提交。onDragStart 通过 rememberUpdatedState 读最新 state。

enterCrop 改为幂等(已启用则保留当前框),避免 LaunchedEffect
因 editorState 变化重触时冲掉用户拖动结果。
2026-07-22 15:18:19 +08:00
xfy
b8509381d9 fix(photo-editor): 修复裁剪 tab 流程与裁剪框拖动
两个问题:
1. 切到裁剪 tab 后还要再点裁剪 icon 才进入裁剪——改为切 tab 即自动进入,
   底部按钮由"开关裁剪"改为"✓ 确认裁剪",点击烘焙裁剪并保留在 tab。
2. 裁剪框无法移动——重写 CropOverlay 拖动逻辑:
   - 加 BODY 整体平移(拖框内区域平移整框)
   - onDrag 每帧从 state 读实时裁剪值叠加增量,修复闭包快照导致的抖动
   - dragHandle 内聚到组件内部,消除跨重组延迟
   - 抽出 hitHandle/moveCrop,四角与整体均带边界约束防交叉越界

ViewModel 新增 enterCrop/exitCrop/applyCrop,删除 toggleCrop。
2026-07-22 14:34:42 +08:00
xfy
cec8debff0 feat(date-recorder): 详情页图片灯箱,支持缩放/旋转手势
点击详情页大图弹出全屏深色遮罩,集成 panpf/zoomimage 的
SketchZoomAsyncImage,支持双指缩放、平移、捻合旋转(松手吸附
0/90/180/270°)与双击复位,点空白或返回键关闭。

新增 zoomimage-compose-sketch4 1.6.0 依赖(与项目 sketch 同作者,
SketchZoomAsyncImage 是 AsyncImage 超集,额外提供缩放/旋转/大图分片)。
2026-07-22 14:26:27 +08:00
6 changed files with 272 additions and 40 deletions

View File

@ -105,6 +105,9 @@ dependencies {
implementation(libs.camera.view) implementation(libs.camera.view)
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
// 日期记录器:详情页灯箱图片缩放/旋转(与 sketch 同作者 panpfSketchZoomAsyncImage 是 AsyncImage 超集)
implementation(libs.zoomimage.compose.sketch4)
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}")
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.room.testing) testImplementation(libs.room.testing)

View File

@ -91,6 +91,32 @@ class PhotoEditorViewModel(
} }
} }
/**
* 确认裁剪将当前裁剪框烘焙到源图用裁剪结果替换 [PhotoEditorState.sourceBitmap]
* 重置旋转与裁剪框清空手写笔触坐标系已变化
*/
fun applyCrop() {
update {
if (!it.cropEnabled) return@update it
val cropped = PhotoProcessor.crop(
bitmap = it.rotatedBitmap,
left = it.cropLeft!!,
top = it.cropTop,
right = it.cropRight!!,
bottom = it.cropBottom
)
it.copy(
sourceBitmap = cropped,
rotationDegrees = 0,
cropLeft = null,
cropTop = 0f,
cropRight = null,
cropBottom = 1f,
strokes = emptyList()
)
}
}
/** 更新裁剪框比例。 */ /** 更新裁剪框比例。 */
fun updateCrop(left: Float, top: Float, right: Float, bottom: Float) { fun updateCrop(left: Float, top: Float, right: Float, bottom: Float) {
update { update {

View File

@ -0,0 +1,72 @@
package plus.rua.project.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.github.panpf.sketch.rememberAsyncImageState
import com.github.panpf.zoomimage.SketchZoomAsyncImage
import com.github.panpf.zoomimage.rememberSketchZoomState
/**
* 全屏图片灯箱支持双指缩放平移与旋转查看
*
* 在全屏深色遮罩中居中显示图片底层用 zoomimage [SketchZoomAsyncImage]
* 提供手势双指缩放单指平移双指捻合自由旋转松手后吸附到最近的
* 0/90/180/270°双击复位点击图片外的空白区域或按返回键触发 [onDismiss]
*
* sketch [rememberAsyncImageState] 负责加载zoomimage
* [rememberSketchZoomState] 负责缩放/平移/旋转后者是 sketch `AsyncImage`
* 之外新增的必需参数
*
* @param photoUri 图片 URI null 时直接返回不渲染
* @param contentDescription 无障碍描述 null 表示纯装饰
* @param onDismiss 关闭回调点击空白区域或按返回键时触发
* @param modifier 布局修饰符
*/
@Composable
fun ImageLightbox(
photoUri: String?,
contentDescription: String?,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
if (photoUri == null) return
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
decorFitsSystemWindows = false,
usePlatformDefaultWidth = false
)
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismiss
),
contentAlignment = Alignment.Center
) {
val asyncImageState = rememberAsyncImageState()
val zoomState = rememberSketchZoomState()
SketchZoomAsyncImage(
uri = photoUri,
contentDescription = contentDescription,
state = asyncImageState,
zoomState = zoomState,
modifier = Modifier.fillMaxSize()
)
}
}
}

View File

@ -20,7 +20,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Brush
import androidx.compose.material.icons.filled.ChevronLeft import androidx.compose.material.icons.filled.ChevronLeft
import androidx.compose.material.icons.filled.Crop import androidx.compose.material.icons.filled.Crop
import androidx.compose.material.icons.filled.CropFree
import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.RotateLeft import androidx.compose.material.icons.filled.RotateLeft
import androidx.compose.material.icons.filled.RotateRight import androidx.compose.material.icons.filled.RotateRight
@ -41,10 +40,13 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import kotlin.math.absoluteValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
@ -158,6 +160,7 @@ fun PhotoEditorScreen(
onTabChange = { activeTab = it }, onTabChange = { activeTab = it },
onRotate = viewModel::rotate, onRotate = viewModel::rotate,
onCropToggle = viewModel::toggleCrop, onCropToggle = viewModel::toggleCrop,
onApplyCrop = viewModel::applyCrop,
onCropChange = viewModel::updateCrop, onCropChange = viewModel::updateCrop,
onAddPoint = viewModel::addStrokePoint, onAddPoint = viewModel::addStrokePoint,
onEndStroke = viewModel::endStroke, onEndStroke = viewModel::endStroke,
@ -182,6 +185,7 @@ private fun EditorBody(
onTabChange: (EditTab) -> Unit, onTabChange: (EditTab) -> Unit,
onRotate: (Int) -> Unit, onRotate: (Int) -> Unit,
onCropToggle: () -> Unit, onCropToggle: () -> Unit,
onApplyCrop: () -> Unit,
onCropChange: (Float, Float, Float, Float) -> Unit, onCropChange: (Float, Float, Float, Float) -> Unit,
onAddPoint: (Offset) -> Unit, onAddPoint: (Offset) -> Unit,
onEndStroke: () -> Unit, onEndStroke: () -> Unit,
@ -246,6 +250,7 @@ private fun EditorBody(
state = state, state = state,
onRotate = onRotate, onRotate = onRotate,
onCropToggle = onCropToggle, onCropToggle = onCropToggle,
onApplyCrop = onApplyCrop,
onUndoStroke = onUndoStroke, onUndoStroke = onUndoStroke,
onStrokeColorChange = onStrokeColorChange, onStrokeColorChange = onStrokeColorChange,
modifier = Modifier modifier = Modifier
@ -265,8 +270,6 @@ private fun EditableImage(
onDisplaySizeChange: (Float, Float) -> Unit onDisplaySizeChange: (Float, Float) -> Unit
) { ) {
val rotatedBmp = remember(state.rotationDegrees, state.sourceBitmap) { state.rotatedBitmap } val rotatedBmp = remember(state.rotationDegrees, state.sourceBitmap) { state.rotatedBitmap }
// 裁剪框拖动手柄CROP 模式下使用)
var dragHandle by remember { mutableStateOf<CropHandle?>(null) }
Box( Box(
modifier = Modifier modifier = Modifier
@ -299,8 +302,6 @@ private fun EditableImage(
if (mode == EditTab.CROP && state.cropEnabled) { if (mode == EditTab.CROP && state.cropEnabled) {
CropOverlay( CropOverlay(
state = state, state = state,
dragHandle = dragHandle,
onHandleChange = { dragHandle = it },
onCropChange = onCropChange, onCropChange = onCropChange,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@ -324,7 +325,7 @@ private fun EditableImage(
} }
} }
private enum class CropHandle { TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, NONE } private enum class CropHandle { TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, BODY, NONE }
/** 裁剪框四元组,支持解构以简化 onDrag 回传。 */ /** 裁剪框四元组,支持解构以简化 onDrag 回传。 */
private data class CropRect( private data class CropRect(
@ -334,16 +335,120 @@ private data class CropRect(
val bottom: Float val bottom: Float
) )
/**
* 判断触摸点落在裁剪框的哪个区域用于决定拖动行为
*
* - 落在四角阈值范围内 对应角手柄缩放裁剪框
* - 落在框内 整体平移
* - 落在框外 [CropHandle.NONE]不响应拖动
*
* @param ox 触摸点 x 比例坐标 [0,1]
* @param oy 触摸点 y 比例坐标 [0,1]
* @param l/t/r/b 当前裁剪框四边比例
*/
private fun hitHandle(
ox: Float,
oy: Float,
l: Float,
t: Float,
r: Float,
b: Float
): CropHandle {
val corner = 0.1f
val atTopLeft = (ox - l).absoluteValue < corner && (oy - t).absoluteValue < corner
val atTopRight = (ox - r).absoluteValue < corner && (oy - t).absoluteValue < corner
val atBottomLeft = (ox - l).absoluteValue < corner && (oy - b).absoluteValue < corner
val atBottomRight = (ox - r).absoluteValue < corner && (oy - b).absoluteValue < corner
return when {
atTopLeft -> CropHandle.TOP_LEFT
atTopRight -> CropHandle.TOP_RIGHT
atBottomLeft -> CropHandle.BOTTOM_LEFT
atBottomRight -> CropHandle.BOTTOM_RIGHT
ox in l..r && oy in t..b -> CropHandle.BODY
else -> CropHandle.NONE
}
}
/**
* 将单次拖动增量应用到当前裁剪框返回受边界约束的新四元组
*
* - 角手柄只移动对应角并用 [minSize] 约束避免左右/上下交叉
* - [CropHandle.BODY]整体平移 [0,1] 边界约束
*
* @param dx x 方向增量比例已归一化
* @param dy y 方向增量比例已归一化
* @param l/t/r/b 当前裁剪框四边比例
* @param handle 本次拖动的手柄
* @param minSize 裁剪框最小宽/高比例防止四角交叉
*/
private fun moveCrop(
dx: Float,
dy: Float,
l: Float,
t: Float,
r: Float,
b: Float,
handle: CropHandle,
minSize: Float = 0.1f
): CropRect {
val left = l.coerceIn(0f, 1f)
val top = t.coerceIn(0f, 1f)
val right = r.coerceIn(0f, 1f)
val bottom = b.coerceIn(0f, 1f)
return when (handle) {
CropHandle.TOP_LEFT -> {
val nl = (left + dx).coerceIn(0f, right - minSize)
val nt = (top + dy).coerceIn(0f, bottom - minSize)
CropRect(nl, nt, right, bottom)
}
CropHandle.TOP_RIGHT -> {
val nr = (right + dx).coerceIn(left + minSize, 1f)
val nt = (top + dy).coerceIn(0f, bottom - minSize)
CropRect(left, nt, nr, bottom)
}
CropHandle.BOTTOM_LEFT -> {
val nl = (left + dx).coerceIn(0f, right - minSize)
val nb = (bottom + dy).coerceIn(top + minSize, 1f)
CropRect(nl, top, right, nb)
}
CropHandle.BOTTOM_RIGHT -> {
val nr = (right + dx).coerceIn(left + minSize, 1f)
val nb = (bottom + dy).coerceIn(top + minSize, 1f)
CropRect(left, top, nr, nb)
}
CropHandle.BODY -> {
val w = right - left
val h = bottom - top
val nl = (left + dx).coerceIn(0f, 1f - w)
val nt = (top + dy).coerceIn(0f, 1f - h)
CropRect(nl, nt, nl + w, nt + h)
}
CropHandle.NONE -> CropRect(left, top, right, bottom)
}
}
@Composable @Composable
private fun CropOverlay( private fun CropOverlay(
state: PhotoEditorState, state: PhotoEditorState,
dragHandle: CropHandle?,
onHandleChange: (CropHandle?) -> Unit,
onCropChange: (Float, Float, Float, Float) -> Unit, onCropChange: (Float, Float, Float, Float) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val left = state.cropLeft ?: 0.1f var dragHandle by remember { mutableStateOf(CropHandle.NONE) }
val right = state.cropRight ?: 0.9f // 拖动期间的本地累积值;非拖动时为 null绘制回退到 state
var dragLeft by remember { mutableFloatStateOf(0f) }
var dragTop by remember { mutableFloatStateOf(0f) }
var dragRight by remember { mutableFloatStateOf(0f) }
var dragBottom by remember { mutableFloatStateOf(0f) }
// pointerInput(Unit) 只捕获一次 state用 rememberUpdatedState 让
// 手势 lambda 内始终读到最新的 state含上次拖动提交后的值
val currentState by rememberUpdatedState(state)
// 绘制用值:拖动中取本地累积,否则取 state
val left = if (dragHandle != CropHandle.NONE) dragLeft else (state.cropLeft ?: 0.1f)
val top = if (dragHandle != CropHandle.NONE) dragTop else state.cropTop
val right = if (dragHandle != CropHandle.NONE) dragRight else (state.cropRight ?: 0.9f)
val bottom = if (dragHandle != CropHandle.NONE) dragBottom else state.cropBottom
Canvas( Canvas(
modifier = modifier.pointerInput(Unit) { modifier = modifier.pointerInput(Unit) {
detectDragGestures( detectDragGestures(
@ -351,45 +456,47 @@ private fun CropOverlay(
val size = this.size val size = this.size
val ox = offset.x / size.width val ox = offset.x / size.width
val oy = offset.y / size.height val oy = offset.y / size.height
onHandleChange( val baseL = currentState.cropLeft ?: 0.1f
when { val baseT = currentState.cropTop
ox < 0.2f && oy < 0.2f -> CropHandle.TOP_LEFT val baseR = currentState.cropRight ?: 0.9f
ox > 0.8f && oy < 0.2f -> CropHandle.TOP_RIGHT val baseB = currentState.cropBottom
ox < 0.2f && oy > 0.8f -> CropHandle.BOTTOM_LEFT // 进入拖动:把 state 当前值快照到本地累积
ox > 0.8f && oy > 0.8f -> CropHandle.BOTTOM_RIGHT dragLeft = baseL
else -> CropHandle.NONE dragTop = baseT
} dragRight = baseR
) dragBottom = baseB
dragHandle = hitHandle(ox, oy, baseL, baseT, baseR, baseB)
}, },
onDrag = { change, drag -> onDrag = { change, drag ->
if (dragHandle == CropHandle.NONE) return@detectDragGestures
change.consume() change.consume()
val size = this.size val size = this.size
val dx = drag.x / size.width val dx = drag.x / size.width
val dy = drag.y / size.height val dy = drag.y / size.height
val (nL, nT, nR, nB) = when (dragHandle) { // 在本地累积值上叠加增量,不触发 StateFlow 往返
CropHandle.TOP_LEFT -> val cur = moveCrop(dx, dy, dragLeft, dragTop, dragRight, dragBottom, dragHandle)
CropRect(left + dx, state.cropTop + dy, right, state.cropBottom) dragLeft = cur.left
CropHandle.TOP_RIGHT -> dragTop = cur.top
CropRect(left, state.cropTop + dy, right + dx, state.cropBottom) dragRight = cur.right
CropHandle.BOTTOM_LEFT -> dragBottom = cur.bottom
CropRect(left + dx, state.cropTop, right, state.cropBottom + dy)
CropHandle.BOTTOM_RIGHT ->
CropRect(left, state.cropTop, right + dx, state.cropBottom + dy)
else -> return@detectDragGestures
}
onCropChange(nL, nT, nR, nB)
}, },
onDragEnd = { onHandleChange(null) }, onDragEnd = {
onDragCancel = { onHandleChange(null) } // 拖动结束:一次性提交回 ViewModel
if (dragHandle != CropHandle.NONE) {
onCropChange(dragLeft, dragTop, dragRight, dragBottom)
}
dragHandle = CropHandle.NONE
},
onDragCancel = { dragHandle = CropHandle.NONE }
) )
} }
) { ) {
val w = size.width val w = size.width
val h = size.height val h = size.height
val l = left * w val l = left * w
val t = state.cropTop * h val t = top * h
val r = right * w val r = right * w
val b = state.cropBottom * h val b = bottom * h
// 四周遮罩 // 四周遮罩
drawRect(Color.Black.copy(alpha = 0.5f), topLeft = Offset(0f, 0f), size = Size(l, h)) drawRect(Color.Black.copy(alpha = 0.5f), topLeft = Offset(0f, 0f), size = Size(l, h))
drawRect(Color.Black.copy(alpha = 0.5f), topLeft = Offset(r, 0f), size = Size(w - r, h)) drawRect(Color.Black.copy(alpha = 0.5f), topLeft = Offset(r, 0f), size = Size(w - r, h))
@ -411,6 +518,7 @@ private fun ToolBar(
state: PhotoEditorState, state: PhotoEditorState,
onRotate: (Int) -> Unit, onRotate: (Int) -> Unit,
onCropToggle: () -> Unit, onCropToggle: () -> Unit,
onApplyCrop: () -> Unit,
onUndoStroke: () -> Unit, onUndoStroke: () -> Unit,
onStrokeColorChange: (Color) -> Unit, onStrokeColorChange: (Color) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -429,12 +537,15 @@ private fun ToolBar(
Icon(Icons.Filled.RotateRight, contentDescription = "右转 90°") Icon(Icons.Filled.RotateRight, contentDescription = "右转 90°")
} }
} }
EditTab.CROP -> { EditTab.CROP -> if (state.cropEnabled) {
// 已进入裁剪模式:✓ 确认裁剪(烘焙到源图)
FilledIconButton(onClick = onApplyCrop) {
Icon(Icons.Filled.Done, contentDescription = "确认裁剪")
}
} else {
// 未进入:点击进入裁剪模式
FilledIconButton(onClick = onCropToggle) { FilledIconButton(onClick = onCropToggle) {
Icon( Icon(Icons.Filled.Crop, contentDescription = "进入裁剪")
if (state.cropEnabled) Icons.Filled.CropFree else Icons.Filled.Crop,
contentDescription = if (state.cropEnabled) "关闭裁剪" else "开启裁剪"
)
} }
} }
EditTab.HANDWRITE -> { EditTab.HANDWRITE -> {

View File

@ -1,5 +1,7 @@
package plus.rua.project.ui package plus.rua.project.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -81,6 +83,7 @@ fun RecordDetailScreen(
) )
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var showLightbox by remember { mutableStateOf(false) }
// 删除完成后自动返回 // 删除完成后自动返回
if (state.deleted) { if (state.deleted) {
@ -142,6 +145,7 @@ fun RecordDetailScreen(
else -> DetailContent( else -> DetailContent(
state = state, state = state,
onPhotoClick = { showLightbox = true },
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
@ -165,11 +169,20 @@ fun RecordDetailScreen(
} }
) )
} }
if (showLightbox) {
ImageLightbox(
photoUri = state.photoUri,
contentDescription = state.record?.title,
onDismiss = { showLightbox = false }
)
}
} }
@Composable @Composable
private fun DetailContent( private fun DetailContent(
state: DateRecordDetailUiState, state: DateRecordDetailUiState,
onPhotoClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val record = state.record ?: return val record = state.record ?: return
@ -183,6 +196,11 @@ private fun DetailContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onPhotoClick
)
) )
Column(modifier = Modifier.padding(horizontal = 16.dp)) { Column(modifier = Modifier.padding(horizontal = 16.dp)) {

View File

@ -22,6 +22,7 @@ sketch = "4.4.0"
spotless = "8.8.0" spotless = "8.8.0"
tyme4kt = "1.5.0" tyme4kt = "1.5.0"
versions = "0.54.0" versions = "0.54.0"
zoomimage = "1.6.0"
[libraries] [libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
@ -57,6 +58,7 @@ room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
sketch-animated-webp = { module = "io.github.panpf.sketch4:sketch-animated-webp", version.ref = "sketch" } sketch-animated-webp = { module = "io.github.panpf.sketch4:sketch-animated-webp", version.ref = "sketch" }
sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" } sketch-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" }
tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" } tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" }
zoomimage-compose-sketch4 = { module = "io.github.panpf.zoomimage:zoomimage-compose-sketch4", version.ref = "zoomimage" }
[plugins] [plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" } androidApplication = { id = "com.android.application", version.ref = "agp" }