Refactor OverviewScreen

This commit is contained in:
Jonas Lochmann 2023-02-06 01:00:00 +01:00
parent 6b9aebbeaf
commit 2971e3f55d
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
8 changed files with 315 additions and 242 deletions

View file

@ -32,7 +32,7 @@ fun ScreenMultiplexer(
) { ) {
when (screen) { when (screen) {
null -> {/* nothing to do */ } null -> {/* nothing to do */ }
is Screen.OverviewScreen -> OverviewScreen(screen.content, executeCommand, modifier = modifier) is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier)
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier) is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier)
} }
} }

View file

@ -16,11 +16,9 @@
package io.timelimit.android.ui.model package io.timelimit.android.ui.model
import android.app.Application import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
@ -34,10 +32,6 @@ import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
class MainModel(application: Application): AndroidViewModel(application) { class MainModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "MainModel"
}
val activityModel = ActivityViewModel(application) val activityModel = ActivityViewModel(application)
private val logic = DefaultAppLogic.with(application) private val logic = DefaultAppLogic.with(application)
@ -109,13 +103,7 @@ class MainModel(application: Application): AndroidViewModel(application) {
} }
fun execute(command: UpdateStateCommand) { fun execute(command: UpdateStateCommand) {
state.update { oldState -> command.applyTo(state)
command.transform(oldState) ?: oldState.also {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "execute($command) did not transform state")
}
}
}
} }
fun reportAuthenticationScreenClosed() { fun reportAuthenticationScreenClosed() {

View file

@ -15,11 +15,29 @@
*/ */
package io.timelimit.android.ui.model package io.timelimit.android.ui.model
import android.util.Log
import io.timelimit.android.BuildConfig
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
sealed class UpdateStateCommand { sealed class UpdateStateCommand {
companion object {
private const val LOG_TAG = "UpdateStateCommand"
}
abstract fun transform(state: State): State? abstract fun transform(state: State): State?
fun applyTo(state: MutableStateFlow<State>) {
state.update { oldState ->
transform(oldState) ?: oldState.also {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "$this.transform() did not transform state")
}
}
}
}
object Reset: UpdateStateCommand() { object Reset: UpdateStateCommand() {
override fun transform(state: State) = State.LaunchState override fun transform(state: State) = State.LaunchState
} }

View file

@ -40,7 +40,6 @@ import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import java.util.*
object OverviewHandling { object OverviewHandling {
fun processState( fun processState(
@ -104,6 +103,11 @@ object OverviewHandling {
} }
} }
}, },
addUser = {
launch {
UpdateStateCommand.Overview.AddUser.applyTo(stateLive)
}
},
skipTaskReview = { task -> skipTaskReview = { task ->
launch { launch {
lock.tryWithLock { lock.tryWithLock {
@ -176,17 +180,31 @@ object OverviewHandling {
} }
} }
stateLive.update { state -> UpdateStateCommand.Overview.ManageChild(user.id).applyTo(stateLive)
if (state is State.Overview) State.ManageChild.Main(state, user.id, fromRedirect = false)
else state
}
}
UserType.Parent -> stateLive.update { state ->
if (state is State.Overview) State.ManageParent.Main(state, user.id)
else state
} }
UserType.Parent -> UpdateStateCommand.Overview.ManageParent(user.id).applyTo(stateLive)
} }
} }
},
openDevice = { device ->
launch {
UpdateStateCommand.Overview.ManageDevice(device.device.id).applyTo(stateLive)
}
},
setupDevice = {
launch {
UpdateStateCommand.Overview.SetupDevice.applyTo(stateLive)
}
},
showMoreDevices = {
launch {
UpdateStateCommand.Overview.ShowMoreDevices(it).applyTo(stateLive)
}
},
showMoreUsers = {
launch {
UpdateStateCommand.Overview.ShowAllUsers.applyTo(stateLive)
}
} }
) )
} }
@ -367,10 +385,15 @@ object OverviewHandling {
data class Actions( data class Actions(
val hideIntro: () -> Unit, val hideIntro: () -> Unit,
val addDevice: () -> Unit, val addDevice: () -> Unit,
val addUser: () -> Unit,
val skipTaskReview: (TaskToReview) -> Unit, val skipTaskReview: (TaskToReview) -> Unit,
val reviewReject: (TaskToReview) -> Unit, val reviewReject: (TaskToReview) -> Unit,
val reviewAccept: (TaskToReview) -> Unit, val reviewAccept: (TaskToReview) -> Unit,
val openUser: (UserItem) -> Unit val openUser: (UserItem) -> Unit,
val openDevice: (DeviceItem) -> Unit,
val setupDevice: () -> Unit,
val showMoreDevices: (OverviewState.DeviceList) -> Unit,
val showMoreUsers: () -> Unit
) )
data class IntroFlags( data class IntroFlags(
val showSetupOption: Boolean, val showSetupOption: Boolean,

View file

@ -19,6 +19,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@ -28,24 +30,50 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("devices", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement())
}
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
DeviceItem(it, screen.actions.openDevice)
}
if (screen.devices.canAdd) {
item (key = Pair("devices", "add")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = screen.actions.addDevice,
modifier = Modifier.animateItemPlacement()
)
}
}
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "more")) {
ListCommon.ShowMoreItem(
modifier = Modifier.animateItemPlacement(),
action = { screen.actions.showMoreDevices(screen.devices.canShowMore) }
)
}
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LazyItemScope.DeviceItem( fun LazyItemScope.DeviceItem(
item: OverviewHandling.DeviceItem, item: OverviewHandling.DeviceItem,
executeCommand: (UpdateStateCommand) -> Unit openAction: (OverviewHandling.DeviceItem) -> Unit
) { ) {
ListCardCommon.Card( ListCardCommon.Card(
Modifier Modifier
.animateItemPlacement() .animateItemPlacement()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clickable( .clickable(onClick = { openAction(item) })
onClick = {
executeCommand(UpdateStateCommand.Overview.ManageDevice(item.device.id))
}
)
) { ) {
ListCardCommon.TextWithIcon( ListCardCommon.TextWithIcon(
icon = Icons.Default.Smartphone, icon = Icons.Default.Smartphone,

View file

@ -0,0 +1,196 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.timelimit.android.R
import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
fun LazyListScope.introItems(
screen: OverviewHandling.OverviewScreen,
) {
if (screen.intro.showSetupOption) {
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_finish_setup_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_finish_setup_text))
ListCardCommon.ActionButton(
label = stringResource(R.string.generic_go),
action = screen.actions.setupDevice
)
}
}
}
if (screen.intro.showOutdatedServer) {
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_outdated_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_server_outdated_text))
}
}
}
if (screen.intro.showServerMessage != null) {
item (key = Pair("intro", "server message")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_message),
style = MaterialTheme.typography.h6
)
Text(screen.intro.showServerMessage)
}
}
}
if (screen.intro.showIntro) {
item (key = Pair("intro", "intro")) {
val state = remember {
DismissState(
initialValue = DismissValue.Default,
confirmStateChange = {
screen.actions.hideIntro()
true
}
)
}
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_intro_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_intro_text))
Text(
stringResource(R.string.generic_swipe_to_dismiss),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
if (screen.taskToReview != null) {
item (key = Pair("intro", "task review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.task_review_title),
style = MaterialTheme.typography.h6
)
Text(
stringResource(R.string.task_review_text, screen.taskToReview.task.childName, screen.taskToReview.task.childTask.taskTitle)
)
Text(
stringResource(
R.string.task_review_category,
TimeTextUtil.time(screen.taskToReview.task.childTask.extraTimeDuration, LocalContext.current),
screen.taskToReview.task.categoryTitle
),
style = MaterialTheme.typography.subtitle1
)
screen.taskToReview.task.childTask.lastGrantTimestamp.let { lastGrantTimestamp ->
if (lastGrantTimestamp != 0L) {
Text(
stringResource(
R.string.task_review_last_grant,
DateUtil.formatAbsoluteDate(LocalContext.current, lastGrantTimestamp)
),
style = MaterialTheme.typography.subtitle1
)
}
}
Row {
TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview)
}) {
Text(stringResource(R.string.generic_skip))
}
Spacer(Modifier.weight(1.0f))
OutlinedButton(onClick = { screen.actions.reviewReject(screen.taskToReview) }) {
Text(stringResource(R.string.generic_no))
}
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
Text(stringResource(R.string.generic_yes))
}
}
Text(
stringResource(R.string.purchase_required_info_local_mode_free),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}

