diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index c62e0b6..e45a04a 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -13,8 +15,8 @@ android { applicationId = "com.atridad.magiccounter" minSdk = 31 targetSdk = 36 - versionCode = 2 - versionName = "1.2.0" + versionCode = 3 + versionName = "1.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -34,9 +36,7 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = "17" - } + kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } java { toolchain { diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..8311c80 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt b/android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt index c8d18c3..2f7dc50 100644 --- a/android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt +++ b/android/app/src/main/java/com/atridad/magiccounter/ui/screens/GameScreen.kt @@ -15,6 +15,20 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.zIndex +import java.util.Collections +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.CancellationException import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete @@ -29,12 +43,17 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.Button import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf 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 androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -78,6 +97,14 @@ fun GameScreen( // State for stop game confirmation val showStopConfirm = remember { mutableStateOf(false) } + + // Local list for drag-and-drop reordering + val uiPlayers = remember { mutableStateListOf() } + + LaunchedEffect(state.players) { + uiPlayers.clear() + uiPlayers.addAll(state.players) + } // Initialize state.players.forEach { p -> @@ -100,7 +127,7 @@ fun GameScreen( } fun snapshotState(): GameState = state.copy( - players = state.players.map { p -> + players = uiPlayers.map { p -> PlayerState( id = p.id, name = p.name, @@ -167,19 +194,75 @@ fun GameScreen( gameLocked.value = true } - val displayPlayers = state.players.sortedBy { eliminated[it.id] == true } + val listState = rememberLazyListState() + var draggingItemIndex by remember { mutableStateOf(null) } + var draggingItemOffset by remember { mutableFloatStateOf(0f) } + val haptics = LocalHapticFeedback.current LazyColumn( + state = listState, modifier = Modifier.weight(1f), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - itemsIndexed(displayPlayers, key = { _, item -> item.id }) { index, player -> + itemsIndexed(uiPlayers, key = { _, item -> item.id }) { index, player -> val accent = seatAccentColor(index, MaterialTheme.colorScheme) val perPlayerCommander: Map = commanderDamages[player.id]?.toMap() ?: emptyMap() - PlayerCard( + + val isDragging = index == draggingItemIndex + val currentItemIndex by rememberUpdatedState(index) + + Box( + modifier = Modifier + .then(if (isDragging) Modifier else Modifier.animateItem()) + .zIndex(if (isDragging) 1f else 0f) + .graphicsLayer { + translationY = if (isDragging) draggingItemOffset else 0f + scaleX = if (isDragging) 1.05f else 1f + scaleY = if (isDragging) 1.05f else 1f + shadowElevation = if (isDragging) 8.dp.toPx() else 0f + } + .pointerInput(player.id) { + detectDragGesturesAfterShortPress( + onDragStart = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + draggingItemIndex = currentItemIndex + draggingItemOffset = 0f + }, + onDrag = { change, dragAmount -> + change.consume() + draggingItemOffset += dragAmount.y + + val currentIndex = currentItemIndex + val currentInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == currentIndex } + if (currentInfo != null) { + val currentCenter = currentInfo.offset + currentInfo.size / 2 + draggingItemOffset + val targetItem = listState.layoutInfo.visibleItemsInfo.find { + it.index != currentIndex && + currentCenter > it.offset && + currentCenter < (it.offset + it.size) + } + + if (targetItem != null) { + val targetIndex = targetItem.index + if (currentIndex < uiPlayers.size && targetIndex < uiPlayers.size) { + Collections.swap(uiPlayers, currentIndex, targetIndex) + draggingItemIndex = targetIndex + draggingItemOffset -= (targetItem.offset - currentInfo.offset) + } + } + } + }, + onDragEnd = { + draggingItemIndex = null + draggingItemOffset = 0f + onProgress?.invoke(snapshotState()) + } + ) + } + ) { + PlayerCard( player = player, - gameState = state, opponents = state.players.map { it.id }.filter { it != player.id }, life = lifeTotals[player.id] ?: state.startingLife, onLifeChange = { new -> @@ -218,6 +301,7 @@ fun GameScreen( } } ) + } } } @@ -272,7 +356,6 @@ private fun seatAccentColor(index: Int, scheme: androidx.compose.material3.Color @Composable private fun PlayerCard( player: PlayerState, - gameState: GameState, opponents: List, life: Int, onLifeChange: (Int) -> Unit, @@ -427,4 +510,67 @@ private fun CounterIconButton(icon: ImageVector, contentDescription: String, ena } } +suspend fun PointerInputScope.detectDragGesturesAfterShortPress( + onDragStart: (Offset) -> Unit = { }, + onDragEnd: () -> Unit = { }, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit +) { + while (true) { + val down = awaitPointerEventScope { + awaitFirstDown(requireUnconsumed = false) + } + + var dragStarted = false + try { + withTimeout(200) { + awaitPointerEventScope { + val downId = down.id + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == downId } + + if (change == null || !change.pressed) { + throw CancellationException("Up") + } + + val positionChange = change.position - down.position + if (positionChange.getDistance() > viewConfiguration.touchSlop) { + throw CancellationException("Moved") + } + } + } + } + } catch (_: TimeoutCancellationException) { + dragStarted = true + } catch (_: CancellationException) { + } + + if (dragStarted) { + onDragStart(down.position) + + try { + awaitPointerEventScope { + val downId = down.id + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == downId } + + if (change == null || !change.pressed) { + break + } + + val delta = change.positionChange() + if (delta != Offset.Zero) { + change.consume() + onDrag(change, delta) + } + } + } + } finally { + onDragEnd() + } + } + } +} + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index b903f5d..036d09b 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index b903f5d..036d09b 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..a57e6c3 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..06aae11 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..bb0f116 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..ec183c1 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..51f1078 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..1790207 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..0c436ba 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..9e76376 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..7a0232b 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..7150371 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..c81029e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..3df6500 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..f351178 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..51d07b5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..7b55c10 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..beab31f --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #000000 + \ No newline at end of file diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 3d74cbe..96f1d6d 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.12.1" +agp = "8.12.3" kotlin = "2.0.21" coreKtx = "1.10.1" junit = "4.13.2" diff --git a/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index e0b4c04..752d4ad 100644 Binary files a/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/MagicCounter.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