Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Sources/MijiaBarApp/Art.swift
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,50 @@ struct CookerArt: View {
}
}

struct DishwasherArt: View {
let washing: Bool, tint: Color
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
PowerTimeline(on: washing, running: true) { time, level in
ZStack {
Ambient(tint, level * 0.6, width: 170, height: 116).position(x: 90, y: 76)
RoundedRectangle(cornerRadius: 9, style: .continuous).appliance(tint, level)
.frame(width: 112, height: 118).position(x: 90, y: 70)
RoundedRectangle(cornerRadius: 4).fill(darkGlass)
.frame(width: 100, height: 17).position(x: 90, y: 24)
HStack(spacing: 5) {
Circle().fill(aluminum).frame(width: 5, height: 5)
Capsule().fill(tint).frame(width: 22, height: 3).opacity(0.15 + level * 0.85)
Spacer()
Circle().fill(tint).frame(width: 3, height: 3).opacity(0.15 + level * 0.85)
}
.frame(width: 80).position(x: 90, y: 24)
RoundedRectangle(cornerRadius: 6).fill(aluminum)
.frame(width: 100, height: 84).position(x: 90, y: 78)
RoundedRectangle(cornerRadius: 4).fill(darkGlass)
.overlay {
ZStack {
Rectangle().fill(LinearGradient(colors: [tint.opacity(0.06), tint.opacity(0.45)], startPoint: .top, endPoint: .bottom)).opacity(level)
ForEach(0..<8) { index in
let phase = (time * (reduceMotion ? 0 : 0.45) + Double(index) / 8).truncatingRemainder(dividingBy: 1)
Capsule().fill(tint.opacity(0.8)).frame(width: 2, height: 5)
.offset(x: CGFloat(index % 4) * 18 - 27, y: CGFloat(phase) * 48 - 24)
.opacity(level * sin(phase * .pi))
}
}
.clipShape(RoundedRectangle(cornerRadius: 4))
}
.overlay(RoundedRectangle(cornerRadius: 4).stroke(.white.opacity(0.18), lineWidth: 0.5))
.frame(width: 82, height: 58).position(x: 90, y: 82)
Capsule().fill(aluminum).frame(width: 56, height: 4).position(x: 90, y: 44)
RoundedRectangle(cornerRadius: 2).fill(darkGlass)
.frame(width: 92, height: 4).position(x: 90, y: 124)
}
.frame(width: 180, height: 136)
}
}
}

struct KettleArt: View {
let boiling: Bool, tint: Color
@Environment(\.accessibilityReduceMotion) private var reduceMotion
Expand Down
10 changes: 10 additions & 0 deletions Sources/MijiaBarApp/Snapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extension AppModel {
dev("d26", L("人体传感器", "Motion sensor"), "lumi.sensor_motion.demo", L("客厅", "Living room"), home: "h2"),
dev("d27", L("打印机", "Printer"), "hannto.printer.demo", L("书房", "Study"), home: "h2"),
dev("d28", L("电热水壶", "Electric kettle"), "yunmi.kettle.demo", L("厨房", "Kitchen"), home: "h2"),
dev("d29", L("洗碗机", "Dishwasher"), "viomi.dishwasher.demo", L("厨房", "Kitchen"), home: "h2"),
]
prefs.devices["d1"] = { var p = DevicePrefs(); p.favorite = true; return p }()
prefs.devices["d2"] = { var p = DevicePrefs(); p.favorite = true; return p }()
Expand Down Expand Up @@ -177,6 +178,11 @@ extension AppModel {
P(2, 2, "temperature", "uint8", ["read"], unit: "celsius"),
P(2, 3, "target-temperature", "uint8", range: [40, 100, 1], unit: "celsius")])])
states["d28"] = DeviceState(values: ["2.1": .int(1), "2.2": .int(96), "2.3": .int(100)], spec: kettle, legacy: false, source: "cloud")
let dishwasher = DeviceSpec(type: "", description: "", services: [S(2, "dishwasher", [
P(2, 1, "status", "uint8", ["read"], opts: [ValueOption(value: 1, description: "Washing"), ValueOption(value: 2, description: "Idle")]),
P(2, 2, "left-time", "uint16", ["read"], unit: "minutes"),
P(2, 3, "mode", "uint8", opts: [ValueOption(value: 0, description: "Auto"), ValueOption(value: 1, description: "Intense"), ValueOption(value: 2, description: "Eco")])])])
states["d29"] = DeviceState(values: ["2.1": .int(1), "2.2": .int(42), "2.3": .int(0)], spec: dishwasher, legacy: false, source: "cloud")
let purifier = DeviceSpec(type: "", description: "", services: [S(2, "water-purifier", [
P(2, 1, "tds-out", "uint16", ["read"], unit: "ppm"),
P(2, 2, "tds-in", "uint16", ["read"], unit: "ppm"),
Expand Down Expand Up @@ -207,6 +213,7 @@ struct ArtGallery: View {
("printer", AnyView(PrinterArt(busy: true, error: false, tint: .blue)), AnyView(PrinterArt(busy: false, error: false, tint: .blue))),
("cooker", AnyView(CookerArt(cooking: true, tint: .orange)), AnyView(CookerArt(cooking: false, tint: .orange))),
("kettle", AnyView(KettleArt(boiling: true, tint: .orange)), AnyView(KettleArt(boiling: false, tint: .orange))),
("dishwasher", AnyView(DishwasherArt(washing: true, tint: .orange)), AnyView(DishwasherArt(washing: false, tint: .orange))),
("purifier", AnyView(PurifierArt(on: true, filter: 72, tint: .blue)), AnyView(PurifierArt(on: false, filter: 72, tint: .blue))),
("heater", AnyView(HeaterArt(on: true, level: 0.6, tint: Theme.heat)), AnyView(HeaterArt(on: false, level: 0.6, tint: Theme.heat))),
("dehumidifier", AnyView(DehumidifierArt(on: true, level: 0.4, tint: .cyan)), AnyView(DehumidifierArt(on: false, level: 0.4, tint: .cyan))),
Expand Down Expand Up @@ -385,6 +392,9 @@ enum Snapshot {
m.page = .detail("d28"); render(root, f("kettle"))
m.states["d28"]?.values["2.1"] = .int(2); render(root, f("kettle-idle"))
m.states["d28"]?.values["2.1"] = .int(1)
m.page = .detail("d29"); render(root, f("dishwasher"))
m.states["d29"]?.values["2.1"] = .int(2); render(root, f("dishwasher-idle"))
m.states["d29"]?.values["2.1"] = .int(1)
m.page = .detail("d25"); render(root, f("purifier"))
m.page = .detail("d26"); render(root, f("motion"))
m.page = .detail("d27"); render(root, f("printer"))
Expand Down
7 changes: 5 additions & 2 deletions Sources/MijiaBarApp/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,16 @@ extension DeviceCategory {
}

/// 同一类别里外形不一样的设备:插画和列表图标按型号区分
enum DeviceForm { case doorbell, motion, contact, leak, kettle }
enum DeviceForm { case doorbell, motion, contact, leak, kettle, dishwasher }

extension Device {
var form: DeviceForm? {
let m = model.lowercased()
switch category {
case .camera: return m.contains(".cateye.") ? .doorbell : nil
case .kitchen: return m.contains("kettle") ? .kettle : nil
case .kitchen:
if m.contains("dishwasher") { return .dishwasher }
return m.contains("kettle") ? .kettle : nil
case .sensor:
if m.contains("magnet") { return .contact }
if m.contains("flood") || m.contains("wleak") { return .leak }
Expand All @@ -115,6 +117,7 @@ extension Device {
case .contact: return "door.left.hand.closed"
case .leak: return "drop.triangle"
case .kettle: return "mug"
case .dishwasher: return "dishwasher"
case nil: return category.icon
}
}
Expand Down
33 changes: 31 additions & 2 deletions Sources/MijiaBarApp/TypeDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ struct TypeDetailView: View {
case .speaker: SpeakerDetail(dc: dc)
case .camera: CameraDetail(dc: dc)
case .printer: PrinterDetail(dc: dc)
case .kitchen: if device.form == .kettle { KettleDetail(dc: dc) } else { CookerDetail(dc: dc) }
case .kitchen:
if device.form == .kettle { KettleDetail(dc: dc) }
else if device.form == .dishwasher { DishwasherDetail(dc: dc) }
else { CookerDetail(dc: dc) }
case .purifier: PurifierDetail(dc: dc)
case .heater: HeaterDetail(dc: dc)
case .dehumidifier: DehumidifierDetail(dc: dc)
Expand Down Expand Up @@ -84,7 +87,10 @@ struct DeviceStatusCard: View {
case .speaker: SpeakerArt(playing: false, volume: 0.5, tint: tint)
case .camera: if d.form == .doorbell { DoorbellArt(on: false, tint: tint) } else { CameraArt(on: false, tint: tint) }
case .printer: PrinterArt(busy: false, error: false, tint: tint)
case .kitchen: if d.form == .kettle { KettleArt(boiling: false, tint: tint) } else { CookerArt(cooking: false, tint: tint) }
case .kitchen:
if d.form == .kettle { KettleArt(boiling: false, tint: tint) }
else if d.form == .dishwasher { DishwasherArt(washing: false, tint: tint) }
else { CookerArt(cooking: false, tint: tint) }
case .purifier: PurifierArt(on: false, filter: nil, tint: tint)
case .heater: HeaterArt(on: false, level: 0.3, tint: tint)
case .dehumidifier: DehumidifierArt(on: false, level: 0.3, tint: tint)
Expand Down Expand Up @@ -341,6 +347,29 @@ struct CookerDetail: View {
}
}

struct DishwasherDetail: View {
let dc: DC
var body: some View {
let d = dc.device, tint = Color.orange
let status = dc.p("status")
let raw = dc.v(status).flatMap { value in status?.options?.first { Double($0.value) == value.doubleValue }?.description.lowercased() } ?? ""
let washing = ["washing", "cleaning", "drying", "working", "busy", "running", "default"].contains(raw)
let left = dc.num(dc.p("left-time"))
TypeShell(device: d, tint: tint, isOn: washing, title: washing ? L("清洗中", "Washing") : (dc.text(status) ?? (d.isOnline ? L("在线", "Online") : L("离线", "Offline"))),
subtitle: L("洗碗机", "Dishwasher")) {
DishwasherArt(washing: washing, tint: tint)
} content: {
if let left, left > 0 { BigValue(value: "\(Int(left))", unit: L("分钟", "min"), caption: L("剩余时间", "Remaining")) }
else { BigValue(value: dc.text(status), caption: L("当前状态", "Status")) }
ReadoutRow(items: [("thermometer.medium", dc.num(dc.p("temperature")).map { "\(Int($0))°C" }),
("target", dc.num(dc.p("target-temperature")).map { L("目标 ", "Target ") + "\(Int($0))°C" })], tint: tint)
HStack(spacing: 8) {
PropTile(dc: dc, prop: dc.p("mode"), title: L("模式", "Mode"), icon: "dishwasher", tint: tint)
}
}
}
}

struct KettleDetail: View {
let dc: DC
var body: some View {
Expand Down
2 changes: 2 additions & 0 deletions Sources/MijiaCore/Translations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -742,4 +742,6 @@ public let hantTable: [String: String] = [
"回充": "回充",
"电饭煲": "電子鍋",
"电热水壶": "電熱水壺",
"洗碗机": "洗碗機",
"清洗中": "清洗中",
]
39 changes: 37 additions & 2 deletions Tests/MijiaBarAppTests/TypeDetailTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import MijiaCore
for (category, row) in [(DeviceCategory.humidifier, "humidifier"), (.vacuum, "vacuum"),
(.fridge, "fridge"), (.washer, "washer"), (.waterHeater, "water-heater"), (.curtain, "curtain"), (.airPurifier, "air-purifier"),
(.heater, "heater"), (.dehumidifier, "dehumidifier"),
(.speaker, "speaker"), (.kitchen, "cooker"), (.kitchen, "kettle"), (.purifier, "purifier")] {
(.speaker, "speaker"), (.kitchen, "cooker"), (.kitchen, "kettle"), (.kitchen, "dishwasher"), (.purifier, "purifier")] {
let device = try #require(model.devices.first { $0.category == category })
#expect(device.homeId == (category == .dehumidifier ? "h1" : "h2"))
#expect(model.states[device.did]?.spec != nil)
#expect(ArtGallery.rows.filter { $0.0 == row }.count == 1)
}
#expect(ArtGallery.rows.count == 26)
#expect(ArtGallery.rows.count == 27)
#expect(ArtGallery.size.height == CGFloat(ArtGallery.rows.count * 138))
}

Expand Down Expand Up @@ -270,6 +270,41 @@ import MijiaCore
#expect(!contains(offArt(device, tint: device.category.color), type: CookerArt.self))
}

@Test func dishwasherDemoRoutingAndProps() throws {
let model = AppModel(mock: true)
let device = try #require(model.devices.first { $0.did == "d29" })
#expect(device.category == .kitchen)
#expect(device.form == .dishwasher)
#expect(device.icon == "dishwasher")
#expect(device.homeId == "h2")
#expect(device.roomName == L("厨房", "Kitchen"))
#expect(device.isOnline)
let state = try #require(model.states[device.did])
let context = DC(device: device, state: state)
let status = try #require(context.p("status"))
#expect(!status.writable)
#expect(status.options?.map(\.description) == ["Washing", "Idle"])
let washingOption = try #require(status.options?.first { $0.description == "Washing" })
#expect(context.num(status) == Double(washingOption.value))
let left = try #require(context.p("left-time"))
#expect(!left.writable)
#expect(left.unit == "minutes")
#expect(context.num(left) == 42)
let mode = try #require(context.p("mode"))
#expect(mode.writable)
#expect(mode.options?.map(\.description) == ["Auto", "Intense", "Eco"])
#expect(context.num(mode) == 0)
func contains<Value>(_ value: Any, type: Value.Type) -> Bool {
value is Value || Mirror(reflecting: value).children.contains { contains($0.value, type: type) }
}
#expect(contains(TypeDetailView(device: device, state: state).body, type: DishwasherDetail.self))
#expect(!contains(TypeDetailView(device: device, state: state).body, type: CookerDetail.self))
#expect(!contains(TypeDetailView(device: device, state: state).body, type: DeviceCardView.self))
#expect(contains(offArt(device, tint: device.category.color), type: DishwasherArt.self))
#expect(!contains(offArt(device, tint: device.category.color), type: CookerArt.self))
#expect(!contains(offArt(device, tint: device.category.color), type: WasherArt.self))
}

@Test func purifierDemoRoutingAndProps() throws {
let model = AppModel(mock: true)
let device = try #require(model.devices.first { $0.did == "d25" })
Expand Down
Loading
Loading