Changes
4 changed files (+352/-59)
-
-
@@ -96,6 +96,9 @@// XCFramework // https://mitchellh.com/writing/zig-and-swiftui if (target.result.os.tag.isDarwin()) { // FIXME: Debug build on macOS crashes on WebSocket client creation for some reason. const o = if (optimize == .Debug and target.result.os.tag.isDarwin()) .ReleaseSafe else optimize; const arm = b.addLibrary(.{ .name = "plac_apple_arm", .linkage = .static,
-
@@ -105,7 +108,7 @@ .target = b.resolveTargetQuery(.{.os_tag = .macos, .cpu_arch = .aarch64, }), .optimize = optimize, .optimize = o, }), });
-
@@ -122,7 +125,7 @@ .target = b.resolveTargetQuery(.{.os_tag = .macos, .cpu_arch = .x86_64, }), .optimize = optimize, .optimize = o, }), });
-
-
-
@@ -61,7 +61,7 @@ void plac_discovery_scan_result_release(plac_discovery_scan_result*);// discovery plac_discovery_scan_result *plac_discovery_scan(); plac_discovery_scan_result *plac_discovery_find(char *server_id); plac_discovery_scan_result *plac_discovery_find(const char *server_id); // transport.PlaybackState typedef enum {
-
@@ -140,7 +140,7 @@ plac_transport_zone_list_event *plac_connection_event_get_zone_list_event(plac_connection_event*);// connection.Connection typedef struct { void *__pri; } plac_connection; plac_connection *plac_connection_make(plac_discovery_server*, char *token); plac_connection *plac_connection_make(plac_discovery_server*, const char *token); plac_connection *plac_connection_retain(plac_connection*); void plac_connection_release(plac_connection*); plac_connection_event *plac_connection_get_event(plac_connection*);
-
-
-
@@ -17,89 +17,117 @@import PlacKit import os class Zone { protocol Zone { var id: String { get } var name: String { get } var playback: PlacKit.plac_transport_playback_state { get } } class CoreZone: Identifiable, Zone { var ptr: UnsafeMutablePointer<PlacKit.plac_transport_zone> var id: String { return String(cString: ptr.pointee.id) } var name: String { return String(cString: ptr.pointee.name) } var playback: PlacKit.plac_transport_playback_state { return ptr.pointee.playback } init(ptr: UnsafeMutablePointer<PlacKit.plac_transport_zone>) { self.ptr = ptr } deinit { PlacKit.plac_transport_zone_release(ptr) } } class MockZone: Identifiable, Zone { var id: String var name: String var playback: plac_transport_playback_state init(id: String, name: String, playback: plac_transport_playback_state) { self.id = id self.name = name self.playback = playback } } class ZoneListEvent { let logger = Logger() var ptr: UnsafeMutablePointer<PlacKit.plac_transport_zone_list_event> var addedZones: [Zone] var changedZones: [Zone] var addedZones: [CoreZone] var changedZones: [CoreZone] var removedZoneIds: [String] init(ptr: UnsafeMutablePointer<PlacKit.plac_transport_zone_list_event>) { self.ptr = ptr var addedZones: [Zone] = [] if var current = ptr.pointee.added_zones_ptr.pointee { for _ in 0..<ptr.pointee.added_zones_len { addedZones.append(Zone(ptr: PlacKit.plac_transport_zone_retain(current))) current = current.successor() self.addedZones = [] if ptr.pointee.added_zones_len > 0 { var addedZones: [CoreZone] = [] if var current = ptr.pointee.added_zones_ptr.pointee { for _ in 0..<ptr.pointee.added_zones_len { addedZones.append( CoreZone(ptr: PlacKit.plac_transport_zone_retain(current)) ) current = current.successor() } self.addedZones = addedZones } else { logger.warning("added_zones_ptr is null") } self.addedZones = addedZones } else { logger.warning("added_zones_ptr is null") self.addedZones = [] } var changedZones: [Zone] = [] if var current = ptr.pointee.changed_zones_ptr.pointee { for _ in 0..<ptr.pointee.changed_zones_len { changedZones.append(Zone(ptr: PlacKit.plac_transport_zone_retain(current))) current = current.successor() self.changedZones = [] if ptr.pointee.changed_zones_len > 0 { var changedZones: [CoreZone] = [] if var current = ptr.pointee.changed_zones_ptr.pointee { for _ in 0..<ptr.pointee.changed_zones_len { changedZones.append( CoreZone(ptr: PlacKit.plac_transport_zone_retain(current)) ) current = current.successor() } self.changedZones = changedZones } else { logger.warning("changed_zones_ptr is null") } self.changedZones = changedZones } else { logger.warning("changed_zones_ptr is null") self.changedZones = [] } var removedZoneIds: [String] = [] if var current = ptr.pointee.removed_zone_ids_ptr { for i in 0..<ptr.pointee.removed_zone_ids_len { if let strPtr = current.pointee { removedZoneIds.append(String(cString: strPtr)) } else { logger.warning("removed_zone_ids_ptr[\(i)] is null pointer") self.removedZoneIds = [] if ptr.pointee.removed_zone_ids_len > 0 { var removedZoneIds: [String] = [] if var current = ptr.pointee.removed_zone_ids_ptr { for i in 0..<ptr.pointee.removed_zone_ids_len { if let strPtr = current.pointee { removedZoneIds.append(String(cString: strPtr)) } else { logger.warning("removed_zone_ids_ptr[\(i)] is null pointer") } current = current.successor() } current = current.successor() self.removedZoneIds = removedZoneIds } else { logger.warning("removed_zone_ids_ptr is null") } self.removedZoneIds = removedZoneIds } else { logger.warning("removed_zone_ids_ptr is null") self.removedZoneIds = [] } } deinit { PlacKit.plac_transport_zone_list_event_release(ptr) }
-
-
-
@@ -14,18 +14,280 @@ // limitations under the License.// // SPDX-License-Identifier: Apache-2.0 import PlacKit import SwiftUI import os enum MainViewState { case loading case found(CoreServer) case findError(PlacKit.plac_discovery_scan_result_code) case null_pointer case not_found } struct MainView: View { var serverId: CoreServer.ID? @State private var state: MainViewState = .loading private let logger = Logger() private let queue = DispatchQueue(label: "plac.server-find") var body: some View { VStack { if let id = serverId { Text(id) } else { Text("nil") switch state { case .loading: ProgressView() case .found(let server): ConnectedView(server: server) case .findError(_): ContentUnavailableView { Label( "Server not found", systemImage: "exclamationmark.magnifyingglass" ) } description: { // TODO: Tidy Text("Server not found") } case .not_found: ContentUnavailableView { Label( "Server not found", systemImage: "exclamationmark.magnifyingglass" ) } description: { Text("Selected server does not found on local network.") } case .null_pointer: ContentUnavailableView { Label( "Unable to connect", systemImage: "exclamationmark.magnifyingglass" ) } description: { Text( "Failed to retrieve server connection information. Out of memory." ) } } } .onAppear { if let serverId = serverId { queue.async { let ptr = PlacKit.plac_discovery_find(serverId) guard let ptr = ptr else { logger.error("Failed to scan servers: Out of memory") DispatchQueue.main.async { state = .null_pointer } return } let result = ScanResult(ptr: ptr) if result.code != PlacKit.PLAC_DISCOVERY_SCAN_RESULT_OK { DispatchQueue.main.async { state = .findError(result.code) } return } if result.entries.count < 1 { DispatchQueue.main.async { state = .not_found } return } DispatchQueue.main.async { state = .found(result.entries[0]) } } } } } } struct ConnectedView: View { private var conn: Connection @State private var zone_id: String? = nil @State private var zones: [String: CoreZone] = [:] private let queue = DispatchQueue(label: "plac.event-loop") var body: some View { PureConnectedView(zone_id: $zone_id, zones: zones) .onAppear { queue.async { let logger = Logger() while true { let event = conn.getEvent() guard let event = event else { logger.debug("Got null event, quitting") return } switch event { case .connected(_): conn.subscribeZoneChanges() break case .connectionError(let ev): logger.error("Failed to connect: \(ev.code.rawValue)") break case .zoneList(let ev): DispatchQueue.main.async { for zone in ev.addedZones { zones[zone.id] = zone } for zone in ev.changedZones { zones[zone.id] = zone } for id in ev.removedZoneIds { zones.removeValue(forKey: id) if zone_id == id { zone_id = nil } } if zone_id == nil { zone_id = zones.first?.value.id } } break } } } } } init(server: CoreServer) { conn = Connection(server: server) } } struct PureConnectedView<TZone: Zone & Identifiable>: View { @Binding var zone_id: String? var zones: [String: TZone] = [:] var body: some View { VStack(spacing: 0) { NavigationSplitView { List { NavigationLink { Text("LIBRARY") } label: { Text("Library") } NavigationLink { Text("DISCOVER") } label: { Text("Discover") } } } detail: { } Divider() PlaybackBar<TZone>(zone_id: $zone_id, zones: zones) .frame(maxWidth: .infinity) } } } #Preview("Layout") { @Previewable @State var zone_id: String? = "foo" var zones: [String: MockZone] = [ "foo": MockZone( id: "foo", name: "Foo", playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_PLAYING ), "bar": MockZone( id: "bar", name: "Bar", playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_STOPPED ), ] PureConnectedView<MockZone>(zone_id: $zone_id, zones: zones) } struct PlaybackBar<TZone: Zone & Identifiable>: View { @Binding var zone_id: String? var zones: [String: TZone] = [:] private var zone: TZone? { zones.first(where: { _, zone in zone.id == zone_id })?.value } var body: some View { HStack { HStack { if zone?.playback == PlacKit.PLAC_TRANSPORT_PLAYBACK_PLAYING { Button { } label: { Label("Pause", systemImage: "pause.fill") } } else { Button { } label: { Label("Play", systemImage: "play.fill") } } } .labelStyle(.iconOnly) .buttonStyle(.borderless) .font(.system(size: 20)) Spacer() if zones.count > 0 && zone_id != nil { Picker("Zone", selection: $zone_id) { ForEach( zones.values.sorted(by: { $0.id < $1.id }) ) { zone in Text(zone.name).tag(zone.id as String?) } } .scaledToFit() } } .padding([.horizontal], 10) .padding([.vertical], 8) } } #Preview("PlaybackBar") { @Previewable @State var zone_id: String? = "foo" var zones: [String: MockZone] = [ "foo": MockZone( id: "foo", name: "Foo", playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_PLAYING ), "bar": MockZone( id: "bar", name: "Bar", playback: PlacKit.PLAC_TRANSPORT_PLAYBACK_STOPPED ), ] PlaybackBar(zone_id: $zone_id, zones: zones) }
-