1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2795 Date as an object | Move to bin by right-to-left swipe (#1883)

This commit is contained in:
Konstantin Ivanov 2024-12-07 15:57:35 +01:00 committed by GitHub
parent cc970f3a3f
commit c0439ba497
Signed by: github
GPG key ID: B5690EEEBB952194
11 changed files with 262 additions and 14 deletions

View file

@ -18,6 +18,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
@ -135,6 +136,14 @@ object DateObjectModule {
dispatchers: AppCoroutineDispatchers
): SetObjectDetails = SetObjectDetails(repository, dispatchers)
@JvmStatic
@PerScreen
@Provides
fun getSetObjectListIsArchived(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): SetObjectListIsArchived = SetObjectListIsArchived(repo, dispatchers)
@Module
interface Declarations {
@PerScreen

View file

@ -154,6 +154,9 @@ class DateObjectFragment : BaseComposeFragment(), ObjectTypeSelectionListener {
is DateObjectCommand.SendToast.UnexpectedLayout -> {
toast("Unexpected layout")
}
is DateObjectCommand.SendToast.Error -> {
toast(effect.message)
}
DateObjectCommand.TypeSelectionScreen -> {
val dialog = ObjectTypeSelectionFragment.new(space = space)
dialog.show(childFragmentManager, null)
@ -198,6 +201,7 @@ class DateObjectFragment : BaseComposeFragment(), ObjectTypeSelectionListener {
canPaginate = vm.canPaginate.collectAsStateWithLifecycle().value,
uiCalendarState = vm.uiCalendarState.collectAsStateWithLifecycle().value,
uiSyncStatusState = vm.uiSyncStatusWidgetState.collectAsStateWithLifecycle().value,
uiSnackbarState = vm.uiSnackbarState.collectAsStateWithLifecycle().value,
onDateEvent = vm::onDateEvent
)
}

View file

@ -57,7 +57,8 @@ fun ObjectWrapper.Basic.toUiObjectsListItem(
space: SpaceId,
urlBuilder: UrlBuilder,
objectTypes: List<ObjectWrapper.Type>,
fieldParser: FieldParser
fieldParser: FieldParser,
isOwnerOrEditor: Boolean
): UiObjectsListItem {
val obj = this
val typeUrl = obj.getProperType()
@ -76,6 +77,7 @@ fun ObjectWrapper.Basic.toUiObjectsListItem(
}
}?.name,
layout = layout,
icon = obj.objectIcon(builder = urlBuilder)
icon = obj.objectIcon(builder = urlBuilder),
isPossibleToDelete = isOwnerOrEditor
)
}

View file

@ -14,15 +14,22 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.foundation.components.BottomNavigationMenu
import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
@ -36,8 +43,11 @@ import com.anytypeio.anytype.feature_date.viewmodel.UiFieldsState
import com.anytypeio.anytype.feature_date.viewmodel.UiHeaderState
import com.anytypeio.anytype.feature_date.viewmodel.UiNavigationWidget
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState
import com.anytypeio.anytype.feature_date.viewmodel.UiSnackbarState
import com.anytypeio.anytype.feature_date.viewmodel.UiSyncStatusBadgeState
import com.anytypeio.anytype.feature_date.viewmodel.UiSyncStatusWidgetState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -52,12 +62,31 @@ fun DateMainScreen(
uiSyncStatusState: UiSyncStatusWidgetState,
uiCalendarState: UiCalendarState,
uiContentState: UiContentState,
uiSnackbarState: UiSnackbarState,
canPaginate: Boolean,
onDateEvent: (DateEvent) -> Unit
) {
val scope = rememberCoroutineScope()
val snackBarHostState = remember { SnackbarHostState() }
val snackBarText = stringResource(R.string.all_content_snackbar_title)
val undoText = stringResource(R.string.undo)
LaunchedEffect(key1 = uiSnackbarState) {
if (uiSnackbarState is UiSnackbarState.Visible) {
showMoveToBinSnackbar(
message = "'${uiSnackbarState.message}' $snackBarText",
undo = undoText,
scope = this,
snackBarHostState = snackBarHostState,
objectId = uiSnackbarState.objId,
onDateEvent = onDateEvent
)
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = colorResource(id = R.color.background_primary),
@ -152,6 +181,9 @@ fun DateMainScreen(
isOwnerOrEditor = uiNavigationWidget is UiNavigationWidget.Editor
)
}
},
snackbarHost = {
SnackbarHost(hostState = snackBarHostState)
}
)
if (uiSyncStatusState is UiSyncStatusWidgetState.Visible) {
@ -179,4 +211,32 @@ fun DateMainScreen(
onDateEvent = onDateEvent
)
}
}
private fun showMoveToBinSnackbar(
objectId: Id,
message: String,
undo: String,
scope: CoroutineScope,
snackBarHostState: SnackbarHostState,
onDateEvent: (DateEvent) -> Unit
) {
scope.launch {
val result = snackBarHostState
.showSnackbar(
message = message,
actionLabel = undo,
duration = SnackbarDuration.Short,
withDismissAction = true
)
when (result) {
SnackbarResult.ActionPerformed -> {
onDateEvent(DateEvent.Snackbar.UndoMoveToBin(objectId))
}
SnackbarResult.Dismissed -> {
onDateEvent(DateEvent.Snackbar.OnSnackbarDismiss)
}
}
}
}

