From 51e387ea71f78f5cc54a76afda9a9a21ec4ea612 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:20:17 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=EA=B5=AC=EA=B0=84=20=EC=B5=9C?= =?UTF-8?q?=EB=8C=80=2030=EC=B4=88=20=EB=8F=84=EB=8B=AC=20=EC=8B=9C=20-1s/?= =?UTF-8?q?+1s=20=EB=B2=84=ED=8A=BC=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WriteDiaryScreen/KillingPartSelector.kt | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index d6d1ade..040d3c6 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -480,16 +480,23 @@ fun KillingPartSelector( Spacer(modifier = Modifier.height(6.dp)) // ---- -1s / +1s 버튼: 양쪽 끝 고정 (초 텍스트는 핸들 따라 이동) ---- + // 최대 구간(30s)에 도달했거나 트랙 경계에 닿으면 비활성화 + val durationEps = 0.05f + val atMaxDuration = (endSec - startSec) >= maxDurationSec - durationEps + val canExtendFront = !atMaxDuration && startSec > durationEps + val canExtendBack = !atMaxDuration && endSec < totalDuration.toFloat() - durationEps Box(modifier = Modifier.fillMaxWidth().height(edgeButtonSize)) { EdgeStepButton( label = "-1s", size = edgeButtonSize, + enabled = canExtendFront, modifier = Modifier.align(Alignment.CenterStart), onClick = { extendFront() } ) EdgeStepButton( label = "+1s", size = edgeButtonSize, + enabled = canExtendBack, modifier = Modifier.align(Alignment.CenterEnd), onClick = { extendBack() } ) @@ -597,26 +604,36 @@ private fun EdgeStepButton( label: String, size: androidx.compose.ui.unit.Dp, modifier: Modifier = Modifier, + enabled: Boolean = true, onClick: () -> Unit ) { var pressed by remember { mutableStateOf(false) } - val alpha by animateFloatAsState(if (pressed) 0.5f else 1f, label = "stepBtnAlpha") + val alpha by animateFloatAsState( + when { + !enabled -> 0.3f + pressed -> 0.5f + else -> 1f + }, + label = "stepBtnAlpha" + ) Box( modifier = modifier .size(size) .graphicsLayer { this.alpha = alpha } .clip(CircleShape) - .background(mainGreen) - .pointerInput(Unit) { - detectTapGestures( - onPress = { - pressed = true - tryAwaitRelease() - pressed = false - }, - onTap = { onClick() } - ) - }, + .background(if (enabled) mainGreen else Color(0xFF6E6E6E)) + .then( + if (enabled) Modifier.pointerInput(Unit) { + detectTapGestures( + onPress = { + pressed = true + tryAwaitRelease() + pressed = false + }, + onTap = { onClick() } + ) + } else Modifier + ), contentAlignment = Alignment.Center ) { Text( From 235044b5b1fe73d888d329a61c26843d0fd16956 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:21:45 +0900 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=EA=B5=AC=EA=B0=84=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=88=20=ED=9B=84=20=EC=A4=91=EC=95=99=20=EB=B3=B5=EA=B7=80?= =?UTF-8?q?=20=EB=AA=A8=EC=85=98=EC=97=90=200.5=EC=B4=88=20=EB=94=9C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/WriteDiaryScreen/KillingPartSelector.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index 040d3c6..52cbadb 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -68,7 +68,7 @@ fun formatTime(seconds: Float): String { * - secToX(sec) : viewportWidth/2 + (sec - viewCenterSec) * pxPerSec * * 인터랙션 - * - 핸들 드래그: 구간 리사이즈. 뗀 후 0.4초 tween 으로 구간 중앙이 뷰 중앙으로 복귀 + * - 핸들 드래그: 구간 리사이즈. 뗀 후 0.5초 대기 → 0.4초 tween 으로 구간 중앙이 뷰 중앙으로 복귀 * - 좌측 핸들 탭: 구간 처음부터 재생(onSeek) * - 핸들 0.5초 롱프레스: 핸들 옆 2초 루프 활성(onLoopChange), 떼거나 움직이면 해제 * - 트랙 탭: 그 지점부터 재생(onSeek) @@ -80,6 +80,9 @@ fun formatTime(seconds: Float): String { private enum class HandleSide { LEFT, RIGHT } +/** 구간 조절 후 중앙 복귀 모션이 시작되기 전 대기 시간 */ +private const val recenterDelayMillis = 500 + @Composable fun KillingPartSelector( totalDuration: Int, @@ -151,11 +154,15 @@ fun KillingPartSelector( } } - fun recenter(animated: Boolean = true) { + /** 구간 조절이 끝난 뒤 [delayMillis] 만큼 쉬었다가 구간 중앙을 뷰 중앙으로 복귀 */ + fun recenter(animated: Boolean = true, delayMillis: Int = recenterDelayMillis) { val target = (startSec + endSec) / 2f scope.launch { if (animated) { - viewCenterSec.animateTo(target, tween(durationMillis = 400, easing = LinearEasing)) + viewCenterSec.animateTo( + target, + tween(durationMillis = 400, delayMillis = delayMillis, easing = LinearEasing) + ) } else { viewCenterSec.snapTo(target) } From 0bd8a4231ed918d5a642210c831762a2a9246516 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:29:18 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=ED=95=B8=EB=93=A4=20=EB=A1=B1?= =?UTF-8?q?=ED=94=84=EB=A0=88=EC=8A=A4=202=EC=B4=88=20=EB=A3=A8=ED=94=84?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=81=ED=99=94=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WriteDiaryScreen/KillingPartSelector.kt | 127 +++++++++++++++--- 1 file changed, 111 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index 52cbadb..624e3f0 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -2,7 +2,11 @@ package com.killingpart.killingpoint.ui.screen.WriteDiaryScreen import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -70,7 +74,9 @@ fun formatTime(seconds: Float): String { * 인터랙션 * - 핸들 드래그: 구간 리사이즈. 뗀 후 0.5초 대기 → 0.4초 tween 으로 구간 중앙이 뷰 중앙으로 복귀 * - 좌측 핸들 탭: 구간 처음부터 재생(onSeek) - * - 핸들 0.5초 롱프레스: 핸들 옆 2초 루프 활성(onLoopChange), 떼거나 움직이면 해제 + * - 핸들 0.5초 롱프레스: 선택 구간 안쪽 2초 루프 활성(onLoopChange), 떼거나 움직이면 해제 + * 좌핸들 = start~start+2, 우핸들 = end-2~end + * 루프 중에는 핸들이 바깥으로 비켜서고 2초 밴드/배지가 강조 표시됨 * - 트랙 탭: 그 지점부터 재생(onSeek) * - -1s/+1s: 구간 앞/뒤 1초 확장(재생 유지), 조정 후 중앙 복귀 * @@ -83,6 +89,9 @@ private enum class HandleSide { LEFT, RIGHT } /** 구간 조절 후 중앙 복귀 모션이 시작되기 전 대기 시간 */ private const val recenterDelayMillis = 500 +/** 핸들 롱프레스 시 반복 재생되는 구간 길이(초) */ +private const val loopWindowSec = 2f + @Composable fun KillingPartSelector( totalDuration: Int, @@ -229,8 +238,8 @@ fun KillingPartSelector( fun activateLoop(side: HandleSide) { loopSide = side val (ls, le) = when (side) { - HandleSide.LEFT -> startSec to (startSec + 2f).coerceAtMost(endSec) - HandleSide.RIGHT -> (endSec - 2f).coerceAtLeast(startSec) to endSec + HandleSide.LEFT -> startSec to (startSec + loopWindowSec).coerceAtMost(endSec) + HandleSide.RIGHT -> (endSec - loopWindowSec).coerceAtLeast(startSec) to endSec } latestOnLoopChange(ls, le) } @@ -242,6 +251,41 @@ fun KillingPartSelector( } } + // ---- 2초 루프 구간(절대 초). 두 핸들 모두 선택 구간 안쪽으로 잡는다 ---- + val loopStartSec = when (loopSide) { + HandleSide.LEFT -> startSec + HandleSide.RIGHT -> (endSec - loopWindowSec).coerceAtLeast(startSec) + null -> Float.NaN + } + val loopEndSec = when (loopSide) { + HandleSide.LEFT -> (startSec + loopWindowSec).coerceAtMost(endSec) + HandleSide.RIGHT -> endSec + null -> Float.NaN + } + + // 루프 중에는 해당 핸들이 2초 밴드를 가리지 않도록 바깥쪽으로 비켜선다. + // (2초 폭 ≈ 19dp < 핸들 폭 22dp 라 비키지 않으면 밴드가 완전히 가려짐) + val leftHandleShiftPx by animateFloatAsState( + if (loopSide == HandleSide.LEFT) -handleWidthPx else 0f, + label = "leftHandleShift" + ) + val rightHandleShiftPx by animateFloatAsState( + if (loopSide == HandleSide.RIGHT) handleWidthPx else 0f, + label = "rightHandleShift" + ) + + // 루프 밴드 깜빡임(강조) + val loopPulseTransition = rememberInfiniteTransition(label = "loopPulse") + val loopPulse by loopPulseTransition.animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ), + label = "loopPulseValue" + ) + fun extendFront() { val newStart = (startSec - 1f) .coerceAtLeast(0f) @@ -299,16 +343,6 @@ fun KillingPartSelector( val stepSec = (barWidthPx + barGapPx) / pxPerSec val eps = 0.001f - val loopStartSec = when (loopSide) { - HandleSide.LEFT -> startSec - HandleSide.RIGHT -> (endSec - 2f).coerceAtLeast(startSec) - null -> Float.NaN - } - val loopEndSec = when (loopSide) { - HandleSide.LEFT -> (startSec + 2f).coerceAtMost(endSec) - HandleSide.RIGHT -> endSec - null -> Float.NaN - } var i = 0 var sec = 0f @@ -355,15 +389,41 @@ fun KillingPartSelector( } // 루프 2초 밴드 하이라이트 (박스 컨테이너 라운드에 맞춰 클립) + // - 밴드 밖 선택 구간은 어둡게 덮어 2초 구간만 도드라지게 + // - 밴드는 펄스로 밝기가 오가고, 양 끝에 경계선을 그려 루프 범위를 명확히 if (loopSide != null) { val lx = sx(loopStartSec) val rx = sx(loopEndSec) clipPath(boxPath) { + // 루프 밖 구간 디밍 drawRect( - color = mainGreen.copy(alpha = 0.28f), + color = Color(0xFF060606).copy(alpha = 0.55f), + topLeft = Offset(boxLeft, boxTop), + size = Size((lx - boxLeft).coerceAtLeast(0f), bH) + ) + drawRect( + color = Color(0xFF060606).copy(alpha = 0.55f), + topLeft = Offset(rx, boxTop), + size = Size((boxRight - rx).coerceAtLeast(0f), bH) + ) + // 2초 밴드 + drawRect( + color = mainGreen.copy(alpha = 0.22f + 0.20f * loopPulse), topLeft = Offset(lx, boxTop), size = Size((rx - lx).coerceAtLeast(0f), bH) ) + // 밴드 경계선 + val edgePx = with(density) { 2.dp.toPx() } + drawRect( + color = mainGreen, + topLeft = Offset(lx, boxTop), + size = Size(edgePx, bH) + ) + drawRect( + color = mainGreen, + topLeft = Offset(rx - edgePx, boxTop), + size = Size(edgePx, bH) + ) } } @@ -400,8 +460,9 @@ fun KillingPartSelector( modifier = Modifier .offset { // 박스 컨테이너 안쪽: 핸들 왼쪽 끝이 구간 시작(박스 좌측)에 맞도록 + // 루프 중에는 밴드가 보이도록 핸들 폭만큼 왼쪽으로 비켜섬 IntOffset( - secToX(startSec).roundToInt(), + (secToX(startSec) + leftHandleShiftPx).roundToInt(), 0 ) } @@ -431,8 +492,9 @@ fun KillingPartSelector( modifier = Modifier .offset { // 박스 컨테이너 안쪽: 핸들 오른쪽 끝이 구간 끝(박스 우측)에 맞도록 + // 루프 중에는 밴드가 보이도록 핸들 폭만큼 오른쪽으로 비켜섬 IntOffset( - (secToX(endSec) - handleWidthPx).roundToInt(), + (secToX(endSec) - handleWidthPx + rightHandleShiftPx).roundToInt(), 0 ) } @@ -453,6 +515,18 @@ fun KillingPartSelector( ) ) + // ---- 루프 배지: 2초 밴드 위에 "2초 반복" 표시 ---- + if (loopSide != null && viewportWidthPx > 0f) { + val bandCenterX = secToX((loopStartSec + loopEndSec) / 2f) + LoopBadge( + modifier = Modifier + .align(Alignment.TopCenter) + .zIndex(11f) + .offset { + IntOffset((bandCenterX - viewportWidthPx / 2f).roundToInt(), 0) + } + ) + } } Spacer(modifier = Modifier.height(4.dp)) @@ -593,6 +667,27 @@ private fun HandleView( } } +/** 롱프레스 2초 루프 중 밴드 위에 뜨는 배지 */ +@Composable +private fun LoopBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .height(15.dp) + .clip(RoundedCornerShape(8.dp)) + .background(mainGreen) + .padding(horizontal = 6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "2초 반복", + fontFamily = PaperlogyFontFamily, + fontWeight = FontWeight.W700, + fontSize = 8.sp, + color = Color(0xFF0A0A0A) + ) + } +} + @Composable private fun HandleTimeLabel(text: String, modifier: Modifier = Modifier) { Box(modifier = modifier.width(76.dp), contentAlignment = Alignment.Center) { From 692fc2bb4d2966eeab12de4887c7daa3a44cbf31 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:47:17 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=8A=A4=ED=8E=99=ED=8A=B8?= =?UTF-8?q?=EB=9F=BC=EB=B0=94=20=EA=B0=80=EB=A1=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WriteDiaryScreen/KillingPartSelector.kt | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index 624e3f0..b187cd6 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -78,6 +78,8 @@ fun formatTime(seconds: Float): String { * 좌핸들 = start~start+2, 우핸들 = end-2~end * 루프 중에는 핸들이 바깥으로 비켜서고 2초 밴드/배지가 강조 표시됨 * - 트랙 탭: 그 지점부터 재생(onSeek) + * - 트랙 가로 드래그: 구간 길이를 유지한 채 스펙트럼바 스크롤(구간은 화면 중앙 고정), + * 뗀 뒤 새 구간 시작부터 다시 재생 (미니맵 스크럽과 동일) * - -1s/+1s: 구간 앞/뒤 1초 확장(재생 유지), 조정 후 중앙 복귀 * * 재생 표시 @@ -131,6 +133,9 @@ fun KillingPartSelector( val barWidthPx = with(density) { barWidth.toPx() } val barGapPx = with(density) { barGap.toPx() } val handleWidthPx = with(density) { handleWidth.toPx() } + val handleHeightPx = with(density) { handleHeight.toPx() } + val trackHeightPx = with(density) { trackHeight.toPx() } + val handleTouchPadPx = with(density) { 4.dp.toPx() } // ---- 상태 ---- var viewportWidthPx by remember { mutableStateOf(0f) } @@ -308,6 +313,19 @@ fun KillingPartSelector( } } + /** 스펙트럼바 가로 스크롤: 구간 길이는 유지한 채 통째로 이동(미니맵 스크럽과 동일 동작) */ + fun panSectionTo(newStartSec: Float) { + val dur = endSec - startSec + val ns = newStartSec.coerceIn(0f, (totalDuration.toFloat() - dur).coerceAtLeast(0f)) + if (ns != startSec) { + startSec = ns + endSec = ns + dur + commit() + } + // 구간은 항상 화면 중앙에 고정된 채 파형만 흐르도록 + recenter(animated = false) + } + Column(modifier = Modifier.fillMaxWidth()) { // ===== 메인 트랙 ===== @@ -322,12 +340,63 @@ fun KillingPartSelector( if (initialized) pxPerSec = w / visibleSeconds } } - // 트랙 탭 → 그 지점부터 재생 + // 트랙 탭 → 그 지점부터 재생 / 가로 드래그 → 스펙트럼바 스크롤 .pointerInput(Unit) { - detectTapGestures { pos -> - val sec = xToSec(pos.x) - if (sec in startSec..endSec) { - latestOnSeek(sec) + val slop = viewConfiguration.touchSlop + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + // 핸들 위에서 시작한 제스처는 핸들(리사이즈/루프)이 처리 + if (isOnHandle( + pos = down.position, + leftHandleLeftX = secToX(startSec) + leftHandleShiftPx, + rightHandleLeftX = secToX(endSec) - handleWidthPx + rightHandleShiftPx, + handleWidthPx = handleWidthPx, + handleHeightPx = handleHeightPx, + trackHeightPx = trackHeightPx, + padPx = handleTouchPadPx + ) + ) return@awaitEachGesture + var panning = false + var totalDx = 0f + var accStartSec = 0f + var canceled = false + + while (true) { + val event = awaitPointerEvent() + val ch = event.changes.firstOrNull { it.id == down.id } ?: break + // 핸들 등 자식이 가져간 제스처는 트랙에서 처리하지 않음 + if (ch.isConsumed && !panning) { canceled = true; break } + if (!ch.pressed) break + + val dx = ch.positionChange().x + totalDx += dx + if (!panning && abs(totalDx) > slop && pxPerSec > 0f) { + panning = true + accStartSec = startSec + } + if (panning) { + // 오른쪽으로 끌면 이전 시간대가 보이도록(파형이 따라옴) + accStartSec -= dx / pxPerSec + panSectionTo(accStartSec) + ch.consume() + } + } + + when { + canceled -> Unit + panning -> { + commit(force = true) + // 새 구간 시작부터 다시 미리듣기 + latestOnSeek(startSec) + latestOnHandleAdjusted?.invoke(KillingPartHandle.SPECTRUM_BAR) + } + else -> { + // 탭: 누른 지점부터 미리듣기 + val sec = xToSec(down.position.x) + if (sec in startSec..endSec) { + latestOnSeek(sec) + } + } } } } @@ -631,6 +700,24 @@ fun KillingPartSelector( } } +/** 트랙 위 좌표가 좌/우 핸들 영역(여유 [padPx] 포함) 안인지 */ +private fun isOnHandle( + pos: Offset, + leftHandleLeftX: Float, + rightHandleLeftX: Float, + handleWidthPx: Float, + handleHeightPx: Float, + trackHeightPx: Float, + padPx: Float +): Boolean { + val top = (trackHeightPx - handleHeightPx) / 2f - padPx + val bottom = trackHeightPx - top + if (pos.y < top || pos.y > bottom) return false + val inLeft = pos.x >= leftHandleLeftX - padPx && pos.x <= leftHandleLeftX + handleWidthPx + padPx + val inRight = pos.x >= rightHandleLeftX - padPx && pos.x <= rightHandleLeftX + handleWidthPx + padPx + return inLeft || inRight +} + /** 핸들 시간 라벨 상수 (분석 이벤트 handle_side 값) */ private object KillingPartHandle { const val LEFT = "left" From bde679935259442720ba56d7529ae3b6bd6cacf1 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:49:06 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=A2=8C=EC=B8=A1=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=20=EC=A1=B0=EC=A0=88=20=EC=A2=85=EB=A3=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=EC=A0=90=EB=B6=80=ED=84=B0=20=EC=9E=AC?= =?UTF-8?q?=EC=83=9D=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/WriteDiaryScreen/KillingPartSelector.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index b187cd6..0adc689 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -73,6 +73,7 @@ fun formatTime(seconds: Float): String { * * 인터랙션 * - 핸들 드래그: 구간 리사이즈. 뗀 후 0.5초 대기 → 0.4초 tween 으로 구간 중앙이 뷰 중앙으로 복귀 + * 좌핸들은 뗄 때 새 시작점부터 재생 새로고침(onSeek), 우핸들은 재생 유지 * - 좌측 핸들 탭: 구간 처음부터 재생(onSeek) * - 핸들 0.5초 롱프레스: 선택 구간 안쪽 2초 루프 활성(onLoopChange), 떼거나 움직이면 해제 * 좌핸들 = start~start+2, 우핸들 = end-2~end @@ -547,6 +548,8 @@ fun KillingPartSelector( onDragEnd = { commit(force = true) recenter() + // 좌측 핸들 조절이 끝나면 새 시작점부터 다시 미리듣기 + latestOnSeek(startSec) latestOnHandleAdjusted?.invoke(KillingPartHandle.LEFT) } ) From 666cf6215bd28a8663c8bfb562ca7ec0e2d361cd Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:56:01 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=EA=B5=AC=EA=B0=84=EC=9E=90=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20UI=20=ED=95=9C=20=ED=99=94=EB=A9=B4=EC=97=90=20?= =?UTF-8?q?=EB=AF=B8=EB=8B=88=EB=A7=B5=EA=B9=8C=EC=A7=80=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EC=98=A4=EB=8F=84=EB=A1=9D=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WriteDiaryScreen/KillingPartSelector.kt | 80 ++++++++----------- .../WriteDiaryScreen/SelectDurationScreen.kt | 6 +- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index 0adc689..9109437 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -127,7 +127,7 @@ fun KillingPartSelector( val handleHeight = 74.dp val handleCorner = 7.dp val boxCorner = 12.dp - val edgeButtonSize = 32.dp + val edgeButtonSize = 36.dp val barWidth = 3.dp val barGap = 4.dp @@ -587,58 +587,41 @@ fun KillingPartSelector( ) ) - // ---- 루프 배지: 2초 밴드 위에 "2초 반복" 표시 ---- - if (loopSide != null && viewportWidthPx > 0f) { - val bandCenterX = secToX((loopStartSec + loopEndSec) / 2f) - LoopBadge( - modifier = Modifier - .align(Alignment.TopCenter) - .zIndex(11f) - .offset { - IntOffset((bandCenterX - viewportWidthPx / 2f).roundToInt(), 0) - } - ) - } } Spacer(modifier = Modifier.height(4.dp)) - // ---- 핸들 시간 라벨 (핸들 위치 따라, 살짝 위로) ---- + // ---- 시간 라벨 + -1s/+1s 버튼: 같은 줄에 배치 ---- + // 라벨은 핸들 위치를 따라 이동하되 양끝 버튼 영역은 침범하지 않도록 클램프 + // 최대 구간(30s)에 도달했거나 트랙 경계에 닿으면 버튼 비활성화 + val durationEps = 0.05f + val atMaxDuration = (endSec - startSec) >= maxDurationSec - durationEps + val canExtendFront = !atMaxDuration && startSec > durationEps + val canExtendBack = !atMaxDuration && endSec < totalDuration.toFloat() - durationEps + val labelHalfPx = with(density) { 38.dp.toPx() } - Box(modifier = Modifier.fillMaxWidth().height(16.dp)) { + val edgeButtonPx = with(density) { edgeButtonSize.toPx() } + val labelGapPx = with(density) { 2.dp.toPx() } + fun labelOffsetX(sec: Float): Int { + val minX = edgeButtonPx + labelGapPx + val maxX = (viewportWidthPx - edgeButtonPx - labelGapPx - labelHalfPx * 2f) + .coerceAtLeast(minX) + return (secToX(sec) - labelHalfPx).coerceIn(minX, maxX).roundToInt() + } + + Box(modifier = Modifier.fillMaxWidth().height(edgeButtonSize)) { HandleTimeLabel( text = formatTime(startSec), - modifier = Modifier.offset { - IntOffset( - (secToX(startSec) - labelHalfPx) - .coerceIn(0f, (viewportWidthPx - labelHalfPx * 2f).coerceAtLeast(0f)) - .roundToInt(), - 0 - ) - } + modifier = Modifier + .align(Alignment.CenterStart) + .offset { IntOffset(labelOffsetX(startSec), 0) } ) HandleTimeLabel( text = formatTime(endSec), - modifier = Modifier.offset { - IntOffset( - (secToX(endSec) - labelHalfPx) - .coerceIn(0f, (viewportWidthPx - labelHalfPx * 2f).coerceAtLeast(0f)) - .roundToInt(), - 0 - ) - } + modifier = Modifier + .align(Alignment.CenterStart) + .offset { IntOffset(labelOffsetX(endSec), 0) } ) - } - - Spacer(modifier = Modifier.height(6.dp)) - - // ---- -1s / +1s 버튼: 양쪽 끝 고정 (초 텍스트는 핸들 따라 이동) ---- - // 최대 구간(30s)에 도달했거나 트랙 경계에 닿으면 비활성화 - val durationEps = 0.05f - val atMaxDuration = (endSec - startSec) >= maxDurationSec - durationEps - val canExtendFront = !atMaxDuration && startSec > durationEps - val canExtendBack = !atMaxDuration && endSec < totalDuration.toFloat() - durationEps - Box(modifier = Modifier.fillMaxWidth().height(edgeButtonSize)) { EdgeStepButton( label = "-1s", size = edgeButtonSize, @@ -655,10 +638,11 @@ fun KillingPartSelector( ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(10.dp)) - // ===== 미니맵 ===== + // ===== 미니맵 (가리지 않도록 한 줄 통째로 사용) ===== MiniMap( + modifier = Modifier.fillMaxWidth(), totalDuration = totalDuration, startSec = startSec, endSec = endSec, @@ -678,7 +662,7 @@ fun KillingPartSelector( } ) - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp), @@ -844,7 +828,8 @@ private fun MiniMap( startSec: Float, endSec: Float, onScrub: (newStartSec: Float) -> Unit, - onScrubEnd: () -> Unit + onScrubEnd: () -> Unit, + modifier: Modifier = Modifier ) { val density = LocalDensity.current var widthPx by remember { mutableStateOf(0f) } @@ -852,8 +837,7 @@ private fun MiniMap( val latestEndSec by rememberUpdatedState(endSec) Box( - modifier = Modifier - .fillMaxWidth() + modifier = modifier .height(44.dp) .background(Color(0xFF1F1F1F), RoundedCornerShape(22.dp)) .border(1.dp, Color(0xFF3A3A3A), RoundedCornerShape(22.dp)) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/SelectDurationScreen.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/SelectDurationScreen.kt index e9eaeed..e73a653 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/SelectDurationScreen.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/SelectDurationScreen.kt @@ -409,7 +409,7 @@ fun SelectDurationScreen( } ) } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) if (candidateVideos.isNotEmpty()) { val toggleColor = if (isCandidateExpanded) Color(0xFFD9D9D9) else Color(0xFF878787) @@ -515,10 +515,10 @@ fun SelectDurationScreen( } } - Spacer(Modifier.height(if (tutorialMode) 20.dp else 38.dp)) + Spacer(Modifier.height(if (tutorialMode) 16.dp else 20.dp)) } - Spacer(modifier = Modifier.height(80.dp)) + Spacer(modifier = Modifier.height(24.dp)) } } From 912abe408089256082fa4d5304e7d107e39d1975 Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 16:58:25 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=202=EC=B4=88=20=EB=A3=A8=ED=94=84=20?= =?UTF-8?q?=EB=B0=B0=EC=A7=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WriteDiaryScreen/KillingPartSelector.kt | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt index 9109437..2d5bada 100644 --- a/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt +++ b/app/src/main/java/com/killingpart/killingpoint/ui/screen/WriteDiaryScreen/KillingPartSelector.kt @@ -77,7 +77,7 @@ fun formatTime(seconds: Float): String { * - 좌측 핸들 탭: 구간 처음부터 재생(onSeek) * - 핸들 0.5초 롱프레스: 선택 구간 안쪽 2초 루프 활성(onLoopChange), 떼거나 움직이면 해제 * 좌핸들 = start~start+2, 우핸들 = end-2~end - * 루프 중에는 핸들이 바깥으로 비켜서고 2초 밴드/배지가 강조 표시됨 + * 루프 중에는 핸들이 바깥으로 비켜서고 2초 밴드가 강조 표시됨 * - 트랙 탭: 그 지점부터 재생(onSeek) * - 트랙 가로 드래그: 구간 길이를 유지한 채 스펙트럼바 스크롤(구간은 화면 중앙 고정), * 뗀 뒤 새 구간 시작부터 다시 재생 (미니맵 스크럽과 동일) @@ -741,27 +741,6 @@ private fun HandleView( } } -/** 롱프레스 2초 루프 중 밴드 위에 뜨는 배지 */ -@Composable -private fun LoopBadge(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .height(15.dp) - .clip(RoundedCornerShape(8.dp)) - .background(mainGreen) - .padding(horizontal = 6.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "2초 반복", - fontFamily = PaperlogyFontFamily, - fontWeight = FontWeight.W700, - fontSize = 8.sp, - color = Color(0xFF0A0A0A) - ) - } -} - @Composable private fun HandleTimeLabel(text: String, modifier: Modifier = Modifier) { Box(modifier = modifier.width(76.dp), contentAlignment = Alignment.Center) { From 2e3fbdfdd17b184baed6173e7ce12f00607eeedd Mon Sep 17 00:00:00 2001 From: latteeea Date: Fri, 7 Aug 2026 17:01:16 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20=EB=B2=84=EC=A0=84=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B3=80=EA=B2=BD(2.3.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca90936..a4e9960 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.killingpart.killingpoint" minSdk = 29 targetSdk = 36 - versionCode = 45 - versionName = "2.3.6" + versionCode = 46 + versionName = "2.3.7" buildConfigField("String", "AMPLITUDE_API_KEY", "\"fcb84a98b48f87f85e7112a1587976fd\"") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"