mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-11 10:18:05 +09:00
DROID-741 Widgets | Enhancement | Allow deleting widgets (#2902)
This commit is contained in:
parent
ee4c810bf6
commit
1309af7752
8 changed files with 141 additions and 19 deletions
|
@ -19,6 +19,7 @@ import com.anytypeio.anytype.domain.objects.ObjectStore
|
|||
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.anytypeio.anytype.domain.widgets.CreateWidget
|
||||
import com.anytypeio.anytype.domain.widgets.DeleteWidget
|
||||
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
|
||||
import com.anytypeio.anytype.presentation.home.HomeScreenViewModel
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
|
@ -71,11 +72,18 @@ object HomeScreenModule {
|
|||
@Provides
|
||||
@PerScreen
|
||||
fun createWidget(
|
||||
repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
repo: BlockRepository
|
||||
): CreateWidget = CreateWidget(
|
||||
repo = repo,
|
||||
dispatchers = dispatchers
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun deleteWidget(
|
||||
repo: BlockRepository,
|
||||
): DeleteWidget = DeleteWidget(
|
||||
repo = repo
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.anytypeio.anytype.ui.home
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
|
@ -22,10 +24,12 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.presentation.widgets.TreePath
|
||||
import com.anytypeio.anytype.presentation.widgets.WidgetView
|
||||
import timber.log.Timber
|
||||
|
@ -36,7 +40,8 @@ fun HomeScreen(
|
|||
onExpand: (TreePath) -> Unit,
|
||||
onCreateWidget: () -> Unit,
|
||||
onEditWidgets: () -> Unit,
|
||||
onRefresh: () -> Unit
|
||||
onRefresh: () -> Unit,
|
||||
onDeleteWidget: (Id) -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
|
@ -55,6 +60,9 @@ fun HomeScreen(
|
|||
.padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 6.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Circle {
|
||||
onDeleteWidget(item.id)
|
||||
}
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
TreeWidgetHeader(item)
|
||||
item.elements.forEach { element ->
|
||||
|
@ -73,7 +81,7 @@ fun HomeScreen(
|
|||
.width(20.dp)
|
||||
.height(20.dp)
|
||||
) {
|
||||
when(val icon = element.icon) {
|
||||
when (val icon = element.icon) {
|
||||
is WidgetView.Tree.Icon.Branch -> {
|
||||
Text(
|
||||
text = ">",
|
||||
|
@ -122,6 +130,9 @@ fun HomeScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
Circle {
|
||||
onDeleteWidget(item.id)
|
||||
}
|
||||
}
|
||||
is WidgetView.Action.CreateWidget -> {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
|
@ -168,7 +179,9 @@ private fun TreeWidgetHeader(item: WidgetView.Tree) {
|
|||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = ">",
|
||||
modifier = Modifier.align(Alignment.CenterVertically).rotate(90f)
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.rotate(90f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -190,4 +203,25 @@ fun WidgetActionButton(
|
|||
) {
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Circle(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(24.dp)
|
||||
.width(24.dp)) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(4.dp)
|
||||
.clickable { onClick() },
|
||||
onDraw = {
|
||||
drawCircle(color = Color.Red)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ class HomeScreenFragment : BaseComposeFragment() {
|
|||
onCreateWidget = {
|
||||
findNavController().navigate(R.id.selectWidgetSourceScreen)
|
||||
},
|
||||
onDeleteWidget = vm::onDeleteWidgetClicked,
|
||||
onEditWidgets = { context.toast("Coming soon") },
|
||||
onRefresh = vm::onRefresh
|
||||
)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package com.anytypeio.anytype.domain.block.interactor
|
||||
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.base.Either
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.base.Either
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
|
||||
/**
|
||||
* Use-case for unlinking blocks from its context.
|
||||
* Unlinking is a remplacement for delete operations.
|
||||
* Unlinking is a replacement for delete operations.
|
||||
*/
|
||||
open class UnlinkBlocks(private val repo: BlockRepository) :
|
||||
BaseUseCase<Payload, UnlinkBlocks.Params>() {
|
||||
|
|
|
@ -2,12 +2,10 @@ package com.anytypeio.anytype.domain.widgets
|
|||
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultatInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
|
||||
class CreateWidget(
|
||||
private val dispatchers: AppCoroutineDispatchers,
|
||||
private val repo: BlockRepository
|
||||
) : ResultatInteractor<CreateWidget.Params, Payload>() {
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.anytypeio.anytype.domain.widgets
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.domain.base.ResultatInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
|
||||
class DeleteWidget(
|
||||
private val repo: BlockRepository
|
||||
) : ResultatInteractor<DeleteWidget.Params, Payload>() {
|
||||
|
||||
override suspend fun execute(params: Params) = repo.unlink(
|
||||
command = Command.Unlink(
|
||||
context = params.ctx,
|
||||
targets = params.targets
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* Params for deleting widget blocks from widget object
|
||||
* @property [ctx] id of the widget object
|
||||
* @property [targets] ids of widget blocks to delete from widget object
|
||||
*/
|
||||
data class Params(
|
||||
val ctx: Id,
|
||||
val targets: List<Id>
|
||||
)
|
||||
}
|
|
@ -17,6 +17,7 @@ import com.anytypeio.anytype.domain.misc.Reducer
|
|||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.widgets.CreateWidget
|
||||
import com.anytypeio.anytype.domain.widgets.DeleteWidget
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import com.anytypeio.anytype.presentation.widgets.LinkWidgetContainer
|
||||
|
@ -44,6 +45,7 @@ class HomeScreenViewModel(
|
|||
private val configStorage: ConfigStorage,
|
||||
private val openObject: OpenObject,
|
||||
private val createWidget: CreateWidget,
|
||||
private val deleteWidget: DeleteWidget,
|
||||
private val objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val widgetEventDispatcher: Dispatcher<WidgetDispatchEvent>,
|
||||
|
@ -84,6 +86,7 @@ class HomeScreenViewModel(
|
|||
}
|
||||
}
|
||||
}.map { state ->
|
||||
Timber.d("Emitting new state: ${state::class.java.simpleName}")
|
||||
when (state) {
|
||||
is ObjectViewState.Failure -> {
|
||||
emptyList()
|
||||
|
@ -102,6 +105,7 @@ class HomeScreenViewModel(
|
|||
}
|
||||
}
|
||||
}.collect {
|
||||
Timber.d("Emitting list of widgets: ${it.size}")
|
||||
widgets.value = it
|
||||
}
|
||||
}
|
||||
|
@ -121,16 +125,22 @@ class HomeScreenViewModel(
|
|||
}
|
||||
}
|
||||
}.collect {
|
||||
Timber.d("Emitting list of containers: ${it.size}")
|
||||
containers.value = it
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
containers.flatMapLatest {
|
||||
combine(
|
||||
it.map { m -> m.view }
|
||||
) { array ->
|
||||
array.toList()
|
||||
containers.flatMapLatest { list ->
|
||||
Timber.d("Receiving list of containers: ${list.size}")
|
||||
if (list.isNotEmpty()) {
|
||||
combine(
|
||||
list.map { m -> m.view }
|
||||
) { array ->
|
||||
array.toList()
|
||||
}
|
||||
} else {
|
||||
flowOf(emptyList())
|
||||
}
|
||||
}.flowOn(appCoroutineDispatchers.io).collect {
|
||||
Timber.d("Views update: $it")
|
||||
|
@ -183,7 +193,7 @@ class HomeScreenViewModel(
|
|||
ctx = config.widgets,
|
||||
source = source
|
||||
)
|
||||
).collect { status ->
|
||||
).flowOn(appCoroutineDispatchers.io).collect { status ->
|
||||
Timber.d("Status while creating widget: $status")
|
||||
when (status) {
|
||||
is Resultat.Failure -> {
|
||||
|
@ -201,6 +211,32 @@ class HomeScreenViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun proceedWithDeletingWidget(widget: Id) {
|
||||
viewModelScope.launch {
|
||||
val config = configStorage.get()
|
||||
deleteWidget(
|
||||
DeleteWidget.Params(
|
||||
ctx = config.widgets,
|
||||
targets = listOf(widget)
|
||||
)
|
||||
).flowOn(appCoroutineDispatchers.io).collect { status ->
|
||||
Timber.d("Status while deleting widget: $status")
|
||||
when (status) {
|
||||
is Resultat.Failure -> {
|
||||
sendToast("Error while deleting widget: ${status.exception}")
|
||||
Timber.e(status.exception, "Error while deleting widget")
|
||||
}
|
||||
is Resultat.Loading -> {
|
||||
// Do nothing?
|
||||
}
|
||||
is Resultat.Success -> {
|
||||
objectPayloadDispatcher.send(status.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("For debugging only")
|
||||
fun onRefresh() {
|
||||
proceedWithOpeningWidgetObject(widgetObject = configStorage.get().widgets)
|
||||
|
@ -214,6 +250,10 @@ class HomeScreenViewModel(
|
|||
expanded.onExpand(linkPath = path)
|
||||
}
|
||||
|
||||
fun onDeleteWidgetClicked(widget: Id) {
|
||||
proceedWithDeletingWidget(widget)
|
||||
}
|
||||
|
||||
// TODO move to a separate reducer inject into this VM's constructor
|
||||
override fun reduce(state: ObjectView, event: Payload): ObjectView {
|
||||
var curr = state
|
||||
|
@ -222,6 +262,11 @@ class HomeScreenViewModel(
|
|||
is Event.Command.AddBlock -> {
|
||||
curr = curr.copy(blocks = curr.blocks + e.blocks)
|
||||
}
|
||||
is Event.Command.DeleteBlock -> {
|
||||
curr = curr.copy(
|
||||
blocks = curr.blocks.filter { !e.targets.contains(it.id) }
|
||||
)
|
||||
}
|
||||
is Event.Command.UpdateStructure -> {
|
||||
curr = curr.copy(
|
||||
blocks = curr.blocks.replace(
|
||||
|
@ -247,6 +292,7 @@ class HomeScreenViewModel(
|
|||
private val configStorage: ConfigStorage,
|
||||
private val openObject: OpenObject,
|
||||
private val createWidget: CreateWidget,
|
||||
private val deleteWidget: DeleteWidget,
|
||||
private val objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val widgetEventDispatcher: Dispatcher<WidgetDispatchEvent>,
|
||||
|
@ -258,6 +304,7 @@ class HomeScreenViewModel(
|
|||
configStorage = configStorage,
|
||||
openObject = openObject,
|
||||
createWidget = createWidget,
|
||||
deleteWidget = deleteWidget,
|
||||
objectSearchSubscriptionContainer = objectSearchSubscriptionContainer,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
widgetEventDispatcher = widgetEventDispatcher,
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
|||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.widgets.CreateWidget
|
||||
import com.anytypeio.anytype.domain.widgets.DeleteWidget
|
||||
import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer
|
||||
|
@ -54,6 +55,9 @@ class HomeViewModelTest {
|
|||
@Mock
|
||||
lateinit var createWidget: CreateWidget
|
||||
|
||||
@Mock
|
||||
lateinit var deleteWidget: DeleteWidget
|
||||
|
||||
@Mock
|
||||
lateinit var interceptEvents: InterceptEvents
|
||||
|
||||
|
@ -318,6 +322,7 @@ class HomeViewModelTest {
|
|||
configStorage = configStorage,
|
||||
interceptEvents = interceptEvents,
|
||||
createWidget = createWidget,
|
||||
deleteWidget = deleteWidget,
|
||||
objectPayloadDispatcher = objectPayloadDispatcher,
|
||||
widgetEventDispatcher = widgetEventDispatcher,
|
||||
openObject = openObject,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue