feat(ui): complete app experience redesign
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package com.flagship.abox.manager.domain
|
||||
|
||||
import java.util.UUID
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ActivityEventWriter(
|
||||
private val repository: ActivityRepository,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
fun recordDevice(
|
||||
profileId: ProviderProfileId,
|
||||
deviceName: DeviceName,
|
||||
kind: ActivityEventKind,
|
||||
outcome: ActivityOutcome,
|
||||
errorCode: String? = null,
|
||||
source: ActivitySource = ActivitySource.USER,
|
||||
) {
|
||||
record(
|
||||
profileId = profileId,
|
||||
kind = kind,
|
||||
source = source,
|
||||
outcome = outcome,
|
||||
target = ActivityTarget(ActivityTargetType.DEVICE, deviceName.value, deviceName.value),
|
||||
errorCode = errorCode,
|
||||
)
|
||||
}
|
||||
|
||||
fun record(
|
||||
profileId: ProviderProfileId,
|
||||
kind: ActivityEventKind,
|
||||
source: ActivitySource,
|
||||
outcome: ActivityOutcome,
|
||||
target: ActivityTarget,
|
||||
errorCode: String? = null,
|
||||
) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
repository.append(
|
||||
ActivityEvent(
|
||||
id = ActivityEventId(UUID.randomUUID().toString()),
|
||||
profileId = profileId,
|
||||
occurredAtEpochMillis = System.currentTimeMillis(),
|
||||
kind = kind,
|
||||
source = source,
|
||||
outcome = outcome,
|
||||
target = target,
|
||||
errorCode = errorCode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-14
@@ -92,8 +92,7 @@ class InMemorySceneRepository : SceneRepository {
|
||||
|
||||
private fun flowFor(profileId: ProviderProfileId): MutableStateFlow<DomainResult<List<Scene>>> =
|
||||
flows.getOrPut(profileId) {
|
||||
val initial = if (profileId == LOCAL_EXPERIENCE_PROFILE) defaultLocalScenes(profileId) else emptyList()
|
||||
MutableStateFlow(DomainResult.Success(initial))
|
||||
MutableStateFlow(DomainResult.Success(defaultScenes(profileId)))
|
||||
}
|
||||
|
||||
private fun currentScenes(profileId: ProviderProfileId): List<Scene>? =
|
||||
@@ -107,12 +106,30 @@ class InMemorySceneRepository : SceneRepository {
|
||||
DomainError.InvalidRequest(detail = "Scene ${sceneId.value} not found"),
|
||||
)
|
||||
|
||||
private fun defaultLocalScenes(profileId: ProviderProfileId): List<Scene> = listOf(
|
||||
private fun defaultScenes(profileId: ProviderProfileId): List<Scene> = listOf(
|
||||
Scene(
|
||||
id = SceneId("${profileId.value}:scene-arriving-home"),
|
||||
profileId = profileId,
|
||||
name = "回家模式",
|
||||
description = "开启 Zigbee 插座控制器",
|
||||
type = AutomationType.MANUAL,
|
||||
enabled = true,
|
||||
icon = SceneIcon.HOME,
|
||||
color = SceneColor("#4CAF50"),
|
||||
actions = listOf(
|
||||
SceneAction.SocketPower(
|
||||
id = SceneActionId("${profileId.value}:scene-arriving-home:socket-1"),
|
||||
profileId = profileId,
|
||||
deviceName = DeviceName("Zigbee插座控制器"),
|
||||
powerState = SocketPowerState.ON,
|
||||
),
|
||||
),
|
||||
),
|
||||
Scene(
|
||||
id = SceneId("${profileId.value}:scene-leaving-home"),
|
||||
profileId = profileId,
|
||||
name = "离家模式",
|
||||
description = "关闭插座并延时关闭电视",
|
||||
description = "关闭 Zigbee 插座控制器",
|
||||
type = AutomationType.MANUAL,
|
||||
enabled = true,
|
||||
icon = SceneIcon.AWAY,
|
||||
@@ -124,16 +141,32 @@ class InMemorySceneRepository : SceneRepository {
|
||||
deviceName = DeviceName("Zigbee插座控制器"),
|
||||
powerState = SocketPowerState.OFF,
|
||||
),
|
||||
SceneAction.Delay(
|
||||
id = SceneActionId("${profileId.value}:scene-leaving-home:delay-1"),
|
||||
profileId = profileId,
|
||||
durationSeconds = 1,
|
||||
),
|
||||
),
|
||||
Scene(
|
||||
id = SceneId("${profileId.value}:scene-temperature-alert"),
|
||||
profileId = profileId,
|
||||
name = "高温提醒",
|
||||
description = "室温高于 28°C 时发送提醒",
|
||||
type = AutomationType.AUTOMATIC,
|
||||
enabled = true,
|
||||
icon = SceneIcon.THERMOSTAT,
|
||||
color = SceneColor("#EF5350"),
|
||||
triggers = listOf(
|
||||
SceneTrigger.Temperature(
|
||||
id = SceneTriggerId("${profileId.value}:trigger-temp"),
|
||||
deviceName = DeviceName("温湿度传感器"),
|
||||
operator = ComparisonOperator.GREATER_THAN,
|
||||
thresholdCelsius = 28.0,
|
||||
label = "温度高于 28°C",
|
||||
),
|
||||
SceneAction.InfraredKey(
|
||||
id = SceneActionId("${profileId.value}:scene-leaving-home:ir-1"),
|
||||
),
|
||||
actions = listOf(
|
||||
SceneAction.Notification(
|
||||
id = SceneActionId("${profileId.value}:scene-temperature-alert:notification-1"),
|
||||
profileId = profileId,
|
||||
deviceName = DeviceName("红外控制器"),
|
||||
key = "1",
|
||||
title = "温度提醒",
|
||||
message = "当前室温已高于 28°C",
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -148,7 +181,7 @@ class InMemorySceneRepository : SceneRepository {
|
||||
color = SceneColor("#26A69A"),
|
||||
triggers = listOf(
|
||||
SceneTrigger.Temperature(
|
||||
id = SceneTriggerId("${profileId.value}:trigger-temp"),
|
||||
id = SceneTriggerId("${profileId.value}:trigger-cooling-temp"),
|
||||
deviceName = DeviceName("温湿度传感器"),
|
||||
operator = ComparisonOperator.GREATER_THAN,
|
||||
thresholdCelsius = 28.0,
|
||||
@@ -157,7 +190,7 @@ class InMemorySceneRepository : SceneRepository {
|
||||
),
|
||||
conditions = listOf(
|
||||
SceneCondition.SocketPower(
|
||||
id = SceneConditionId("${profileId.value}:cond-socket"),
|
||||
id = SceneConditionId("${profileId.value}:cond-cooling-socket"),
|
||||
deviceName = DeviceName("Zigbee插座控制器"),
|
||||
expectedState = SocketPowerState.OFF,
|
||||
),
|
||||
|
||||
+2
-14
@@ -23,18 +23,6 @@ class AddVirtualRemoteKey(
|
||||
}
|
||||
|
||||
val buttonId = "CUSTOM_" + UUID.randomUUID().toString().take(8).uppercase()
|
||||
val assignedSlot = if (remote.transmissionMode == TransmissionMode.STATIC_SLOTS) {
|
||||
// Find next available slot
|
||||
val quota = when (val result = repository.slotQuota(profileId, remote.deviceName)) {
|
||||
is DomainResult.Success -> result.value
|
||||
is DomainResult.Failure -> return result
|
||||
}
|
||||
val usedSlots = quota.assignedStaticSlots
|
||||
val nextSlot = (1..99).firstOrNull { it !in usedSlots }
|
||||
?: return DomainResult.Failure(DomainError.InvalidRequest(detail = "槽位配额已满,无法分配静态槽位"))
|
||||
nextSlot.toString()
|
||||
} else null
|
||||
|
||||
val binding = RemoteKeyBinding(
|
||||
id = UUID.randomUUID().toString(),
|
||||
remoteId = remoteId,
|
||||
@@ -42,9 +30,9 @@ class AddVirtualRemoteKey(
|
||||
hardwareDeviceName = remote.deviceName,
|
||||
buttonId = buttonId,
|
||||
displayName = trimmed,
|
||||
assignedSlot = assignedSlot,
|
||||
assignedSlot = null,
|
||||
rawHexCode = null,
|
||||
status = InfraredKeyStatus.FAILED, // Initially unlearned
|
||||
status = InfraredKeyStatus.UNLEARNED,
|
||||
)
|
||||
|
||||
when (val res = repository.updateKeyBinding(binding)) {
|
||||
|
||||
+2
-2
@@ -69,7 +69,7 @@ class CreateVirtualRemote(
|
||||
displayName = template.displayName,
|
||||
assignedSlot = (index + 1).toString(),
|
||||
rawHexCode = template.defaultHexCode,
|
||||
status = if (template.defaultHexCode != null) InfraredKeyStatus.READY else InfraredKeyStatus.FAILED,
|
||||
status = if (template.defaultHexCode != null) InfraredKeyStatus.READY else InfraredKeyStatus.UNLEARNED,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ class CreateVirtualRemote(
|
||||
status = if (template.defaultHexCode != null || type == RemoteDeviceType.AIR_CONDITIONER) {
|
||||
InfraredKeyStatus.READY
|
||||
} else {
|
||||
InfraredKeyStatus.FAILED
|
||||
InfraredKeyStatus.UNLEARNED
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ data class InfraredKeyRef(
|
||||
)
|
||||
|
||||
enum class InfraredKeyStatus {
|
||||
UNLEARNED,
|
||||
LEARNING,
|
||||
READY,
|
||||
FAILED,
|
||||
|
||||
@@ -181,6 +181,7 @@ class RelearnInfraredKey(
|
||||
val result = repository.transitionStatus(
|
||||
ref = ref,
|
||||
expectedStatuses = setOf(
|
||||
InfraredKeyStatus.UNLEARNED,
|
||||
InfraredKeyStatus.READY,
|
||||
InfraredKeyStatus.FAILED,
|
||||
InfraredKeyStatus.LEARNING,
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ class LearnVirtualRemoteKey(
|
||||
is DomainResult.Success -> res.value ?: return DomainResult.Failure(DomainError.NoData(detail = "Remote not found"))
|
||||
is DomainResult.Failure -> return res
|
||||
}
|
||||
if (remote.type != RemoteDeviceType.CUSTOM) {
|
||||
return DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Only custom remotes support learning"),
|
||||
)
|
||||
}
|
||||
val bindings = when (val res = repository.getKeyBindings(remoteId)) {
|
||||
is DomainResult.Success -> res.value
|
||||
is DomainResult.Failure -> return res
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ data class RemoteKeyBinding(
|
||||
val displayName: String,
|
||||
val assignedSlot: String? = null,
|
||||
val rawHexCode: String? = null,
|
||||
val status: InfraredKeyStatus = InfraredKeyStatus.READY,
|
||||
val status: InfraredKeyStatus = InfraredKeyStatus.UNLEARNED,
|
||||
val lastErrorCode: String? = null,
|
||||
val lastErrorDetail: String? = null,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user