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

DROID-946 Widgets | Enhancement | GUI basics for link-styled widgets + Processing widget object payload updates (#2901)

This commit is contained in:
Evgenii Kozlov 2023-02-07 17:58:30 +01:00 committed by GitHub
parent 25a184085f
commit 878f84a052
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 22 deletions

View file

@ -56,11 +56,7 @@ fun HomeScreen(
shape = RoundedCornerShape(16.dp)
) {
Column(Modifier.padding(16.dp)) {
Text(
text = item.obj.name.orEmpty().trim(),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(vertical = 8.dp)
)
TreeWidgetHeader(item)
item.elements.forEach { element ->
Row(
modifier = Modifier
@ -108,6 +104,25 @@ fun HomeScreen(
}
}
}
is WidgetView.Link -> {
Card(
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.padding(start = 20.dp, end = 20.dp, top = 6.dp, bottom = 6.dp),
shape = RoundedCornerShape(16.dp)
) {
Box(Modifier.fillMaxHeight()) {
Text(
text = item.obj.name.orEmpty().trim(),
style = MaterialTheme.typography.h6,
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 16.dp)
)
}
}
}
is WidgetView.Action.CreateWidget -> {
Box(Modifier.fillMaxWidth()) {
WidgetActionButton(
@ -141,6 +156,23 @@ fun HomeScreen(
}
}
@Composable
private fun TreeWidgetHeader(item: WidgetView.Tree) {
Row() {
Text(
// TODO trimming should be a part of presentation module.
text = item.obj.name.orEmpty().trim(),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(vertical = 8.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = ">",
modifier = Modifier.align(Alignment.CenterVertically).rotate(90f)
)
}
}
@Composable
fun WidgetActionButton(
modifier: Modifier,

View file

@ -0,0 +1,40 @@
package com.anytypeio.anytype.core_models.ext
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Struct
fun Map<Id, Struct>.process(event: Event.Command.Details) = when (event) {
is Event.Command.Details.Set -> set(event.target, event.details.map)
is Event.Command.Details.Amend -> amend(event.target, event.details)
is Event.Command.Details.Unset -> unset(event.target, event.keys)
}
fun Map<Id, Struct>.set(
target: Id,
details: Struct
): Map<Id, Struct> = this + (target to details)
fun Map<Id, Struct>.amend(
target: Id,
slice: Map<Id, Any?>
): Map<Id, Struct> {
val curr = getOrDefault(target, emptyMap())
val update = buildMap<Id, Struct> {
if (curr.isNotEmpty()) {
put(target, curr + slice)
} else {
put(target, slice)
}
}
return this + update
}
fun Map<Id, Struct>.unset(
target: Id,
keys: List<Id>
): Map<Id, Struct> {
val curr = getOrDefault(target, emptyMap())
val update = curr - keys
return this + mapOf(target to update)
}

View file

@ -7,6 +7,7 @@ import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.ext.process
import com.anytypeio.anytype.core_utils.ext.replace
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.Resultat
@ -18,10 +19,12 @@ import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
import com.anytypeio.anytype.domain.widgets.CreateWidget
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.widgets.LinkWidgetContainer
import com.anytypeio.anytype.presentation.widgets.TreePath
import com.anytypeio.anytype.presentation.widgets.TreeWidgetBranchStateHolder
import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer
import com.anytypeio.anytype.presentation.widgets.Widget
import com.anytypeio.anytype.presentation.widgets.WidgetContainer
import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent
import com.anytypeio.anytype.presentation.widgets.WidgetView
import com.anytypeio.anytype.presentation.widgets.parseWidgets
@ -52,7 +55,7 @@ class HomeScreenViewModel(
private val objectViewState = MutableStateFlow<ObjectViewState>(ObjectViewState.Idle)
private val widgets = MutableStateFlow<List<Widget>>(emptyList())
private val containers = MutableStateFlow<List<TreeWidgetContainer>>(emptyList())
private val containers = MutableStateFlow<List<WidgetContainer>>(emptyList())
private val expanded = TreeWidgetBranchStateHolder()
init {
@ -107,14 +110,14 @@ class HomeScreenViewModel(
widgets.map {
it.map { w ->
when (w) {
is Widget.Link -> TODO()
is Widget.Tree -> {
TreeWidgetContainer(
widget = w,
container = objectSearchSubscriptionContainer,
expandedBranches = expanded.stream(w.id)
)
}
is Widget.Link -> LinkWidgetContainer(
widget = w
)
is Widget.Tree -> TreeWidgetContainer(
widget = w,
container = objectSearchSubscriptionContainer,
expandedBranches = expanded.stream(w.id)
)
}
}
}.collect {
@ -217,9 +220,7 @@ class HomeScreenViewModel(
event.events.forEach { e ->
when (e) {
is Event.Command.AddBlock -> {
curr = curr.copy(
blocks = curr.blocks + e.blocks
)
curr = curr.copy(blocks = curr.blocks + e.blocks)
}
is Event.Command.UpdateStructure -> {
curr = curr.copy(
@ -231,6 +232,9 @@ class HomeScreenViewModel(
)
)
}
is Event.Command.Details -> {
curr = curr.copy(details = curr.details.process(e))
}
else -> {
Timber.d("Skipping event: $e")
}

View file

@ -0,0 +1,15 @@
package com.anytypeio.anytype.presentation.widgets
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class LinkWidgetContainer(
private val widget: Widget.Link
) : WidgetContainer {
override val view: Flow<WidgetView.Link> = flowOf(
WidgetView.Link(
id = widget.id,
obj = widget.source
)
)
}

View file

@ -15,11 +15,11 @@ class TreeWidgetContainer(
private val widget: Widget.Tree,
private val container: ObjectSearchSubscriptionContainer,
private val expandedBranches: Flow<List<TreePath>>
) {
) : WidgetContainer {
private val store = mutableMapOf<Id, ObjectWrapper.Basic>()
val view: Flow<WidgetView.Tree> = expandedBranches.mapLatest {
override val view: Flow<WidgetView.Tree> = expandedBranches.mapLatest {
container.get(
subscription = widget.id,
keys = keys,

View file

@ -56,9 +56,11 @@ fun List<Block>.parseWidgets(
)
}
Block.Content.Widget.Layout.LINK -> {
Widget.Link(
id = w.id,
source = data
add(
Widget.Link(
id = w.id,
source = data
)
)
}
}

View file

@ -0,0 +1,7 @@
package com.anytypeio.anytype.presentation.widgets
import kotlinx.coroutines.flow.Flow
sealed interface WidgetContainer {
val view: Flow<WidgetView>
}

View file

@ -24,6 +24,11 @@ sealed class WidgetView {
}
}
data class Link(
val id: Id,
val obj: ObjectWrapper.Basic,
) : WidgetView()
sealed class Action : WidgetView() {
object EditWidgets : Action()
object CreateWidget: Action()

View file

@ -199,6 +199,81 @@ class HomeViewModelTest {
}
}
@Test
fun `should emit link-widget and actions`() = runTest {
// SETUP
val sourceObject = StubObject(
id = "SOURCE OBJECT",
links = emptyList()
)
val sourceLink = StubLinkToObjectBlock(
id = "SOURCE LINK",
target = sourceObject.id
)
val widgetBlock = StubWidgetBlock(
id = "WIDGET BLOCK",
layout = Block.Content.Widget.Layout.LINK,
children = listOf(sourceLink.id)
)
val smartBlock = StubSmartBlock(
id = WIDGET_OBJECT_ID,
children = listOf(widgetBlock.id),
type = SmartBlockType.WIDGET
)
val givenObjectView = StubObjectView(
root = WIDGET_OBJECT_ID,
type = SmartBlockType.WIDGET,
blocks = listOf(
smartBlock,
widgetBlock,
sourceLink
),
details = mapOf(
sourceObject.id to sourceObject.map
)
)
stubConfig()
stubInterceptEvents(events = emptyFlow())
stubOpenObject(givenObjectView)
stubObjectSearchContainer(
subscription = widgetBlock.id,
targets = emptyList()
)
val vm = buildViewModel()
// TESTING
vm.views.test {
val firstTimeState = awaitItem()
assertEquals(
actual = firstTimeState,
expected = HomeScreenViewModel.actions
)
val secondTimeItem = awaitItem()
assertEquals(
expected = buildList {
add(
WidgetView.Link(
id = widgetBlock.id,
obj = sourceObject
)
)
addAll(HomeScreenViewModel.actions)
},
actual = secondTimeItem
)
verify(openObject, times(1)).invoke(WIDGET_OBJECT_ID)
}
}
private fun stubInterceptEvents(events: Flow<List<Event>>) {
interceptEvents.stub {
on { build(InterceptEvents.Params(WIDGET_OBJECT_ID)) } doReturn events