Changes
2 changed files (+193/-28)
-
-
@@ -21,35 +21,32 @@ @Observablefinal class ZoneDataModel { let conn: Communicatable private var zoneID: TransportService.Zone.ID? = nil private var _zones: [TransportService.Zone.ID: TransportService.Zone] = [:] public var zone: TransportService.Zone? = nil private var _zones: [TransportService.Zone] = [] private(set) var zones: [TransportService.Zone.ID: TransportService.Zone] { private(set) var zones: [TransportService.Zone] { get { _zones } set { _zones = newValue if let zoneID = zoneID, _zones[zoneID] != nil { if let zoneID = zone?.id { for zone in _zones { if zone.id == zoneID { return } } } for zone in _zones { self.zone = zone return } zoneID = _zones.first?.key self.zone = nil } } var zone: TransportService.Zone? { get { if let zoneID = zoneID { zones[zoneID] } else { nil } } set { zoneID = newValue?.id } } init(conn: Communicatable) { self.conn = conn }
-
@@ -60,24 +57,36 @@ Moo(subscribeZoneChange: .init(subscriptionID: UUID().uuidString))) let body = try TransportService.SubscribeZoneChangesResponse(msg) zones = body.zones.reduce(into: [:]) { dict, zone in dict[zone.id] = zone } zones = body.zones for try await message in await conn.messages.compactMap({ TransportService.ZoneChangeEvent($0) }) { for added in message.addedZones { zones[added.id] = added putZone(zone: added) } for changed in message.changedZones { zones[changed.id] = changed putZone(zone: changed) } for removedID in message.removedZoneIDs { zones[removedID] = nil removeZone(zoneID: removedID) } } } private func putZone(zone: TransportService.Zone) { if let index = zones.firstIndex(where: { z in z.id == zone.id }) { zones[index] = zone } else { zones.append(zone) } } private func removeZone(zoneID: TransportService.Zone.ID) { if let index = zones.firstIndex(where: { z in z.id == zoneID }) { zones.remove(at: index) } } }
-
-
-
@@ -106,11 +106,7 @@if model.zones.count > 0 && zone != nil { @Bindable var model = model Picker("Zone", selection: $model.zone) { ForEach( model.zones.values.sorted(by: { $0.id < $1.id }) ) { (zone: TransportService.Zone) in ForEach(model.zones) { (zone: TransportService.Zone) in Text(zone.displayName).tag(zone) } }
-
@@ -143,3 +139,163 @@isPerformingAction = false } } // MARK: - Previews private actor MockServer: Communicatable { var messages: AsyncStream<Moo> { AsyncStream { stream in Task { do { var i: Int = 0 while true { try Task.checkCancellation() try await Task.sleep(for: .seconds(1)) i += 1 if i > 60 { i = 0 } let body = """ { "zones_seek_changed": [ { "zone_id": "foo", "seek_position": \(String(i, radix: 10)) } ] } """ stream.yield( Moo( verb: "CONTINUE", service: "idk", headers: [ "Content-Type": "application/json", "Content-Length": String(body.utf8.count, radix: 10), ], body: body.data(using: .utf8)! ) ) } } catch { stream.finish() } } } } func request(_ msg: consuming Moo) async throws -> Moo { try await request(msg: msg, timeout: nil) } func request(_ msg: consuming Moo, timeout: ContinuousClock.Instant.Duration) async throws -> Moo { try await request(msg: msg, timeout: timeout) } fileprivate enum MockRequestError: Error { case unsupportedMessage } private func request( msg: Moo, timeout: ContinuousClock.Instant.Duration? = nil ) async throws -> Moo { switch msg.service { case "\(TransportService.id)/subscribe_zones": let body = """ { "zones": [ { "zone_id": "0-foo", "display_name": "Foo", "outputs": [ { "output_id": "foo-out1", "display_name": "Foo Output #1", "volume": { "type": "number", "min": 0, "max": 100, "step": 1, "value": 50, "is_muted": false } }, { "output_id": "foo-out2", "display_name": "Foo Output #2", "volume": { "type": "incremental" } } ], "now_playing": { "seek_position": 89, "length": 120, "one_line": { "line1": "A song" }, "two_line": { "line1": "A song", "line2": "That's it" }, "three_line": { "line1": "A song", "line2": "Really, that's it" } }, "state": "playing", "is_previous_allowed": true, "is_next_allowed": false, "is_pause_allowed": true, "is_play_allowed": true, "is_seek_allowed": true }, { "zone_id": "1-bar", "display_name": "Bar", "outputs": [], "state": "stopped" } ] } """ return Moo( verb: "COMPLETE", service: "Subscribed", headers: [ "Content-Type": "application/json", "Content-Length": String(body.utf8.count, radix: 10), ], body: body.data(using: .utf8)! ) default: throw MockRequestError.unsupportedMessage } } func send(_ msg: consuming Moo) async throws { // No-op } } #Preview { let model = ZoneDataModel(conn: MockServer()) PlaybackBar() .environment(model) .task { do { try await model.watchChanges() } catch { print(error) } } }
-