View file

@ -1,6 +1,10 @@
package com.anytypeio.anytype.feature_date.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
@ -16,6 +20,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@ -24,6 +31,7 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
@ -34,6 +42,8 @@ import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.common.ShimmerEffect
import com.anytypeio.anytype.core_ui.extensions.swapList
import com.anytypeio.anytype.core_ui.foundation.DismissBackground
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
@ -47,6 +57,7 @@ import com.anytypeio.anytype.feature_date.ui.models.StubVerticalItems
import com.anytypeio.anytype.feature_date.viewmodel.UiContentState
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListItem
import com.anytypeio.anytype.feature_date.viewmodel.UiObjectsListState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
@ -100,15 +111,18 @@ fun ObjectsScreen(
val item = items[index]
when (item) {
is UiObjectsListItem.Item -> {
ListItem(
SwipeToDismissListItems(
modifier = Modifier
.fillMaxWidth()
.animateItem()
.noRippleThrottledClickable {
onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item))
},
item = item
item = item,
onDateEvent = onDateEvent
)
Divider(paddingStart = 16.dp, paddingEnd = 16.dp)
}
is UiObjectsListItem.Loading -> {
ListItemLoading(modifier = Modifier)
}
@ -229,6 +243,71 @@ private fun ListItemLoading(
)
}
@Composable
fun SwipeToDismissListItems(
item: UiObjectsListItem.Item,
modifier: Modifier,
animationDuration: Int = 500,
onDateEvent: (DateEvent) -> Unit,
) {
var isRemoved by remember { mutableStateOf(false) }
val dismissState = rememberSwipeToDismissBoxState(
initialValue = SwipeToDismissBoxValue.Settled,
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
isRemoved = true
true
} else {
false
}
return@rememberSwipeToDismissBoxState true
},
positionalThreshold = { it * .5f }
)
if (dismissState.currentValue != SwipeToDismissBoxValue.Settled) {
LaunchedEffect(Unit) {
dismissState.snapTo(SwipeToDismissBoxValue.Settled)
}
}
LaunchedEffect(key1 = isRemoved) {
if (isRemoved) {
delay(animationDuration.toLong())
onDateEvent(DateEvent.ObjectsList.OnObjectMoveToBin(item))
}
}
AnimatedVisibility(
visible = !isRemoved,
exit = shrinkVertically(
animationSpec = tween(durationMillis = animationDuration),
shrinkTowards = Alignment.Top
) + fadeOut()
) {
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
enableDismissFromEndToStart = item.isPossibleToDelete,
enableDismissFromStartToEnd = false,
backgroundContent = {
DismissBackground(
actionText = stringResource(R.string.move_to_bin),
dismissState = dismissState
)
},
content = {
ListItem(
modifier = Modifier
.noRippleThrottledClickable {
onDateEvent(DateEvent.ObjectsList.OnObjectClicked(item))
},
item = item
)
}
)
}
}
@Composable
private fun BoxScope.LoadingState() {
val loadingAlpha by animateFloatAsState(targetValue = 1f, label = "")

View file

@ -42,6 +42,7 @@ sealed class DateEvent {
sealed class ObjectsList : DateEvent() {
data class OnObjectClicked(val item: UiObjectsListItem) : ObjectsList()
data class OnObjectMoveToBin(val item: UiObjectsListItem.Item) : ObjectsList()
data object OnLoadMore : ObjectsList()
}
@ -53,4 +54,9 @@ sealed class DateEvent {
sealed class SyncStatusWidget : DateEvent() {
data object OnSyncStatusDismiss : SyncStatusWidget()
}
sealed class Snackbar : DateEvent() {
data object OnSnackbarDismiss : Snackbar()
data class UndoMoveToBin(val objectId: String) : Snackbar()
}
}

View file

@ -137,7 +137,8 @@ sealed class UiObjectsListItem {
val typeName: String? = null,
val createdBy: String? = null,
val layout: ObjectType.Layout? = null,
val icon: ObjectIcon = ObjectIcon.None
val icon: ObjectIcon = ObjectIcon.None,
val isPossibleToDelete: Boolean = false
) : UiObjectsListItem()
}
@ -179,3 +180,8 @@ sealed class UiErrorState {
data class Other(val msg: String) : Reason()
}
}
sealed class UiSnackbarState {
data object Hidden : UiSnackbarState()
data class Visible(val message: String, val objId: Id) : UiSnackbarState()
}

