Implements UI for adding servers in settings, moves to tab model on Phone
This commit is contained in:
@@ -264,6 +264,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = QueueCube/App/Entitlements.plist;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@@ -299,6 +300,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = QueueCube/App/Entitlements.plist;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
||||
8
QueueCube/App/Entitlements.plist
Normal file
8
QueueCube/App/Entitlements.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.networking.multicast</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
55
QueueCube/Backend/Server.swift
Normal file
55
QueueCube/Backend/Server.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// Server.swift
|
||||
// QueueCube
|
||||
//
|
||||
// Created by James Magahern on 6/10/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Server: Identifiable
|
||||
{
|
||||
let serviceName: String?
|
||||
let baseURL: URL
|
||||
|
||||
var id: String { baseURL.absoluteString }
|
||||
|
||||
var displayName: String {
|
||||
if let serviceName {
|
||||
return serviceName.queueCubeServiceName
|
||||
}
|
||||
|
||||
let components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
return components.host ?? baseURL.absoluteString
|
||||
}
|
||||
|
||||
init?(serviceName: String?, host: String, port: UInt16) {
|
||||
self.serviceName = serviceName
|
||||
|
||||
// Assumes this is the local service discovery path, which is http
|
||||
// Bounjour gives us the interface sometimes, which we can handle, but need to percent encode.
|
||||
let host = host.replacingOccurrences(of: "%", with: "%25")
|
||||
guard let url = URL(string: "http://\(host):\(port)/api") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.baseURL = url
|
||||
}
|
||||
|
||||
init(baseURL: URL) {
|
||||
self.serviceName = nil
|
||||
self.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
extension String
|
||||
{
|
||||
var queueCubeServiceName: String {
|
||||
let regex = /.* \((.*)\)/
|
||||
if let match = try? regex.firstMatch(in: self) {
|
||||
return String(match.output.1)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ struct Settings
|
||||
{
|
||||
case serverURL
|
||||
}
|
||||
|
||||
struct Server: Codable
|
||||
{
|
||||
let address: String
|
||||
let port: UInt32
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"%@" : {
|
||||
|
||||
},
|
||||
"ADD" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -21,6 +24,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ADD_SERVER" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Add Server"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CANCEL" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CONFIGURATION" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -41,6 +64,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DISCOVERED" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Discovered"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DONE" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -51,6 +84,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ENTER_MANUALLY" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enter Manually"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"FAVORITES" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -61,8 +104,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"General" : {
|
||||
|
||||
"FINDING_SERVERS" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Finding Servers…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GENERAL" : {
|
||||
"extractionState" : "manual",
|
||||
@@ -116,6 +166,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SERVERS" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Servers"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"SETTINGS" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -24,4 +24,10 @@ extension LocalizedStringKey
|
||||
static let connectionError = LocalizedStringKey("CONNECTION_ERROR")
|
||||
static let playlist = LocalizedStringKey("PLAYLIST")
|
||||
static let favorites = LocalizedStringKey("FAVORITES")
|
||||
static let servers = LocalizedStringKey("SERVERS")
|
||||
static let addServer = LocalizedStringKey("ADD_SERVER")
|
||||
static let cancel = LocalizedStringKey("CANCEL")
|
||||
static let manual = LocalizedStringKey("ENTER_MANUALLY")
|
||||
static let discovered = LocalizedStringKey("DISCOVERED")
|
||||
static let findingServers = LocalizedStringKey("FINDING_SERVERS")
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ struct ContentView: View
|
||||
try await api.add(mediaURL: strippedURL)
|
||||
case .favorites:
|
||||
try await api.addFavorite(mediaURL: strippedURL)
|
||||
case .settings:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,6 +193,7 @@ class MainViewModel
|
||||
enum MainTab: String, CaseIterable {
|
||||
case playlist
|
||||
case favorites
|
||||
case settings
|
||||
}
|
||||
|
||||
struct MainView: View
|
||||
@@ -214,44 +217,45 @@ struct MainView: View
|
||||
var body: some View {
|
||||
let showConfigurationDialog = model.api == nil
|
||||
|
||||
VStack {
|
||||
VStack {
|
||||
NowPlayingView(model: model.nowPlayingViewModel)
|
||||
.padding()
|
||||
.disabled(showConfigurationDialog || model.connectionError != nil)
|
||||
|
||||
if showConfigurationDialog {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "server.rack")
|
||||
Text(.notConfigured)
|
||||
} actions: {
|
||||
Button {
|
||||
isSettingsVisible = true
|
||||
} label: {
|
||||
Text(.settings)
|
||||
}
|
||||
}
|
||||
} else if model.connectionError != nil {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
Text(.connectionError)
|
||||
}
|
||||
} else {
|
||||
TabView(selection: $model.selectedTab) {
|
||||
Tab(.playlist, systemImage: "list.bullet", value: .playlist) {
|
||||
PlaylistView(model: model.playlistModel)
|
||||
}
|
||||
|
||||
Tab(.favorites, systemImage: "heart.fill", value: .favorites) {
|
||||
FavoritesView(model: model.favoritesModel)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 640.0)
|
||||
}
|
||||
TabView(selection: $model.selectedTab) {
|
||||
Tab(.playlist, systemImage: "list.bullet", value: .playlist) {
|
||||
PlaylistView(model: model.playlistModel)
|
||||
}
|
||||
.frame(minHeight: 0.0)
|
||||
.layoutPriority(1.0)
|
||||
|
||||
Tab(.favorites, systemImage: "heart.fill", value: .favorites) {
|
||||
FavoritesView(model: model.favoritesModel)
|
||||
}
|
||||
|
||||
Tab(.settings, systemImage: "gear", value: .settings) {
|
||||
SettingsView(onDone: {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if false
|
||||
VStack {
|
||||
if showConfigurationDialog {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "server.rack")
|
||||
Text(.notConfigured)
|
||||
} actions: {
|
||||
Button {
|
||||
isSettingsVisible = true
|
||||
} label: {
|
||||
Text(.settings)
|
||||
}
|
||||
}
|
||||
} else if model.connectionError != nil {
|
||||
ContentPlaceholderView {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
Text(.connectionError)
|
||||
}
|
||||
} else {
|
||||
TabView(selection: $model.selectedTab) {
|
||||
}
|
||||
.frame(maxWidth: 640.0)
|
||||
}
|
||||
|
||||
AddMediaBarView(model: model.addMediaViewModel)
|
||||
.layoutPriority(2.0)
|
||||
.disabled(showConfigurationDialog)
|
||||
@@ -260,6 +264,8 @@ struct MainView: View
|
||||
SettingsView(onDone: { isSettingsVisible = false })
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,133 +5,360 @@
|
||||
// Created by James Magahern on 5/2/25.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Network
|
||||
import SwiftUI
|
||||
|
||||
@Observable
|
||||
class SettingsViewModel
|
||||
{
|
||||
var serverURL: String = ""
|
||||
var validationState: ValidationState = .empty
|
||||
|
||||
private var validationTimer: Timer? = nil
|
||||
|
||||
init() {
|
||||
validateSettings()
|
||||
observeForValidation()
|
||||
}
|
||||
|
||||
static func fromDefaults() -> SettingsViewModel {
|
||||
let settings = Settings.fromDefaults()
|
||||
let model = SettingsViewModel()
|
||||
model.serverURL = settings.serverURL ?? ""
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
private func observeForValidation() {
|
||||
withObservationTracking {
|
||||
_ = serverURL
|
||||
} onChange: {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
setNeedsValidation()
|
||||
saveSettings()
|
||||
|
||||
observeForValidation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNeedsValidation() {
|
||||
self.validationTimer?.invalidate()
|
||||
self.validationTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
self?.validateSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private func validateSettings() {
|
||||
guard !serverURL.isEmpty else {
|
||||
validationState = .empty
|
||||
return
|
||||
}
|
||||
|
||||
self.validationState = .validating
|
||||
|
||||
Task {
|
||||
do {
|
||||
let url = try URL(string: serverURL).try_unwrap()
|
||||
let api = API(baseURL: url)
|
||||
_ = try await api.fetchNowPlayingInfo()
|
||||
|
||||
self.validationState = .valid
|
||||
} catch {
|
||||
print("Validation failed: \(error)")
|
||||
self.validationState = .notValid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSettings() {
|
||||
Settings(serverURL: self.serverURL)
|
||||
.save()
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum ValidationState
|
||||
{
|
||||
case empty
|
||||
case validating
|
||||
case notValid
|
||||
case valid
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView: View
|
||||
{
|
||||
let onDone: () -> Void
|
||||
@State var model = SettingsViewModel.fromDefaults()
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
Tab("General", systemImage: "gear") {
|
||||
generalTab()
|
||||
NavigationStack {
|
||||
List {
|
||||
NavigationLink(destination: GeneralSettingsView()) {
|
||||
Image(systemName: "gear")
|
||||
Text(.general)
|
||||
}
|
||||
|
||||
NavigationLink(destination: ServerListSettingsView()) {
|
||||
Image(systemName: "server.rack")
|
||||
Text(.servers)
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(.settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GeneralSettingsView: View
|
||||
{
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerListSettingsView: View
|
||||
{
|
||||
@State var model = ViewModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
List(model.configuredServers) { server in
|
||||
serverListItem(server)
|
||||
}
|
||||
}
|
||||
|
||||
.navigationTitle(.servers)
|
||||
|
||||
.toolbar {
|
||||
Button {
|
||||
model.isAddServerPresented = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.sheet(isPresented: $model.isAddServerPresented) {
|
||||
NavigationView {
|
||||
AddServerView(onAddServer: { model.onAddServer(server: $0) })
|
||||
.navigationTitle(.addServer)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .cancellationAction) {
|
||||
Button(.cancel) { model.isAddServerPresented = false }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func generalTab() -> some View {
|
||||
func serverListItem(_ server: Server) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "hifispeaker.fill")
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(server.displayName)
|
||||
.lineLimit(1)
|
||||
.bold()
|
||||
|
||||
Text(server.baseURL.absoluteString)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
@Observable
|
||||
class ViewModel
|
||||
{
|
||||
var configuredServers: [Server]
|
||||
var isAddServerPresented = false
|
||||
|
||||
init() {
|
||||
self.configuredServers = []
|
||||
}
|
||||
|
||||
func onAddServer(server: Server) {
|
||||
isAddServerPresented = false
|
||||
configuredServers.append(server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddServerView: View
|
||||
{
|
||||
let onAddServer: (Server) -> Void
|
||||
@State var model = ViewModel()
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(.configuration) {
|
||||
// Manual Entry
|
||||
Section(.manual) {
|
||||
TextField(.serverURL, text: $model.serverURL)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.URL)
|
||||
|
||||
switch model.validationState {
|
||||
case .empty:
|
||||
EmptyView()
|
||||
case .validating:
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text(.validating)
|
||||
}
|
||||
case .notValid:
|
||||
HStack {
|
||||
Image(systemName: "x.circle.fill")
|
||||
Text(.unableToConnect)
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
case .valid:
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
Text(.serverIsOnline)
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
|
||||
Button {
|
||||
// Force unwrap, since we validated it at this point.
|
||||
let server = Server(baseURL: URL(string: model.serverURL)!)
|
||||
onAddServer(server)
|
||||
} label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(.addServer)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch model.validationState {
|
||||
case .empty:
|
||||
EmptyView()
|
||||
case .validating:
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text(.validating)
|
||||
// Discovered
|
||||
Section(.discovered) {
|
||||
if model.discoveredServers.isEmpty {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
Text(.findingServers)
|
||||
}
|
||||
} else {
|
||||
List(model.discoveredServers) { (server: DiscoveredEndpoint) in
|
||||
Button {
|
||||
resolveEndpoint(server)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "network")
|
||||
Text("\(server.displayName)")
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
}
|
||||
}
|
||||
case .notValid:
|
||||
HStack {
|
||||
Image(systemName: "x.circle.fill")
|
||||
Text(.unableToConnect)
|
||||
}
|
||||
.foregroundStyle(.red)
|
||||
case .valid:
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
Text(.serverIsOnline)
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
}
|
||||
|
||||
.task {
|
||||
model.startDiscovery()
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveEndpoint(_ endpoint: DiscoveredEndpoint) {
|
||||
Task {
|
||||
let server = try await endpoint.resolve()
|
||||
onAddServer(server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
@Observable
|
||||
class ViewModel
|
||||
{
|
||||
var serverURL: String = ""
|
||||
var validationState: ValidationState = .empty
|
||||
|
||||
var discoveredServers: [DiscoveredEndpoint] = []
|
||||
private let browser = NWBrowser(for: .bonjour(type: "_queuecube._tcp.", domain: nil), using: .tcp)
|
||||
|
||||
private var validationTimer: Timer? = nil
|
||||
|
||||
init() {
|
||||
observeForValidation()
|
||||
}
|
||||
|
||||
public func startDiscovery() {
|
||||
browser.browseResultsChangedHandler = { [weak self] results, changes in
|
||||
guard let self else { return }
|
||||
self.discoveredServers = results.map { DiscoveredEndpoint(result: $0) }
|
||||
}
|
||||
|
||||
browser.stateUpdateHandler = { state in
|
||||
if case .failed(let error) = state {
|
||||
print("Discovery error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
browser.start(queue: .global(qos: .userInitiated))
|
||||
}
|
||||
|
||||
private func observeForValidation() {
|
||||
withObservationTracking {
|
||||
_ = serverURL
|
||||
} onChange: {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
setNeedsValidation()
|
||||
saveSettings()
|
||||
|
||||
observeForValidation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNeedsValidation() {
|
||||
self.validationTimer?.invalidate()
|
||||
self.validationTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
self?.validateSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private func validateSettings() {
|
||||
guard !serverURL.isEmpty else {
|
||||
validationState = .empty
|
||||
return
|
||||
}
|
||||
|
||||
self.validationState = .validating
|
||||
|
||||
Task {
|
||||
do {
|
||||
let url = try URL(string: serverURL).try_unwrap()
|
||||
let api = API(baseURL: url)
|
||||
_ = try await api.fetchNowPlayingInfo()
|
||||
|
||||
self.validationState = .valid
|
||||
} catch {
|
||||
print("Validation failed: \(error)")
|
||||
self.validationState = .notValid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSettings() {
|
||||
Settings(serverURL: self.serverURL)
|
||||
.save()
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum ValidationState
|
||||
{
|
||||
case empty
|
||||
case validating
|
||||
case notValid
|
||||
case valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DiscoveredEndpoint: Identifiable
|
||||
{
|
||||
let endpoint: NWEndpoint
|
||||
let serviceName: String
|
||||
|
||||
var displayName: String {
|
||||
serviceName.queueCubeServiceName
|
||||
}
|
||||
|
||||
var id: String { serviceName }
|
||||
|
||||
init(result: NWBrowser.Result) {
|
||||
self.endpoint = result.endpoint
|
||||
|
||||
switch result.endpoint {
|
||||
case .service(name: let name, type: _, domain: _, interface: _):
|
||||
self.serviceName = name
|
||||
default:
|
||||
self.serviceName = "(Unknown)"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func resolve() async throws -> Server {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
let connection = NWConnection(to: endpoint, using: .tcp)
|
||||
connection.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .preparing: break
|
||||
case .ready:
|
||||
// xxx: is this really the right way to do this? Maybe we should not try to turn this into a URL.
|
||||
if case .hostPort(host: let host, port: let port) = connection.currentPath?.remoteEndpoint {
|
||||
let address = switch host {
|
||||
case .name(let string, _): string
|
||||
case .ipv4(let iPv4Address): iPv4Address.debugDescription
|
||||
case .ipv6(let iPv6Address): iPv6Address.debugDescription
|
||||
default: "unknown"
|
||||
}
|
||||
|
||||
if let server = Server(serviceName: serviceName, host: address, port: port.rawValue) {
|
||||
continuation.resume(returning: server)
|
||||
} else {
|
||||
continuation.resume(throwing: Self.Error.urlError)
|
||||
}
|
||||
} else {
|
||||
continuation.resume(throwing: Self.Error.endpointIncorrect)
|
||||
}
|
||||
case .cancelled:
|
||||
continuation.resume(throwing: Self.Error.cancelledConnection)
|
||||
case .failed(let error):
|
||||
continuation.resume(throwing: error)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .global(qos: .userInitiated))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
enum Error: Swift.Error
|
||||
{
|
||||
case cancelledConnection
|
||||
case endpointIncorrect
|
||||
case urlError
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user