refactor(NodeItem): replace NodeInfo with NodeEntity

This commit is contained in:
andrekir 2024-09-21 15:45:10 -03:00 committed by Andre K
parent 89a3171b58
commit 83dc389d6d
12 changed files with 373 additions and 160 deletions

View file

@ -3,7 +3,6 @@ package com.geeksville.mesh.ui
import android.content.ActivityNotFoundException
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -11,91 +10,79 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.Position
import com.geeksville.mesh.R
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.GpsCoordinateFormat
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.ui.theme.HyperlinkBlue
import com.geeksville.mesh.util.GPSFormat
import java.net.URLEncoder
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LinkedCoordinates(
modifier: Modifier = Modifier,
position: Position?,
latitude: Double,
longitude: Double,
format: Int,
nodeName: String?
nodeName: String,
) {
if (position?.isValid() == true) {
val uriHandler = LocalUriHandler.current
val style = SpanStyle(
color = HyperlinkBlue,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.Underline
val uriHandler = LocalUriHandler.current
val style = SpanStyle(
color = HyperlinkBlue,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.Underline
)
val annotatedString = buildAnnotatedString {
pushStringAnnotation(
tag = "gps",
// URI scheme is defined at:
// https://developer.android.com/guide/components/intents-common#Maps
annotation = "geo:0,0?q=${latitude},${longitude}&z=17&label=${
URLEncoder.encode(nodeName, "utf-8")
}"
)
val name = nodeName ?: stringResource(id = R.string.unknown_username)
val annotatedString = buildAnnotatedString {
pushStringAnnotation(
tag = "gps",
// URI scheme is defined at:
// https://developer.android.com/guide/components/intents-common#Maps
annotation = "geo:0,0?q=${position.latitude},${position.longitude}&z=17&label=${
URLEncoder.encode(name, "utf-8")
}"
)
withStyle(style = style) {
append(position.gpsString(format))
withStyle(style = style) {
val gpsString = when (format) {
GpsCoordinateFormat.DEC_VALUE -> GPSFormat.toDEC(latitude, longitude)
GpsCoordinateFormat.DMS_VALUE -> GPSFormat.toDMS(latitude, longitude)
GpsCoordinateFormat.UTM_VALUE -> GPSFormat.toUTM(latitude, longitude)
GpsCoordinateFormat.MGRS_VALUE -> GPSFormat.toMGRS(latitude, longitude)
else -> GPSFormat.toDEC(latitude, longitude)
}
pop()
append(gpsString)
}
val clipboardManager: ClipboardManager = LocalClipboardManager.current
Text(
modifier = modifier.combinedClickable(
onClick = {
annotatedString.getStringAnnotations(
tag = "gps",
start = 0,
end = annotatedString.length
).firstOrNull()?.let {
try {
uriHandler.openUri(it.item)
} catch (ex: ActivityNotFoundException) {
debug("No application found: $ex")
}
pop()
}
val clipboardManager: ClipboardManager = LocalClipboardManager.current
Text(
modifier = modifier.combinedClickable(
onClick = {
annotatedString.getStringAnnotations(
tag = "gps",
start = 0,
end = annotatedString.length
).firstOrNull()?.let {
try {
uriHandler.openUri(it.item)
} catch (ex: ActivityNotFoundException) {
debug("No application found: $ex")
}
},
onLongClick = {
clipboardManager.setText(annotatedString)
debug("Copied to clipboard")
}
),
text = annotatedString
)
} else {
// Placeholder for ConstraintLayoutReference; renders no visible content
Box(modifier = modifier)
}
}
@Composable
@Preview
fun LinkedCoordinatesSimplePreview() {
AppTheme {
LinkedCoordinates(
position = Position(37.7749, -122.4194, 0),
format = 1,
nodeName = "Test Node Name"
)
}
},
onLongClick = {
clipboardManager.setText(annotatedString)
debug("Copied to clipboard")
}
),
text = annotatedString
)
}
@PreviewLightDark
@ -105,7 +92,8 @@ fun LinkedCoordinatesPreview(
) {
AppTheme {
LinkedCoordinates(
position = Position(37.7749, -122.4194, 0),
latitude = 37.7749,
longitude = -122.4194,
format = format,
nodeName = "Test Node Name"
)
@ -115,4 +103,4 @@ fun LinkedCoordinatesPreview(
class GPSFormatPreviewParameterProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int>
get() = sequenceOf(0, 1, 2)
}
}

View file

@ -1,8 +1,5 @@
@file:Suppress(
"FunctionNaming",
"LongMethod",
"LongParameterList",
"DestructuringDeclarationWithTooManyEntries",
"MagicNumber",
"CyclomaticComplexMethod",
)
@ -53,20 +50,22 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.DeviceConfig
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.compose.ElevationInfo
import com.geeksville.mesh.ui.compose.SatelliteCountInfo
import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.metersIn
import com.geeksville.mesh.util.toDistanceString
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun NodeItem(
thisNodeInfo: NodeInfo?,
thatNodeInfo: NodeInfo,
thisNode: NodeEntity?,
thatNode: NodeEntity,
gpsFormat: Int,
distanceUnits: Int,
tempInFahrenheit: Boolean,
@ -75,28 +74,29 @@ fun NodeItem(
blinking: Boolean = false,
expanded: Boolean = false,
currentTimeMillis: Long,
hasPublicKey: Boolean = false,
) {
val isUnknownUser = thatNodeInfo.user?.hwModel == MeshProtos.HardwareModel.UNSET
val hasPublicKey = !thatNode.user.publicKey.isEmpty
val isUnknownUser = thatNode.user.hwModel == MeshProtos.HardwareModel.UNSET
val unknownShortName = stringResource(id = R.string.unknown_node_short_name)
val longName = thatNodeInfo.user?.longName ?: stringResource(id = R.string.unknown_username)
val longName = thatNode.user.longName.ifEmpty { stringResource(id = R.string.unknown_username) }
val nodeName = if (hasPublicKey) "🔒 $longName" else longName
val isThisNode = thisNodeInfo?.num == thatNodeInfo.num
val distance = thisNodeInfo?.distanceStr(thatNodeInfo, distanceUnits)
val (textColor, nodeColor) = thatNodeInfo.colors
val isThisNode = thisNode?.num == thatNode.num
val distance = thisNode?.distance(thatNode)?.let {
val system = DisplayConfig.DisplayUnits.forNumber(distanceUnits)
if (it == 0) null else it.toDistanceString(system)
}
val (textColor, nodeColor) = thatNode.colors
val position = thatNodeInfo.position
val hwInfoString = thatNodeInfo.user?.hwModelString ?: MeshProtos.HardwareModel.UNSET.name
val roleName =
if (isUnknownUser) {
DeviceConfig.Role.UNRECOGNIZED.name
} else {
thatNodeInfo.user?.role?.let { role ->
DeviceConfig.Role.forNumber(role)?.name
} ?: DeviceConfig.Role.UNRECOGNIZED.name
}
val nodeId = thatNodeInfo.user?.id ?: "???"
val hwInfoString = thatNode.user.hwModel.let { hwModel ->
if (hwModel == MeshProtos.HardwareModel.UNSET) MeshProtos.HardwareModel.UNSET.name
else hwModel.name.replace('_', '-').replace('p', '.').lowercase()
}
val roleName = if (isUnknownUser) {
DeviceConfig.Role.UNRECOGNIZED.name
} else {
thatNode.user.role.name
}
val nodeId = thatNode.user.id.ifEmpty { "???" }
val highlight = Color(0x33FFFFFF)
val bgColor by animateColorAsState(
@ -153,7 +153,7 @@ fun NodeItem(
content = {
Text(
modifier = Modifier.fillMaxWidth(),
text = thatNodeInfo.user?.shortName ?: unknownShortName,
text = thatNode.user.shortName.ifEmpty { unknownShortName },
fontWeight = FontWeight.Normal,
fontSize = MaterialTheme.typography.button.fontSize,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
@ -163,14 +163,14 @@ fun NodeItem(
)
Text(
modifier = Modifier.weight(1f),
text = nodeName,
text = if (hasPublicKey) "🔒 $longName" else longName,
style = style,
textDecoration = TextDecoration.LineThrough.takeIf { isIgnored },
softWrap = true,
)
LastHeardInfo(
lastHeard = thatNodeInfo.lastHeard,
lastHeard = thatNode.lastHeard,
currentTimeMillis = currentTimeMillis
)
}
@ -188,8 +188,8 @@ fun NodeItem(
Spacer(modifier = Modifier.width(16.dp))
}
BatteryInfo(
batteryLevel = thatNodeInfo.batteryLevel,
voltage = thatNodeInfo.voltage
batteryLevel = thatNode.batteryLevel,
voltage = thatNode.voltage
)
}
Spacer(modifier = Modifier.height(4.dp))
@ -198,11 +198,11 @@ fun NodeItem(
horizontalArrangement = Arrangement.SpaceBetween
) {
signalInfo(
nodeInfo = thatNodeInfo,
node = thatNode,
isThisNode = isThisNode
)
if (position?.isValid() == true) {
val satCount = position.satellitesInView
thatNode.validPosition?.let { position ->
val satCount = position.satsInView
if (satCount > 0) {
SatelliteCountInfo(
satCount = satCount
@ -215,9 +215,10 @@ fun NodeItem(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
thatNodeInfo.environmentMetrics?.getDisplayString(tempInFahrenheit)?.let { envMetrics ->
val telemetryString = thatNode.getTelemetryString(tempInFahrenheit)
if (telemetryString.isNotEmpty()) {
Text(
text = envMetrics,
text = telemetryString,
color = MaterialTheme.colors.onSurface,
fontSize = MaterialTheme.typography.button.fontSize
)
@ -233,16 +234,19 @@ fun NodeItem(
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
DisableSelection {
LinkedCoordinates(
position = position,
format = gpsFormat,
nodeName = nodeName
)
thatNode.validPosition?.let {
DisableSelection {
LinkedCoordinates(
latitude = thatNode.latitude,
longitude = thatNode.longitude,
format = gpsFormat,
nodeName = longName
)
}
}
val system =
ConfigProtos.Config.DisplayConfig.DisplayUnits.forNumber(distanceUnits)
if (position?.isValid() == true) {
thatNode.validPosition?.let { position ->
val altitude = position.altitude.metersIn(system)
val elevationSuffix = stringResource(id = R.string.elevation_suffix)
ElevationInfo(
@ -266,13 +270,15 @@ fun NodeItem(
modifier = Modifier.weight(1f),
text = roleName,
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.button.fontSize
fontSize = MaterialTheme.typography.button.fontSize,
style = style,
)
Text(
modifier = Modifier.weight(1f),
text = nodeId,
textAlign = TextAlign.End,
fontSize = MaterialTheme.typography.button.fontSize
fontSize = MaterialTheme.typography.button.fontSize,
style = style,
)
}
}
@ -286,11 +292,11 @@ fun NodeItem(
@Preview(showBackground = false)
fun NodeInfoSimplePreview() {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
val thatNodeInfo = NodeInfoPreviewParameterProvider().values.last()
val thisNode = NodeEntityPreviewParameterProvider().values.first()
val thatNode = NodeEntityPreviewParameterProvider().values.last()
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
1,
0,
true,
@ -305,19 +311,19 @@ fun NodeInfoSimplePreview() {
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
)
fun NodeInfoPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
thatNodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
thatNode: NodeEntity
) {
AppTheme {
val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first()
val thisNode = NodeEntityPreviewParameterProvider().values.first()
Column {
Text(
text = "Details Collapsed",
color = MaterialTheme.colors.onBackground
)
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
gpsFormat = 0,
distanceUnits = 1,
tempInFahrenheit = true,
@ -329,8 +335,8 @@ fun NodeInfoPreview(
color = MaterialTheme.colors.onBackground
)
NodeItem(
thisNodeInfo = thisNodeInfo,
thatNodeInfo = thatNodeInfo,
thisNode = thisNode,
thatNode = thatNode,
gpsFormat = 0,
distanceUnits = 1,
tempInFahrenheit = true,

View file

@ -26,7 +26,6 @@ import com.geeksville.mesh.databinding.SettingsFragmentBinding
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.util.exceptionToSnackbar
@ -218,10 +217,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.usernameEditText.onEditorAction(EditorInfo.IME_ACTION_DONE) {
debug("received IME_ACTION_DONE")
val n = binding.usernameEditText.text.toString().trim()
model.ourNodeInfo.value?.user?.let {
val user = it.copy(longName = n, shortName = getInitials(n))
if (n.isNotEmpty()) model.setOwner(user)
}
if (n.isNotEmpty()) model.setOwner(n)
requireActivity().hideKeyboard()
}

View file

@ -8,31 +8,31 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.ui.preview.NodeEntityPreviewParameterProvider
import com.geeksville.mesh.ui.theme.AppTheme
@Composable
fun signalInfo(
modifier: Modifier = Modifier,
nodeInfo: NodeInfo,
node: NodeEntity,
isThisNode: Boolean
): Boolean {
val text = if (isThisNode) {
stringResource(R.string.channel_air_util).format(
nodeInfo.deviceMetrics?.channelUtilization,
nodeInfo.deviceMetrics?.airUtilTx
node.deviceMetrics.channelUtilization,
node.deviceMetrics.airUtilTx
)
} else {
buildList {
if (nodeInfo.channel > 0) add("ch:${nodeInfo.channel}")
if (nodeInfo.hopsAway == 0) {
if (nodeInfo.snr < 100F && nodeInfo.rssi < 0) {
add("RSSI: %d SNR: %.1f".format(nodeInfo.rssi, nodeInfo.snr))
if (node.channel > 0) add("ch:${node.channel}")
if (node.hopsAway == 0) {
if (node.snr < 100F && node.rssi < 0) {
add("RSSI: %d SNR: %.1f".format(node.rssi, node.snr))
}
} else {
add("%s: %d".format(stringResource(R.string.hops_away), nodeInfo.hopsAway))
add("%s: %d".format(stringResource(R.string.hops_away), node.hopsAway))
}
}.joinToString(" ")
}
@ -54,15 +54,12 @@ fun signalInfo(
fun SignalInfoSimplePreview() {
AppTheme {
signalInfo(
nodeInfo = NodeInfo(
node = NodeEntity(
num = 1,
position = null,
lastHeard = 0,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceMetrics = null,
user = null,
hopsAway = 0
),
isThisNode = false
@ -73,12 +70,12 @@ fun SignalInfoSimplePreview() {
@PreviewLightDark
@Composable
fun SignalInfoPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
nodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
node: NodeEntity
) {
AppTheme {
signalInfo(
nodeInfo = nodeInfo,
node = node,
isThisNode = false
)
}
@ -87,12 +84,12 @@ fun SignalInfoPreview(
@Composable
@PreviewLightDark
fun SignalInfoSelfPreview(
@PreviewParameter(NodeInfoPreviewParameterProvider::class)
nodeInfo: NodeInfo
@PreviewParameter(NodeEntityPreviewParameterProvider::class)
node: NodeEntity
) {
AppTheme {
signalInfo(
nodeInfo = nodeInfo,
node = node,
isThisNode = true
)
}

View file

@ -133,7 +133,7 @@ fun NodesScreen(
val state by model.nodesUiState.collectAsStateWithLifecycle()
val nodes by model.nodeList.collectAsStateWithLifecycle()
val ourNodeInfo by model.ourNodeInfo.collectAsStateWithLifecycle()
val ourNode by model.ourNodeInfo.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
val focusedNode by model.focusedNode.collectAsStateWithLifecycle()
@ -172,8 +172,8 @@ fun NodesScreen(
items(nodes, key = { it.num }) { node ->
val nodeInfo = node.toNodeInfo()
NodeItem(
thisNodeInfo = ourNodeInfo,
thatNodeInfo = nodeInfo,
thisNode = ourNode,
thatNode = node,
gpsFormat = state.gpsFormat,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
@ -185,7 +185,6 @@ fun NodesScreen(
blinking = nodeInfo == focusedNode,
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
hasPublicKey = !node.user.publicKey.isEmpty
)
}
}

View file

@ -318,7 +318,7 @@ fun MapView(
fun MapView.onNodesChanged(nodes: Collection<NodeInfo>): List<MarkerWithLabel> {
val nodesWithPosition = nodes.filter { it.validPosition != null }
val ourNode = model.ourNodeInfo.value
val ourNode = model.ourNodeInfo.value?.toNodeInfo()
val gpsFormat = model.config.display.gpsFormat.number
val displayUnits = model.config.display.units.number
return nodesWithPosition.map { node ->

View file

@ -0,0 +1,146 @@
package com.geeksville.mesh.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.geeksville.mesh.DeviceMetrics.Companion.currentTime
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.database.entity.NodeEntity
import com.geeksville.mesh.deviceMetrics
import com.geeksville.mesh.environmentMetrics
import com.geeksville.mesh.paxcount
import com.geeksville.mesh.position
import com.geeksville.mesh.telemetry
import com.geeksville.mesh.user
import kotlin.random.Random
class NodeEntityPreviewParameterProvider : PreviewParameterProvider<NodeEntity> {
val mickeyMouse = NodeEntity(
num = 1955,
user = user {
id = "mickeyMouseId"
longName = "Mickey Mouse"
shortName = "MM"
hwModel = MeshProtos.HardwareModel.TBEAM
},
longName = "Mickey Mouse",
shortName = "MM",
position = position {
latitudeI = 338125110
longitudeI = -1179189760
altitude = 138
satsInView = 4
},
latitude = 33.812511,
longitude = -117.918976,
lastHeard = currentTime(),
channel = 0,
snr = 12.5F,
rssi = -42,
deviceTelemetry = telemetry {
deviceMetrics = deviceMetrics {
channelUtilization = 2.4F
airUtilTx = 3.5F
batteryLevel = 85
voltage = 3.7F
uptimeSeconds = 3600
}
},
hopsAway = 0
)
private val minnieMouse = mickeyMouse.copy(
num = Random.nextInt(),
user = user {
longName = "Minnie Mouse"
shortName = "MiMo"
id = "minnieMouseId"
hwModel = MeshProtos.HardwareModel.HELTEC_V3
},
longName = "Minnie Mouse",
shortName = "MiMo",
snr = 12.5F,
rssi = -42,
position = position {},
latitude = 0.0,
longitude = 0.0,
hopsAway = 1
)
private val donaldDuck = NodeEntity(
num = Random.nextInt(),
position = position {
latitudeI = 338052347
longitudeI = -1179208460
altitude = 121
satsInView = 66
},
latitude = 33.8052347,
longitude = -117.9208460,
lastHeard = currentTime() - 300,
channel = 0,
snr = 12.5F,
rssi = -42,
deviceTelemetry = telemetry {
deviceMetrics = deviceMetrics {
channelUtilization = 2.4F
airUtilTx = 3.5F
batteryLevel = 85
voltage = 3.7F
uptimeSeconds = 3600
}
},
user = user {
id = "donaldDuckId"
longName = "Donald Duck, the Grand Duck of the Ducks"
shortName = "DoDu"
hwModel = MeshProtos.HardwareModel.HELTEC_V3
},
longName = "Donald Duck, the Grand Duck of the Ducks",
shortName = "DoDu",
environmentTelemetry = telemetry {
environmentMetrics = environmentMetrics {
temperature = 28.0F
relativeHumidity = 50.0F
barometricPressure = 1013.25F
gasResistance = 0.0F
voltage = 3.7F
current = 0.0F
iaq = 100
}
},
paxcounter = paxcount {
wifi = 30
ble = 39
uptime = 420
},
hopsAway = 2
)
private val unknown = donaldDuck.copy(
user = user {
id = "myId"
longName = "Meshtastic myId"
shortName = "myId"
hwModel = MeshProtos.HardwareModel.UNSET
},
longName = "Meshtastic myId",
shortName = null,
environmentTelemetry = telemetry {
environmentMetrics = environmentMetrics {}
},
paxcounter = paxcount {},
)
private val almostNothing = NodeEntity(
num = Random.nextInt(),
)
override val values: Sequence<NodeEntity>
get() = sequenceOf(
mickeyMouse, // "this" node
unknown,
almostNothing,
minnieMouse,
donaldDuck
)
}