View file

@ -11,6 +11,7 @@ sealed class DateObjectCommand {
data object TypeSelectionScreen : DateObjectCommand()
data object ExitToSpaceWidgets : DateObjectCommand()
sealed class SendToast : DateObjectCommand() {
data class Error(val message: String) : SendToast()
data class UnexpectedLayout(val layout: String) : SendToast()
}
data object OpenGlobalSearch : DateObjectCommand()

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
@ -31,7 +32,8 @@ class DateObjectVMFactory @Inject constructor(
private val dateProvider: DateProvider,
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser
private val fieldParser: FieldParser,
private val setObjectListIsArchived: SetObjectListIsArchived
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -50,6 +52,8 @@ class DateObjectVMFactory @Inject constructor(
dateProvider = dateProvider,
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
createObject = createObject,
fieldParser = fieldParser
fieldParser = fieldParser,
setObjectListIsArchived = setObjectListIsArchived
) as T
}

View file

@ -21,6 +21,7 @@ import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
@ -41,6 +42,7 @@ import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel.Companion
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState
import kotlin.collections.map
import kotlin.text.take
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
@ -82,7 +84,8 @@ class DateObjectViewModel(
private val dateProvider: DateProvider,
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser
private val fieldParser: FieldParser,
private val setObjectListIsArchived: SetObjectListIsArchived
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
val uiCalendarIconState = MutableStateFlow<UiCalendarIconState>(UiCalendarIconState.Hidden)
@ -97,6 +100,7 @@ class DateObjectViewModel(
val uiCalendarState = MutableStateFlow<UiCalendarState>(UiCalendarState.Hidden)
val uiSyncStatusWidgetState =
MutableStateFlow<UiSyncStatusWidgetState>(UiSyncStatusWidgetState.Hidden)
val uiSnackbarState = MutableStateFlow<UiSnackbarState>(UiSnackbarState.Hidden)
val effects = MutableSharedFlow<DateObjectCommand>()
val errorState = MutableStateFlow<UiErrorState>(UiErrorState.Hidden)
@ -397,7 +401,8 @@ class DateObjectViewModel(
space = vmParams.spaceId,
urlBuilder = urlBuilder,
objectTypes = storeOfObjectTypes.getAll(),
fieldParser = fieldParser
fieldParser = fieldParser,
isOwnerOrEditor = permission.value?.isOwnerOrEditor() == true
)
}
uiContentState.value = if (items.isEmpty()) {
@ -640,6 +645,18 @@ class DateObjectViewModel(
is DateEvent.NavigationWidget -> onNavigationWidgetEvent(event)
is DateEvent.ObjectsList -> onObjectsListEvent(event)
is DateEvent.SyncStatusWidget -> onSyncStatusWidgetEvent(event)
is DateEvent.Snackbar -> onSnackbarEvent(event)
}
}
private fun onSnackbarEvent(event: DateEvent.Snackbar) {
when (event) {
DateEvent.Snackbar.OnSnackbarDismiss -> {
proceedWithDismissSnackbar()
}
is DateEvent.Snackbar.UndoMoveToBin -> {
proceedWithUndoMoveToBin(event.objectId)
}
}
}
@ -666,6 +683,7 @@ class DateObjectViewModel(
when (event) {
DateEvent.ObjectsList.OnLoadMore -> updateLimit()
is DateEvent.ObjectsList.OnObjectClicked -> onItemClicked(event.item)
is DateEvent.ObjectsList.OnObjectMoveToBin -> proceedWithMoveToBin(event.item)
}
}
@ -797,6 +815,48 @@ class DateObjectViewModel(
}
}
}
fun proceedWithMoveToBin(item: UiObjectsListItem.Item) {
val params = SetObjectListIsArchived.Params(
targets = listOf(item.id),
isArchived = true
)
viewModelScope.launch {
setObjectListIsArchived.async(params).fold(
onSuccess = { ids ->
Timber.d("Successfully archived object: $ids")
val name = item.name
uiSnackbarState.value = UiSnackbarState.Visible(
message = name.take(10),
objId = item.id
)
},
onFailure = { e ->
Timber.e(e, "Error while archiving object")
effects.emit(DateObjectCommand.SendToast.Error("Error while archiving object"))
}
)
}
}
fun proceedWithUndoMoveToBin(objectId: Id) {
val params = SetObjectListIsArchived.Params(
targets = listOf(objectId),
isArchived = false
)
viewModelScope.launch {
setObjectListIsArchived.async(params).fold(
onSuccess = { ids ->
Timber.d("Successfully archived object: $ids")
uiSnackbarState.value = UiSnackbarState.Hidden
},
onFailure = { e ->
Timber.e(e, "Error while un-archiving object")
effects.emit(DateObjectCommand.SendToast.Error("Error while un-archiving object"))
}
)
}
}
//endregion
//region Ui State
@ -847,6 +907,10 @@ class DateObjectViewModel(
)
}
}
fun proceedWithDismissSnackbar() {
uiSnackbarState.value = UiSnackbarState.Hidden
}
//endregion
companion object {

View file

@ -656,9 +656,22 @@ class CollectionViewModel(
}
private fun changeObjectListBinStatus(ids: List<Id>, isArchived: Boolean) {
launch {
setObjectListIsArchived.stream(SetObjectListIsArchived.Params(ids, isArchived))
.collect { it.progressiveFold() }
viewModelScope.launch {
val params = SetObjectListIsArchived.Params(ids, isArchived)
setObjectListIsArchived.async(params)
.fold(
onSuccess = {
if (isArchived) {
toasts.emit("Objects moved to bin")
} else {
toasts.emit("Objects restored")
}
},
onFailure = {
toasts.emit("Error while moving files to bin")
Timber.e(it, "Error while moving files to bin")
}
)
}
}