enhancement: reorderable instance selector (#1045)

This commit is contained in:
Diego Beraldin 2024-06-27 18:00:17 +02:00 committed by GitHub
parent 136cefaf2a
commit 38eadde947
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 71 additions and 34 deletions

View File

@ -76,28 +76,30 @@ class SelectInstanceBottomSheet : Screen {
var instanceToDelete by remember { mutableStateOf<String?>(null) } var instanceToDelete by remember { mutableStateOf<String?>(null) }
LaunchedEffect(model) { LaunchedEffect(model) {
model.effects.onEach { evt -> model.effects
when (evt) { .onEach { evt ->
SelectInstanceMviModel.Effect.CloseDialog -> { when (evt) {
changeInstanceDialogOpen = false SelectInstanceMviModel.Effect.CloseDialog -> {
} changeInstanceDialogOpen = false
}
is SelectInstanceMviModel.Effect.Confirm -> { is SelectInstanceMviModel.Effect.Confirm -> {
notificationCenter.send(NotificationCenterEvent.InstanceSelected(evt.instance)) notificationCenter.send(NotificationCenterEvent.InstanceSelected(evt.instance))
navigationCoordinator.hideBottomSheet() navigationCoordinator.hideBottomSheet()
}
} }
} }.launchIn(this)
}.launchIn(this)
} }
Column( Column(
modifier = modifier =
Modifier.padding( Modifier
top = Spacing.s, .padding(
start = Spacing.s, top = Spacing.s,
end = Spacing.s, start = Spacing.s,
bottom = Spacing.m, end = Spacing.s,
).fillMaxHeight(0.6f), bottom = Spacing.m,
).fillMaxHeight(0.6f),
verticalArrangement = Arrangement.spacedBy(Spacing.s), verticalArrangement = Arrangement.spacedBy(Spacing.s),
) { ) {
Box( Box(
@ -158,15 +160,19 @@ class SelectInstanceBottomSheet : Screen {
shadowElevation = elevation, shadowElevation = elevation,
) { ) {
SelectInstanceItem( SelectInstanceItem(
modifier = Modifier.draggableHandle(),
instance = instance, instance = instance,
isActive = isActive, isActive = isActive,
onDragStarted =
rememberCallback(model) {
model.reduce(SelectInstanceMviModel.Intent.HapticIndication)
},
onClick = onClick =
rememberCallback(model) { rememberCallback(model) {
model.reduce( model.reduce(
SelectInstanceMviModel.Intent.SelectInstance(instance), SelectInstanceMviModel.Intent.SelectInstance(instance),
) )
}, },
reorderableScope = this,
options = options =
buildList { buildList {
if (!isActive) { if (!isActive) {

View File

@ -8,13 +8,24 @@ interface SelectInstanceMviModel :
MviModel<SelectInstanceMviModel.Intent, SelectInstanceMviModel.State, SelectInstanceMviModel.Effect>, MviModel<SelectInstanceMviModel.Intent, SelectInstanceMviModel.State, SelectInstanceMviModel.Effect>,
ScreenModel { ScreenModel {
sealed interface Intent { sealed interface Intent {
data class SelectInstance(val value: String) : Intent data class SelectInstance(
val value: String,
) : Intent
data class DeleteInstance(val value: String) : Intent data class DeleteInstance(
val value: String,
) : Intent
data class ChangeInstanceName(val value: String) : Intent data class ChangeInstanceName(
val value: String,
) : Intent
data class SwapIntances(val from: Int, val to: Int) : Intent data object HapticIndication : Intent
data class SwapIntances(
val from: Int,
val to: Int,
) : Intent
data object SubmitChangeInstanceDialog : Intent data object SubmitChangeInstanceDialog : Intent
} }
@ -30,6 +41,8 @@ interface SelectInstanceMviModel :
sealed interface Effect { sealed interface Effect {
data object CloseDialog : Effect data object CloseDialog : Effect
data class Confirm(val instance: String) : Effect data class Confirm(
val instance: String,
) : Effect
} }
} }

View File

@ -4,6 +4,7 @@ import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.InstanceSelectionRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.InstanceSelectionRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.ValidationError import com.github.diegoberaldin.raccoonforlemmy.core.utils.ValidationError
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@ -18,22 +19,27 @@ class SelectInstanceViewModel(
private val instanceRepository: InstanceSelectionRepository, private val instanceRepository: InstanceSelectionRepository,
private val communityRepository: CommunityRepository, private val communityRepository: CommunityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository, private val apiConfigurationRepository: ApiConfigurationRepository,
) : SelectInstanceMviModel, private val hapticFeedback: HapticFeedback,
DefaultMviModel<SelectInstanceMviModel.Intent, SelectInstanceMviModel.State, SelectInstanceMviModel.Effect>( ) : DefaultMviModel<SelectInstanceMviModel.Intent, SelectInstanceMviModel.State, SelectInstanceMviModel.Effect>(
initialState = SelectInstanceMviModel.State(), initialState = SelectInstanceMviModel.State(),
) { ),
SelectInstanceMviModel {
private val saveOperationChannel = Channel<List<String>>() private val saveOperationChannel = Channel<List<String>>()
init { init {
screenModelScope.launch { screenModelScope.launch {
apiConfigurationRepository.instance.onEach { instance -> apiConfigurationRepository.instance
updateState { it.copy(currentInstance = instance) } .onEach { instance ->
}.launchIn(this) updateState { it.copy(currentInstance = instance) }
}.launchIn(this)
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
saveOperationChannel.receiveAsFlow().debounce(500).onEach { newInstances -> saveOperationChannel
instanceRepository.updateAll(newInstances) .receiveAsFlow()
}.launchIn(this) .debounce(500)
.onEach { newInstances ->
instanceRepository.updateAll(newInstances)
}.launchIn(this)
} }
if (uiState.value.instances.isEmpty()) { if (uiState.value.instances.isEmpty()) {
@ -59,6 +65,7 @@ class SelectInstanceViewModel(
is SelectInstanceMviModel.Intent.SubmitChangeInstanceDialog -> submitChangeInstance() is SelectInstanceMviModel.Intent.SubmitChangeInstanceDialog -> submitChangeInstance()
is SelectInstanceMviModel.Intent.DeleteInstance -> deleteInstance(intent.value) is SelectInstanceMviModel.Intent.DeleteInstance -> deleteInstance(intent.value)
is SelectInstanceMviModel.Intent.SwapIntances -> swapInstances(intent.from, intent.to) is SelectInstanceMviModel.Intent.SwapIntances -> swapInstances(intent.from, intent.to)
SelectInstanceMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
} }
} }

View File

@ -33,13 +33,16 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import sh.calvin.reorderable.ReorderableCollectionItemScope
@Composable @Composable
internal fun SelectInstanceItem( internal fun SelectInstanceItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
reorderableScope: ReorderableCollectionItemScope,
instance: String, instance: String,
isActive: Boolean = false, isActive: Boolean = false,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
onDragStarted: (() -> Unit)? = null,
options: List<Option> = emptyList(), options: List<Option> = emptyList(),
onOptionSelected: ((OptionId) -> Unit)? = null, onOptionSelected: ((OptionId) -> Unit)? = null,
) { ) {
@ -84,10 +87,17 @@ internal fun SelectInstanceItem(
Modifier Modifier
.size(IconSize.m) .size(IconSize.m)
.padding(Spacing.xs) .padding(Spacing.xs)
.onGloballyPositioned { .then(
with(reorderableScope) {
Modifier.draggableHandle(
onDragStarted = {
onDragStarted?.invoke()
},
)
},
).onGloballyPositioned {
optionsOffset = it.positionInParent() optionsOffset = it.positionInParent()
} }.onClick(
.onClick(
onClick = { onClick = {
optionsMenuOpen = true optionsMenuOpen = true
}, },

View File

@ -11,6 +11,7 @@ val selectInstanceModule =
instanceRepository = get(), instanceRepository = get(),
communityRepository = get(), communityRepository = get(),
apiConfigurationRepository = get(), apiConfigurationRepository = get(),
hapticFeedback = get(),
) )
} }
} }