View file

@ -15,30 +15,16 @@
*/ */
package io.timelimit.android.ui.overview.overview package io.timelimit.android.ui.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.timelimit.android.R
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
fun OverviewScreen( fun OverviewScreen(
screen: OverviewHandling.OverviewScreen, screen: OverviewHandling.OverviewScreen,
executeCommand: (UpdateStateCommand) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
LazyColumn ( LazyColumn (
@ -46,198 +32,8 @@ fun OverviewScreen(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier modifier = modifier
) { ) {
if (screen.intro.showSetupOption) { introItems(screen)
item (key = Pair("intro", "finish setup")) { deviceItems(screen)
ListCardCommon.Card( userItems(screen)
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_finish_setup_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_finish_setup_text))
ListCardCommon.ActionButton(
label = stringResource(R.string.generic_go),
action = {
executeCommand(UpdateStateCommand.Overview.SetupDevice)
}
)
}
}
}
if (screen.intro.showOutdatedServer) {
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_outdated_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_server_outdated_text))
}
}
}
if (screen.intro.showServerMessage != null) {
item (key = Pair("intro", "servermessage")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_message),
style = MaterialTheme.typography.h6
)
Text(screen.intro.showServerMessage)
}
}
}
if (screen.intro.showIntro) {
item (key = Pair("intro", "intro")) {
val state = remember {
DismissState(
initialValue = DismissValue.Default,
confirmStateChange = {
screen.actions.hideIntro()
true
}
)
}
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_intro_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_intro_text))
Text(
stringResource(R.string.generic_swipe_to_dismiss),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
if (screen.taskToReview != null) {
item (key = Pair("task", "review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.task_review_title),
style = MaterialTheme.typography.h6
)
Text(
stringResource(R.string.task_review_text, screen.taskToReview.task.childName, screen.taskToReview.task.childTask.taskTitle)
)
Text(
stringResource(
R.string.task_review_category,
TimeTextUtil.time(screen.taskToReview.task.childTask.extraTimeDuration, LocalContext.current),
screen.taskToReview.task.categoryTitle
),
style = MaterialTheme.typography.subtitle1
)
screen.taskToReview.task.childTask.lastGrantTimestamp.let { lastGrantTimestamp ->
if (lastGrantTimestamp != 0L) {
Text(
stringResource(
R.string.task_review_last_grant,
DateUtil.formatAbsoluteDate(LocalContext.current, lastGrantTimestamp)
),
style = MaterialTheme.typography.subtitle1
)
}
}
Row {
TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview)
}) {
Text(stringResource(R.string.generic_skip))
}
Spacer(Modifier.weight(1.0f))
OutlinedButton(onClick = { screen.actions.reviewReject(screen.taskToReview) }) {
Text(stringResource(R.string.generic_no))
}
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
Text(stringResource(R.string.generic_yes))
}
}
Text(
stringResource(R.string.purchase_required_info_local_mode_free),
style = MaterialTheme.typography.subtitle1
)
}
}
}
item (key = Pair("devices", "header")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement()) }
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
DeviceItem(it, executeCommand)
}
if (screen.devices.canAdd) {
item (key = Pair("devices", "add")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = screen.actions.addDevice,
modifier = Modifier.animateItemPlacement()
)
}
}
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "show more")) { ListCommon.ShowMoreItem(modifier = Modifier.animateItemPlacement()) {
executeCommand(UpdateStateCommand.Overview.ShowMoreDevices(screen.devices.canShowMore))
}}
}
item (key = Pair("header", "users")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement()) }
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) }
if (screen.users.canAdd) item (key = Pair("header", "user.create")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = { executeCommand(UpdateStateCommand.Overview.AddUser) },
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("header", "user.more")) {
ListCommon.ShowMoreItem (modifier = Modifier.animateItemPlacement()) { executeCommand(UpdateStateCommand.Overview.ShowAllUsers) }
}
} }
} }

View file

@ -19,12 +19,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.AlarmOff
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -33,6 +32,31 @@ import io.timelimit.android.R
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.ui.model.main.OverviewHandling import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("users", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement())
}
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) }
if (screen.users.canAdd) item (key = Pair("users", "create")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = screen.actions.addUser,
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("users", "more")) {
ListCommon.ShowMoreItem (
modifier = Modifier.animateItemPlacement(),
action = screen.actions.showMoreUsers
)
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LazyItemScope.UserItem( fun LazyItemScope.UserItem(