mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -04:00
feat(profiles): Add drag-to-reorder for profile list
Wire up the missing drag-to-reorder UI that the priority hint already advertises. Long-press a profile row to reorder. Extracts a reusable ReorderableState component into common/compose. Backend (repo + ViewModel) was already in place; this adds the gesture handling, auto-scroll, visual feedback, and ID-based reorder validation.
This commit is contained in:
@@ -0,0 +1,191 @@
|
|||||||
|
package eu.darken.capod.common.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
|
class ReorderableState<T>(
|
||||||
|
val lazyListState: LazyListState,
|
||||||
|
private val hapticFeedback: HapticFeedback,
|
||||||
|
private val key: (T) -> Any,
|
||||||
|
) {
|
||||||
|
private val _items = mutableStateListOf<T>()
|
||||||
|
val items: List<T> get() = _items
|
||||||
|
|
||||||
|
private var originalItems: List<T> = emptyList()
|
||||||
|
|
||||||
|
var draggedIndex: Int? by mutableStateOf(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var dragOffsetY: Float by mutableFloatStateOf(0f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
val isDragging: Boolean get() = draggedIndex != null
|
||||||
|
|
||||||
|
fun syncItems(newItems: List<T>) {
|
||||||
|
if (isDragging) return
|
||||||
|
_items.clear()
|
||||||
|
_items.addAll(newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDragStart(item: T) {
|
||||||
|
val itemKey = key(item)
|
||||||
|
val index = _items.indexOfFirst { key(it) == itemKey }
|
||||||
|
if (index == -1) return
|
||||||
|
originalItems = _items.toList()
|
||||||
|
draggedIndex = index
|
||||||
|
dragOffsetY = 0f
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDrag(delta: Float) {
|
||||||
|
val currentIndex = draggedIndex ?: return
|
||||||
|
dragOffsetY += delta
|
||||||
|
|
||||||
|
val layoutInfo = lazyListState.layoutInfo
|
||||||
|
val draggedItemInfo = layoutInfo.visibleItemsInfo
|
||||||
|
.firstOrNull { it.index == currentIndex } ?: return
|
||||||
|
val draggedCenter = draggedItemInfo.offset + draggedItemInfo.size / 2 + dragOffsetY.toInt()
|
||||||
|
|
||||||
|
val targetInfo = layoutInfo.visibleItemsInfo.firstOrNull { itemInfo ->
|
||||||
|
itemInfo.index != currentIndex &&
|
||||||
|
itemInfo.index < _items.size &&
|
||||||
|
draggedCenter in itemInfo.offset..(itemInfo.offset + itemInfo.size)
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
val targetIndex = targetInfo.index
|
||||||
|
dragOffsetY += (draggedItemInfo.offset - targetInfo.offset).toFloat()
|
||||||
|
_items.swap(currentIndex, targetIndex)
|
||||||
|
draggedIndex = targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDragEnd(onReorder: (List<T>) -> Unit) {
|
||||||
|
val reordered = _items.toList()
|
||||||
|
draggedIndex = null
|
||||||
|
dragOffsetY = 0f
|
||||||
|
originalItems = emptyList()
|
||||||
|
onReorder(reordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDragCancel() {
|
||||||
|
if (originalItems.isNotEmpty()) {
|
||||||
|
_items.clear()
|
||||||
|
_items.addAll(originalItems)
|
||||||
|
}
|
||||||
|
draggedIndex = null
|
||||||
|
dragOffsetY = 0f
|
||||||
|
originalItems = emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> MutableList<T>.swap(from: Int, to: Int) {
|
||||||
|
val temp = this[from]
|
||||||
|
this[from] = this[to]
|
||||||
|
this[to] = temp
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> rememberReorderableState(
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
items: List<T>,
|
||||||
|
key: (T) -> Any,
|
||||||
|
): ReorderableState<T> {
|
||||||
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
return remember(lazyListState) {
|
||||||
|
ReorderableState(lazyListState, hapticFeedback, key).also {
|
||||||
|
it.syncItems(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> ReorderableAutoScroll(state: ReorderableState<T>) {
|
||||||
|
val scrollThresholdPx = with(LocalDensity.current) { 80.dp.toPx() }
|
||||||
|
LaunchedEffect(state.isDragging) {
|
||||||
|
if (!state.isDragging) return@LaunchedEffect
|
||||||
|
while (isActive) {
|
||||||
|
val layoutInfo = state.lazyListState.layoutInfo
|
||||||
|
val viewportStart = layoutInfo.viewportStartOffset
|
||||||
|
val viewportEnd = layoutInfo.viewportEndOffset
|
||||||
|
val draggedIdx = state.draggedIndex ?: break
|
||||||
|
val draggedItem = layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggedIdx }
|
||||||
|
if (draggedItem != null) {
|
||||||
|
val itemCenter = draggedItem.offset + draggedItem.size / 2 + state.dragOffsetY
|
||||||
|
val scrollSpeed = 8f
|
||||||
|
val delta = when {
|
||||||
|
itemCenter < viewportStart + scrollThresholdPx -> -scrollSpeed
|
||||||
|
itemCenter > viewportEnd - scrollThresholdPx -> scrollSpeed
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
if (delta != 0f) state.lazyListState.scrollBy(delta)
|
||||||
|
}
|
||||||
|
delay(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> reorderableItemModifier(
|
||||||
|
state: ReorderableState<T>,
|
||||||
|
index: Int,
|
||||||
|
item: T,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onReorder: (List<T>) -> Unit = {},
|
||||||
|
): Modifier {
|
||||||
|
val isDragging = state.draggedIndex == index
|
||||||
|
val dragOffsetY = if (isDragging) state.dragOffsetY else 0f
|
||||||
|
|
||||||
|
val currentItem by rememberUpdatedState(item)
|
||||||
|
val currentOnReorder by rememberUpdatedState(onReorder)
|
||||||
|
|
||||||
|
val surfaceColor = MaterialTheme.colorScheme.surface
|
||||||
|
|
||||||
|
return Modifier
|
||||||
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
|
.background(surfaceColor)
|
||||||
|
.then(
|
||||||
|
if (enabled) {
|
||||||
|
Modifier.pointerInput(Unit) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = { state.onDragStart(currentItem) },
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
state.onDrag(dragAmount.y)
|
||||||
|
},
|
||||||
|
onDragEnd = { state.onDragEnd(currentOnReorder) },
|
||||||
|
onDragCancel = { state.onDragCancel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.graphicsLayer {
|
||||||
|
if (isDragging) {
|
||||||
|
translationY = dragOffsetY
|
||||||
|
shadowElevation = 8.dp.toPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,12 +88,18 @@ class DeviceProfilesRepo @Inject constructor(
|
|||||||
deviceStateCache.delete(profileId)
|
deviceStateCache.delete(profileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reorderProfiles(profiles: List<DeviceProfile>) = mutex.withLock {
|
suspend fun reorderProfilesById(orderedIds: List<ProfileId>) = mutex.withLock {
|
||||||
settings.profiles.valueBlocking = DeviceProfilesContainer(profiles.toList())
|
val current = settings.profiles.valueBlocking.profiles
|
||||||
log(VERBOSE) { "Reordered ${profiles.size} device profiles" }
|
require(orderedIds.size == current.size && orderedIds.toSet() == current.map { it.id }.toSet()) {
|
||||||
|
"Reorder IDs must match current profile IDs exactly"
|
||||||
|
}
|
||||||
|
val byId = current.associateBy { it.id }
|
||||||
|
val reordered = orderedIds.map { byId.getValue(it) }
|
||||||
|
settings.profiles.valueBlocking = DeviceProfilesContainer(reordered)
|
||||||
|
log(VERBOSE) { "Reordered ${reordered.size} device profiles by ID" }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clear() {
|
suspend fun clear() = mutex.withLock {
|
||||||
settings.profiles.valueBlocking = DeviceProfilesContainer(emptyList())
|
settings.profiles.valueBlocking = DeviceProfilesContainer(emptyList())
|
||||||
deviceStateCache.deleteAll()
|
deviceStateCache.deleteAll()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||||
import androidx.compose.material.icons.twotone.Add
|
import androidx.compose.material.icons.twotone.Add
|
||||||
|
import androidx.compose.material.icons.twotone.DragHandle
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -27,6 +29,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -38,11 +41,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.compose.Preview2
|
import eu.darken.capod.common.compose.Preview2
|
||||||
import eu.darken.capod.common.compose.PreviewWrapper
|
import eu.darken.capod.common.compose.PreviewWrapper
|
||||||
|
import eu.darken.capod.common.compose.ReorderableAutoScroll
|
||||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||||
|
import eu.darken.capod.common.compose.reorderableItemModifier
|
||||||
|
import eu.darken.capod.common.compose.rememberReorderableState
|
||||||
import eu.darken.capod.common.error.ErrorEventHandler
|
import eu.darken.capod.common.error.ErrorEventHandler
|
||||||
import eu.darken.capod.common.navigation.NavigationEventHandler
|
import eu.darken.capod.common.navigation.NavigationEventHandler
|
||||||
import eu.darken.capod.pods.core.apple.PodModel
|
import eu.darken.capod.pods.core.apple.PodModel
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
|
import eu.darken.capod.profiles.core.ProfileId
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) {
|
fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) {
|
||||||
@@ -56,6 +63,7 @@ fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) {
|
|||||||
onBack = { vm.navUp() },
|
onBack = { vm.navUp() },
|
||||||
onAddDevice = { vm.onAddDevice() },
|
onAddDevice = { vm.onAddDevice() },
|
||||||
onEditProfile = { profile -> vm.onEditProfile(profile) },
|
onEditProfile = { profile -> vm.onEditProfile(profile) },
|
||||||
|
onReorder = { ids -> vm.onReorder(ids) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +74,17 @@ fun DeviceManagerScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onAddDevice: () -> Unit,
|
onAddDevice: () -> Unit,
|
||||||
onEditProfile: (DeviceProfile) -> Unit,
|
onEditProfile: (DeviceProfile) -> Unit,
|
||||||
|
onReorder: (List<ProfileId>) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
val reorderableState = rememberReorderableState(lazyListState, state.profiles) { it.id }
|
||||||
|
|
||||||
|
LaunchedEffect(state.profiles) {
|
||||||
|
reorderableState.syncItems(state.profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReorderableAutoScroll(reorderableState)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -98,18 +116,33 @@ fun DeviceManagerScreen(
|
|||||||
onAddDevice = onAddDevice,
|
onAddDevice = onAddDevice,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
val showDragHandles = reorderableState.items.size >= 2
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding),
|
.padding(innerPadding),
|
||||||
) {
|
) {
|
||||||
items(
|
itemsIndexed(
|
||||||
items = state.profiles,
|
items = reorderableState.items,
|
||||||
key = { it.id },
|
key = { _, profile -> profile.id },
|
||||||
) { profile ->
|
) { index, profile ->
|
||||||
|
val isDraggingThis = reorderableState.draggedIndex == index
|
||||||
ProfileRow(
|
ProfileRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.then(if (!isDraggingThis) Modifier.animateItem() else Modifier)
|
||||||
|
.then(
|
||||||
|
reorderableItemModifier(
|
||||||
|
state = reorderableState,
|
||||||
|
index = index,
|
||||||
|
item = profile,
|
||||||
|
enabled = showDragHandles,
|
||||||
|
onReorder = { reordered -> onReorder(reordered.map { it.id }) },
|
||||||
|
)
|
||||||
|
),
|
||||||
profile = profile,
|
profile = profile,
|
||||||
onClick = { onEditProfile(profile) },
|
onClick = { if (!reorderableState.isDragging) onEditProfile(profile) },
|
||||||
|
showDragHandle = showDragHandles,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +227,13 @@ private fun EmptyState(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProfileRow(
|
private fun ProfileRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
profile: DeviceProfile,
|
profile: DeviceProfile,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
showDragHandle: Boolean = false,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
@@ -221,6 +256,16 @@ private fun ProfileRow(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showDragHandle) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.TwoTone.DragHandle,
|
||||||
|
contentDescription = stringResource(R.string.profiles_drag_handle_description),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.padding(12.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.navigation.Nav
|
|||||||
import eu.darken.capod.common.uix.ViewModel4
|
import eu.darken.capod.common.uix.ViewModel4
|
||||||
import eu.darken.capod.profiles.core.DeviceProfile
|
import eu.darken.capod.profiles.core.DeviceProfile
|
||||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||||
|
import eu.darken.capod.profiles.core.ProfileId
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -36,9 +37,9 @@ class DeviceManagerViewModel @Inject constructor(
|
|||||||
navTo(Nav.Main.DeviceProfileCreation(profileId = profile.id))
|
navTo(Nav.Main.DeviceProfileCreation(profileId = profile.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onReorder(profiles: List<DeviceProfile>) = launch {
|
fun onReorder(profileIds: List<ProfileId>) = launch {
|
||||||
log(TAG) { "onReorder(): ${profiles.size} profiles" }
|
log(TAG) { "onReorder(): ${profileIds.size} profiles" }
|
||||||
deviceProfilesRepo.reorderProfiles(profiles)
|
deviceProfilesRepo.reorderProfilesById(profileIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package eu.darken.capod.profiles.core
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import testhelpers.BaseTest
|
||||||
|
import testhelpers.coroutine.runTest2
|
||||||
|
import testhelpers.datastore.FakeDataStoreValue
|
||||||
|
|
||||||
|
class DeviceProfilesRepoReorderTest : BaseTest() {
|
||||||
|
|
||||||
|
private fun createRepo(
|
||||||
|
initialProfiles: List<DeviceProfile>,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
): Pair<DeviceProfilesRepo, FakeDataStoreValue<DeviceProfilesContainer>> {
|
||||||
|
val fakeProfiles = FakeDataStoreValue(DeviceProfilesContainer(initialProfiles))
|
||||||
|
|
||||||
|
val settings = mockk<DeviceProfilesSettings> {
|
||||||
|
every { profiles } returns fakeProfiles.mock
|
||||||
|
every { defaultProfileCreated } returns FakeDataStoreValue(true).mock
|
||||||
|
}
|
||||||
|
|
||||||
|
val repo = DeviceProfilesRepo(
|
||||||
|
scope = scope,
|
||||||
|
context = mockk(relaxed = true),
|
||||||
|
generalSettings = mockk(relaxed = true),
|
||||||
|
settings = settings,
|
||||||
|
deviceStateCache = mockk(relaxed = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
return repo to fakeProfiles
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reorderProfilesById reorders profiles correctly`() = runTest2 {
|
||||||
|
val p1 = AppleDeviceProfile(id = "1", label = "First")
|
||||||
|
val p2 = AppleDeviceProfile(id = "2", label = "Second")
|
||||||
|
val p3 = AppleDeviceProfile(id = "3", label = "Third")
|
||||||
|
|
||||||
|
val (repo, fakeProfiles) = createRepo(listOf(p1, p2, p3), this)
|
||||||
|
|
||||||
|
repo.reorderProfilesById(listOf("3", "1", "2"))
|
||||||
|
|
||||||
|
fakeProfiles.value.profiles.map { it.id } shouldBe listOf("3", "1", "2")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reorderProfilesById preserves profile data`() = runTest2 {
|
||||||
|
val p1 = AppleDeviceProfile(id = "1", label = "First", minimumSignalQuality = 0.5f)
|
||||||
|
val p2 = AppleDeviceProfile(id = "2", label = "Second", minimumSignalQuality = 0.8f)
|
||||||
|
|
||||||
|
val (repo, fakeProfiles) = createRepo(listOf(p1, p2), this)
|
||||||
|
|
||||||
|
repo.reorderProfilesById(listOf("2", "1"))
|
||||||
|
|
||||||
|
fakeProfiles.value.profiles shouldBe listOf(p2, p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reorderProfilesById rejects mismatched IDs`() = runTest2(expectedError = IllegalArgumentException::class) {
|
||||||
|
val p1 = AppleDeviceProfile(id = "1", label = "First")
|
||||||
|
val p2 = AppleDeviceProfile(id = "2", label = "Second")
|
||||||
|
|
||||||
|
val (repo, _) = createRepo(listOf(p1, p2), this)
|
||||||
|
|
||||||
|
repo.reorderProfilesById(listOf("1", "3"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reorderProfilesById rejects wrong count`() = runTest2(expectedError = IllegalArgumentException::class) {
|
||||||
|
val p1 = AppleDeviceProfile(id = "1", label = "First")
|
||||||
|
val p2 = AppleDeviceProfile(id = "2", label = "Second")
|
||||||
|
|
||||||
|
val (repo, _) = createRepo(listOf(p1, p2), this)
|
||||||
|
|
||||||
|
repo.reorderProfilesById(listOf("1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user