diff --git a/CHANGELOG.md b/CHANGELOG.md index d00f69cbe2d..93a27ec0c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.4.12 + +* `Coroutines` + * `JVM` + * Update `launchSynchronously` signature +* `Selector` + * Project created + ## 0.4.11 * `Common` diff --git a/README.md b/README.md index 1c11fb08176..0686112a8cf 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ You always can look at the { + val selectedItems: List + val itemSelected: SharedFlow + val itemUnselected: SharedFlow + + suspend fun toggleSelection(element: T) + suspend fun forceSelect(element: T) + suspend fun forceDeselect(element: T) + suspend fun clearSelection() +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun Selector.contains(element: T) = selectedItems.contains(element) +@Suppress("NOTHING_TO_INLINE") +inline fun Selector.nothingSelected(): Boolean = selectedItems.isEmpty() +suspend inline fun Selector.toggleSelection(elements: List) = elements.forEach { toggleSelection(it) } +suspend inline fun Selector.forceSelect(elements: List) = elements.forEach { forceSelect(it) } +suspend inline fun Selector.forceDeselect(elements: List) = elements.forEach { forceDeselect(it) } +suspend inline fun Selector.toggleSelection(firstElement: T, vararg elements: T) = toggleSelection(listOf(firstElement) + elements.toList()) +suspend inline fun Selector.forceSelect(firstElement: T, vararg elements: T) = forceSelect(listOf(firstElement) + elements.toList()) +suspend inline fun Selector.forceDeselect(firstElement: T, vararg elements: T) = forceDeselect(listOf(firstElement) + elements.toList()) + +/** + * Realization of [Selector] with one or without selected element. This realization will always have empty + * [selectedItems] when nothing selected and one element in [selectedItems] when something selected. Contains + * [selectedItem] value for simple access to currently selected item. + * + * On calling of [toggleSelection] previous selection will be erased and [itemUnselected] will emit this element. + * + * @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection] + */ +class SingleSelector( + selectedItem: T? = null, + safeChanges: Boolean = true +) : Selector { + var selectedItem: T? = selectedItem + private set + override val selectedItems: List + get() = selectedItem ?.let { listOf(it) } ?: emptyList() + + private val _itemSelected = MutableSharedFlow() + override val itemSelected: SharedFlow = _itemSelected.asSharedFlow() + private val _itemUnselected = MutableSharedFlow() + override val itemUnselected: SharedFlow = _itemUnselected.asSharedFlow() + + private val mutex = if (safeChanges) { + Mutex() + } else { + null + } + + override suspend fun forceDeselect(element: T) { + mutex ?.lock() + if (selectedItem == element) { + selectedItem = null + _itemUnselected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun forceSelect(element: T) { + mutex ?.lock() + if (selectedItem != element) { + selectedItem = element + _itemSelected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun toggleSelection(element: T) { + mutex ?.lock() + if (selectedItem == element) { + selectedItem = null + _itemUnselected.emit(element) + } else { + val previouslySelected = selectedItem + selectedItem = null + if (previouslySelected != null) { + _itemUnselected.emit(previouslySelected) + } + selectedItem = element + _itemSelected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun clearSelection() { + selectedItem ?.let { forceDeselect(it) } + } +} + +/** + * Realization of [Selector] with multiple selected elements. On calling of [toggleSelection] this realization will select passed element OR deselect it if it is already in + * [selectedItems] + * + * @param safeChanges Set to false to disable using of [mutex] for synchronizing changes on [toggleSelection] + */ +class MultipleSelector( + selectedItems: List = emptyList(), + safeChanges: Boolean = true +) : Selector { + private val _selectedItems: MutableList = selectedItems.toMutableList() + override val selectedItems: List = _selectedItems + + private val _itemSelected = MutableSharedFlow() + override val itemSelected: SharedFlow = _itemSelected.asSharedFlow() + private val _itemUnselected = MutableSharedFlow() + override val itemUnselected: SharedFlow = _itemUnselected.asSharedFlow() + + private val mutex = if (safeChanges) { + Mutex() + } else { + null + } + + override suspend fun forceDeselect(element: T) { + mutex ?.lock() + if (_selectedItems.remove(element)) { + _itemUnselected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun forceSelect(element: T) { + mutex ?.lock() + if (element !in _selectedItems && _selectedItems.add(element)) { + _itemSelected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun toggleSelection(element: T) { + mutex ?.lock() + if (_selectedItems.remove(element)) { + _itemUnselected.emit(element) + } else { + _selectedItems.add(element) + _itemSelected.emit(element) + } + mutex ?.unlock() + } + + override suspend fun clearSelection() { + mutex ?.lock() + val preSelectedItems = _selectedItems.toList() + _selectedItems.clear() + preSelectedItems.forEach { _itemUnselected.emit(it) } + mutex ?.unlock() + } +} + +@Suppress("FunctionName", "NOTHING_TO_INLINE") +inline fun Selector( + multiple: Boolean, + safeChanges: Boolean = true +): Selector = if (multiple) { + MultipleSelector(safeChanges = safeChanges) +} else { + SingleSelector(safeChanges = safeChanges) +} diff --git a/selector/common/src/commonMain/kotlin/dev/inmo/micro_utils/selector/SelectorItemsFlow.kt b/selector/common/src/commonMain/kotlin/dev/inmo/micro_utils/selector/SelectorItemsFlow.kt new file mode 100644 index 00000000000..29fd3917cb1 --- /dev/null +++ b/selector/common/src/commonMain/kotlin/dev/inmo/micro_utils/selector/SelectorItemsFlow.kt @@ -0,0 +1,17 @@ +package dev.inmo.micro_utils.selector + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +/** + * @return Returned [SharedFlow] will emit true when [element] has been selected in [this] [Selector] and will emit + * false when this [element] was deselected + * + * @see [Selector] + * @see [Selector.itemSelected] + * @see [Selector.itemUnselected] + */ +fun Selector.itemSelectionFlow(element: T, scope: CoroutineScope): SharedFlow = MutableSharedFlow().apply { + itemSelected.onEach { if (it == element) emit(true) }.launchIn(scope) + itemUnselected.onEach { if (it == element) emit(false) }.launchIn(scope) +}.asSharedFlow() diff --git a/selector/common/src/main/AndroidManifest.xml b/selector/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..af2679c3813 --- /dev/null +++ b/selector/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 320b82da2da..c438c68de35 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ rootProject.name='micro_utils' String[] includes = [ ":common", + ":selector:common", ":pagination:common", ":pagination:exposed", ":pagination:ktor:common",