Compare commits
5 Commits
d47a38bc19
...
ffafee550e
| Author | SHA1 | Date | |
|---|---|---|---|
| ffafee550e | |||
| 70c97ed9d5 | |||
| d3ebd6829b | |||
| b8509381d9 | |||
| cec8debff0 |
@ -105,6 +105,9 @@ dependencies {
|
||||
implementation(libs.camera.view)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
|
||||
// 日期记录器:详情页灯箱图片缩放/旋转(与 sketch 同作者 panpf,SketchZoomAsyncImage 是 AsyncImage 超集)
|
||||
implementation(libs.zoomimage.compose.sketch4)
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${libs.versions.kotlin.get()}")
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.room.testing)
|
||||
|
||||
@ -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) {
|
||||
update {
|
||||
|
||||
72
core/src/main/kotlin/plus/rua/project/ui/ImageLightbox.kt
Normal file
72
core/src/main/kotlin/plus/rua/project/ui/ImageLightbox.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.ChevronLeft
|
||||
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.RotateLeft
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import kotlin.math.absoluteValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
@ -158,6 +160,7 @@ fun PhotoEditorScreen(
|
||||
onTabChange = { activeTab = it },
|
||||
onRotate = viewModel::rotate,
|
||||
onCropToggle = viewModel::toggleCrop,
|
||||
onApplyCrop = viewModel::applyCrop,
|
||||
onCropChange = viewModel::updateCrop,
|
||||
onAddPoint = viewModel::addStrokePoint,
|
||||
onEndStroke = viewModel::endStroke,
|
||||
@ -182,6 +185,7 @@ private fun EditorBody(
|
||||
onTabChange: (EditTab) -> Unit,
|
||||
onRotate: (Int) -> Unit,
|
||||
onCropToggle: () -> Unit,
|
||||
onApplyCrop: () -> Unit,
|
||||
onCropChange: (Float, Float, Float, Float) -> Unit,
|
||||
onAddPoint: (Offset) -> Unit,
|
||||
onEndStroke: () -> Unit,
|
||||
@ -246,6 +250,7 @@ private fun EditorBody(
|
||||
state = state,
|
||||
onRotate = onRotate,
|
||||
onCropToggle = onCropToggle,
|
||||
onApplyCrop = onApplyCrop,
|
||||
onUndoStroke = onUndoStroke,
|
||||
onStrokeColorChange = onStrokeColorChange,
|
||||
modifier = Modifier
|
||||
@ -265,8 +270,6 @@ private fun EditableImage(
|
||||
onDisplaySizeChange: (Float, Float) -> Unit
|
||||
) {
|
||||
val rotatedBmp = remember(state.rotationDegrees, state.sourceBitmap) { state.rotatedBitmap }
|
||||
// 裁剪框拖动手柄(CROP 模式下使用)
|
||||
var dragHandle by remember { mutableStateOf<CropHandle?>(null) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@ -299,8 +302,6 @@ private fun EditableImage(
|
||||
if (mode == EditTab.CROP && state.cropEnabled) {
|
||||
CropOverlay(
|
||||
state = state,
|
||||
dragHandle = dragHandle,
|
||||
onHandleChange = { dragHandle = it },
|
||||
onCropChange = onCropChange,
|
||||
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 回传。 */
|
||||
private data class CropRect(
|
||||
@ -334,16 +335,120 @@ private data class CropRect(
|
||||
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
|
||||
private fun CropOverlay(
|
||||
state: PhotoEditorState,
|
||||
dragHandle: CropHandle?,
|
||||
onHandleChange: (CropHandle?) -> Unit,
|
||||
onCropChange: (Float, Float, Float, Float) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val left = state.cropLeft ?: 0.1f
|
||||
val right = state.cropRight ?: 0.9f
|
||||
var dragHandle by remember { mutableStateOf(CropHandle.NONE) }
|
||||
// 拖动期间的本地累积值;非拖动时为 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(
|
||||
modifier = modifier.pointerInput(Unit) {
|
||||
detectDragGestures(
|
||||
@ -351,45 +456,47 @@ private fun CropOverlay(
|
||||
val size = this.size
|
||||
val ox = offset.x / size.width
|
||||
val oy = offset.y / size.height
|
||||
onHandleChange(
|
||||
when {
|
||||
ox < 0.2f && oy < 0.2f -> CropHandle.TOP_LEFT
|
||||
ox > 0.8f && oy < 0.2f -> CropHandle.TOP_RIGHT
|
||||
ox < 0.2f && oy > 0.8f -> CropHandle.BOTTOM_LEFT
|
||||
ox > 0.8f && oy > 0.8f -> CropHandle.BOTTOM_RIGHT
|
||||
else -> CropHandle.NONE
|
||||
}
|
||||
)
|
||||
val baseL = currentState.cropLeft ?: 0.1f
|
||||
val baseT = currentState.cropTop
|
||||
val baseR = currentState.cropRight ?: 0.9f
|
||||
val baseB = currentState.cropBottom
|
||||
// 进入拖动:把 state 当前值快照到本地累积
|
||||
dragLeft = baseL
|
||||
dragTop = baseT
|
||||
dragRight = baseR
|
||||
dragBottom = baseB
|
||||
dragHandle = hitHandle(ox, oy, baseL, baseT, baseR, baseB)
|
||||
},
|
||||
onDrag = { change, drag ->
|
||||
if (dragHandle == CropHandle.NONE) return@detectDragGestures
|
||||
change.consume()
|
||||
val size = this.size
|
||||
val dx = drag.x / size.width
|
||||
val dy = drag.y / size.height
|
||||
val (nL, nT, nR, nB) = when (dragHandle) {
|
||||
CropHandle.TOP_LEFT ->
|
||||
CropRect(left + dx, state.cropTop + dy, right, state.cropBottom)
|
||||
CropHandle.TOP_RIGHT ->
|
||||
CropRect(left, state.cropTop + dy, right + dx, state.cropBottom)
|
||||
CropHandle.BOTTOM_LEFT ->
|
||||
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)
|
||||
// 在本地累积值上叠加增量,不触发 StateFlow 往返
|
||||
val cur = moveCrop(dx, dy, dragLeft, dragTop, dragRight, dragBottom, dragHandle)
|
||||
dragLeft = cur.left
|
||||
dragTop = cur.top
|
||||
dragRight = cur.right
|
||||
dragBottom = cur.bottom
|
||||
},
|
||||
onDragEnd = { onHandleChange(null) },
|
||||
onDragCancel = { onHandleChange(null) }
|
||||
onDragEnd = {
|
||||
// 拖动结束:一次性提交回 ViewModel
|
||||
if (dragHandle != CropHandle.NONE) {
|
||||
onCropChange(dragLeft, dragTop, dragRight, dragBottom)
|
||||
}
|
||||
dragHandle = CropHandle.NONE
|
||||
},
|
||||
onDragCancel = { dragHandle = CropHandle.NONE }
|
||||
)
|
||||
}
|
||||
) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
val l = left * w
|
||||
val t = state.cropTop * h
|
||||
val t = top * h
|
||||
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(r, 0f), size = Size(w - r, h))
|
||||
@ -411,6 +518,7 @@ private fun ToolBar(
|
||||
state: PhotoEditorState,
|
||||
onRotate: (Int) -> Unit,
|
||||
onCropToggle: () -> Unit,
|
||||
onApplyCrop: () -> Unit,
|
||||
onUndoStroke: () -> Unit,
|
||||
onStrokeColorChange: (Color) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@ -429,12 +537,15 @@ private fun ToolBar(
|
||||
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) {
|
||||
Icon(
|
||||
if (state.cropEnabled) Icons.Filled.CropFree else Icons.Filled.Crop,
|
||||
contentDescription = if (state.cropEnabled) "关闭裁剪" else "开启裁剪"
|
||||
)
|
||||
Icon(Icons.Filled.Crop, contentDescription = "进入裁剪")
|
||||
}
|
||||
}
|
||||
EditTab.HANDWRITE -> {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -81,6 +83,7 @@ fun RecordDetailScreen(
|
||||
)
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var showLightbox by remember { mutableStateOf(false) }
|
||||
|
||||
// 删除完成后自动返回
|
||||
if (state.deleted) {
|
||||
@ -142,6 +145,7 @@ fun RecordDetailScreen(
|
||||
|
||||
else -> DetailContent(
|
||||
state = state,
|
||||
onPhotoClick = { showLightbox = true },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
@ -165,11 +169,20 @@ fun RecordDetailScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showLightbox) {
|
||||
ImageLightbox(
|
||||
photoUri = state.photoUri,
|
||||
contentDescription = state.record?.title,
|
||||
onDismiss = { showLightbox = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailContent(
|
||||
state: DateRecordDetailUiState,
|
||||
onPhotoClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val record = state.record ?: return
|
||||
@ -183,6 +196,11 @@ private fun DetailContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onPhotoClick
|
||||
)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||
|
||||
@ -22,6 +22,7 @@ sketch = "4.4.0"
|
||||
spotless = "8.8.0"
|
||||
tyme4kt = "1.5.0"
|
||||
versions = "0.54.0"
|
||||
zoomimage = "1.6.0"
|
||||
|
||||
[libraries]
|
||||
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-compose = { module = "io.github.panpf.sketch4:sketch-compose", version.ref = "sketch" }
|
||||
tyme4kt = { module = "cn.6tail:tyme4kt", version.ref = "tyme4kt" }
|
||||
zoomimage-compose-sketch4 = { module = "io.github.panpf.zoomimage:zoomimage-compose-sketch4", version.ref = "zoomimage" }
|
||||
|
||||
[plugins]
|
||||
androidApplication = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user