connect: show model of Mac
This commit is contained in:
parent
745cd38827
commit
fd4f17312c
|
@ -20,9 +20,10 @@ import UniformTypeIdentifiers
|
|||
import IQKeyboardManagerSwift
|
||||
#endif
|
||||
|
||||
#if WITH_QEMU_TCI
|
||||
// on visionOS, there is no text to show more than UTM
|
||||
#if WITH_QEMU_TCI && !os(visionOS)
|
||||
let productName = "UTM SE"
|
||||
#elseif WITH_REMOTE
|
||||
#elseif WITH_REMOTE && !os(visionOS)
|
||||
let productName = "UTM Remote"
|
||||
#else
|
||||
let productName = "UTM"
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// Copyright © 2024 osy. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct MacDeviceLabel<Title>: View where Title : StringProtocol {
|
||||
let title: Title
|
||||
let device: MacDevice
|
||||
|
||||
init(_ title: Title, device macDevice: MacDevice) {
|
||||
self.title = title
|
||||
self.device = macDevice
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Label(title, systemImage: device.symbolName)
|
||||
}
|
||||
}
|
||||
|
||||
// credits: https://adamdemasi.com/2023/04/15/mac-device-icon-by-device-class.html
|
||||
|
||||
private extension UTTagClass {
|
||||
static let deviceModelCode = UTTagClass(rawValue: "com.apple.device-model-code")
|
||||
}
|
||||
|
||||
private extension UTType {
|
||||
static let macBook = UTType("com.apple.mac.laptop")
|
||||
static let macBookWithNotch = UTType("com.apple.mac.notched-laptop")
|
||||
static let macMini = UTType("com.apple.macmini")
|
||||
static let macStudio = UTType("com.apple.macstudio")
|
||||
static let iMac = UTType("com.apple.imac")
|
||||
static let macPro = UTType("com.apple.macpro")
|
||||
static let macPro2013 = UTType("com.apple.macpro-cylinder")
|
||||
static let macPro2019 = UTType("com.apple.macpro-2019")
|
||||
}
|
||||
|
||||
struct MacDevice {
|
||||
let model: String
|
||||
let symbolName: String
|
||||
|
||||
#if os(macOS)
|
||||
static let current: Self = {
|
||||
let key = "hw.model"
|
||||
var size = size_t()
|
||||
sysctlbyname(key, nil, &size, nil, 0)
|
||||
let value = malloc(size)
|
||||
defer {
|
||||
value?.deallocate()
|
||||
}
|
||||
sysctlbyname(key, value, &size, nil, 0)
|
||||
guard let cChar = value?.bindMemory(to: CChar.self, capacity: size) else {
|
||||
return Self(model: "Unknown")
|
||||
}
|
||||
return Self(model: String(cString: cChar))
|
||||
}()
|
||||
#endif
|
||||
|
||||
init(model: String?) {
|
||||
self.model = model ?? "Unknown"
|
||||
self.symbolName = Self.symbolName(from: self.model)
|
||||
}
|
||||
|
||||
private static func checkModel(_ model: String, conformsTo type: UTType?) -> Bool {
|
||||
guard let type else {
|
||||
return false
|
||||
}
|
||||
return UTType(tag: model, tagClass: .deviceModelCode, conformingTo: nil)?.conforms(to: type) ?? false
|
||||
}
|
||||
|
||||
private static func symbolName(from model: String) -> String {
|
||||
if checkModel(model, conformsTo: .macBookWithNotch),
|
||||
#available(macOS 14, iOS 17, macCatalyst 17, tvOS 17, watchOS 10, *) {
|
||||
// macbook.gen2 was added with SF Symbols 5.0 (macOS Sonoma, 2023), but MacBooks with a notch
|
||||
// were released in 2021!
|
||||
return "macbook.gen2"
|
||||
} else if checkModel(model, conformsTo: .macBook) {
|
||||
return "laptopcomputer"
|
||||
} else if checkModel(model, conformsTo: .macMini) {
|
||||
return "macmini"
|
||||
} else if checkModel(model, conformsTo: .macStudio) {
|
||||
return "macstudio"
|
||||
} else if checkModel(model, conformsTo: .iMac) {
|
||||
return "desktopcomputer"
|
||||
} else if checkModel(model, conformsTo: .macPro2019) {
|
||||
return "macpro.gen3"
|
||||
} else if checkModel(model, conformsTo: .macPro2013) {
|
||||
return "macpro.gen2"
|
||||
} else if checkModel(model, conformsTo: .macPro) {
|
||||
return "macpro"
|
||||
}
|
||||
return "display"
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MacDeviceLabel("MacBook", device: MacDevice(model: "Mac14,6"))
|
||||
}
|
|
@ -23,10 +23,6 @@ struct UTMRemoteConnectView: View {
|
|||
@State private var selectedServer: UTMRemoteClient.State.Server?
|
||||
@State private var isAutoConnect: Bool = false
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom {
|
||||
UIDevice.current.userInterfaceIdiom
|
||||
}
|
||||
|
||||
private var remoteClient: UTMRemoteClient {
|
||||
data.remoteClient
|
||||
}
|
||||
|
@ -36,6 +32,9 @@ struct UTMRemoteConnectView: View {
|
|||
HStack {
|
||||
ProgressView().progressViewStyle(.circular)
|
||||
Spacer()
|
||||
Text("Select a UTM Server")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
openURL(URL(string: "https://docs.getutm.app/remote/")!)
|
||||
} label: {
|
||||
|
@ -52,41 +51,43 @@ struct UTMRemoteConnectView: View {
|
|||
}
|
||||
}.padding()
|
||||
List {
|
||||
Section(header: Text("Saved")) {
|
||||
ForEach(remoteClientState.savedServers) { server in
|
||||
Button {
|
||||
isAutoConnect = true
|
||||
selectedServer = server
|
||||
} label: {
|
||||
Text(server.name)
|
||||
}.contextMenu {
|
||||
if remoteClientState.savedServers.count > 0 {
|
||||
Section(header: Text("Saved")) {
|
||||
ForEach(remoteClientState.savedServers) { server in
|
||||
Button {
|
||||
isAutoConnect = false
|
||||
isAutoConnect = true
|
||||
selectedServer = server
|
||||
} label: {
|
||||
Label("Edit…", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
DestructiveButton("Delete") {
|
||||
MacDeviceLabel(server.name, device: .init(model: server.model))
|
||||
}.foregroundColor(.primary)
|
||||
.contextMenu {
|
||||
Button {
|
||||
isAutoConnect = false
|
||||
selectedServer = server
|
||||
} label: {
|
||||
Label("Edit…", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
DestructiveButton("Delete") {
|
||||
|
||||
}
|
||||
}
|
||||
}.onDelete { indexSet in
|
||||
|
||||
}
|
||||
}.onDelete { indexSet in
|
||||
|
||||
}
|
||||
}
|
||||
Section(header: Text("Found")) {
|
||||
Section(header: Text("Discovered")) {
|
||||
ForEach(remoteClientState.foundServers) { server in
|
||||
Button {
|
||||
isAutoConnect = true
|
||||
selectedServer = server
|
||||
} label: {
|
||||
Text(server.name)
|
||||
}
|
||||
MacDeviceLabel(server.name, device: .init(model: server.model))
|
||||
}.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}.listStyle(.plain)
|
||||
}.frame(maxWidth: idiom == .pad ? 600 : nil)
|
||||
.alert(item: $remoteClientState.alertMessage) { item in
|
||||
}.listStyle(.insetGrouped)
|
||||
}.alert(item: $remoteClientState.alertMessage) { item in
|
||||
Alert(title: Text(item.message))
|
||||
}
|
||||
.sheet(item: $selectedServer) { server in
|
||||
|
|
|
@ -47,8 +47,8 @@ actor UTMRemoteClient {
|
|||
func startScanning() {
|
||||
scanTask = Task {
|
||||
await withErrorAlert {
|
||||
for try await endpoints in Connection.endpoints(forServiceType: service) {
|
||||
await self.didFindEndpoints(endpoints)
|
||||
for try await results in Connection.browse(forServiceType: service) {
|
||||
await self.didFindResults(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,16 +59,23 @@ actor UTMRemoteClient {
|
|||
scanTask = nil
|
||||
}
|
||||
|
||||
func didFindEndpoints(_ endpoints: [NWEndpoint]) async {
|
||||
self.endpoints = endpoints.reduce(into: [String: NWEndpoint]()) { map, endpoint in
|
||||
map[endpoint.debugDescription] = endpoint
|
||||
func didFindResults(_ results: Set<NWBrowser.Result>) async {
|
||||
self.endpoints = results.reduce(into: [String: NWEndpoint]()) { map, result in
|
||||
map[result.endpoint.debugDescription] = result.endpoint
|
||||
}
|
||||
let servers = endpoints.compactMap { endpoint in
|
||||
switch endpoint {
|
||||
let servers = results.compactMap { result in
|
||||
let model: String?
|
||||
if case .bonjour(let txtRecord) = result.metadata,
|
||||
case .string(let value) = txtRecord.getEntry(for: "Model") {
|
||||
model = value
|
||||
} else {
|
||||
model = nil
|
||||
}
|
||||
switch result.endpoint {
|
||||
case .hostPort(let host, _):
|
||||
return State.Server(hostname: host.debugDescription, name: host.debugDescription, lastSeen: Date())
|
||||
return State.Server(hostname: result.endpoint.hostname!, model: model, name: host.debugDescription, lastSeen: Date())
|
||||
case .service(let name, _, _, _):
|
||||
return State.Server(hostname: endpoint.debugDescription, name: name, lastSeen: Date())
|
||||
return State.Server(hostname: result.endpoint.debugDescription, model: model, name: name, lastSeen: Date())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -107,6 +114,7 @@ extension UTMRemoteClient {
|
|||
typealias ServerFingerprint = String
|
||||
struct Server: Codable, Identifiable, Hashable {
|
||||
let hostname: String
|
||||
var model: String?
|
||||
var fingerprint: ServerFingerprint?
|
||||
var name: String
|
||||
var lastSeen: Date
|
||||
|
|
|
@ -57,6 +57,7 @@ extension UTMRemoteMessageServer {
|
|||
struct Reply: Serializable, Codable {
|
||||
let version: Int
|
||||
let capabilities: UTMCapabilities
|
||||
let model: String
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,6 +98,10 @@ actor UTMRemoteServer {
|
|||
}
|
||||
}
|
||||
|
||||
private var metadata: NWTXTRecord {
|
||||
NWTXTRecord(["Model": MacDevice.current.model])
|
||||
}
|
||||
|
||||
func start() async {
|
||||
do {
|
||||
try await center.requestAuthorization(options: .alert)
|
||||
|
@ -112,7 +116,7 @@ actor UTMRemoteServer {
|
|||
registerNotifications()
|
||||
listener = Task {
|
||||
await withErrorNotification {
|
||||
for try await connection in Connection.advertise(forServiceType: service, identity: keyManager.identity) {
|
||||
for try await connection in Connection.advertise(forServiceType: service, txtRecord: metadata, identity: keyManager.identity) {
|
||||
if let connection = try? await Connection(connection: connection) {
|
||||
await newRemoteConnection(connection)
|
||||
}
|
||||
|
@ -579,7 +583,7 @@ extension UTMRemoteServer {
|
|||
}
|
||||
|
||||
private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
|
||||
return .init(version: UTMRemoteMessageServer.version, capabilities: .current)
|
||||
return .init(version: UTMRemoteMessageServer.version, capabilities: .current, model: MacDevice.current.model)
|
||||
}
|
||||
|
||||
private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply {
|
||||
|
|
|
@ -424,6 +424,8 @@
|
|||
CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
|
||||
CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
|
||||
CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
|
||||
CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
|
||||
CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
|
||||
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; };
|
||||
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
|
||||
CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
|
||||
|
@ -1765,6 +1767,7 @@
|
|||
CE0DF17125A80B6300A51894 /* Bootstrap.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Bootstrap.c; sourceTree = "<group>"; };
|
||||
CE0E9B86252FD06B0026E02B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
CE19392526DCB093005CEC17 /* RAMSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RAMSlider.swift; sourceTree = "<group>"; };
|
||||
CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = "<group>"; };
|
||||
CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = "<group>"; };
|
||||
CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = "<group>"; };
|
||||
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = "<group>"; };
|
||||
|
@ -2876,6 +2879,7 @@
|
|||
CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
|
||||
8471770527CC974F00D3A50B /* DefaultTextField.swift */,
|
||||
8432329328C2ED9000CFBC97 /* FileBrowseField.swift */,
|
||||
CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */,
|
||||
84F909FE289488F90008DBE2 /* MenuLabel.swift */,
|
||||
CED234EC254796E500ED0A57 /* NumberTextField.swift */,
|
||||
CE19392526DCB093005CEC17 /* RAMSlider.swift */,
|
||||
|
@ -3603,6 +3607,7 @@
|
|||
2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
|
||||
CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,
|
||||
85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */,
|
||||
CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
|
||||
CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */,
|
||||
845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */,
|
||||
CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */,
|
||||
|
@ -4025,6 +4030,7 @@
|
|||
CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */,
|
||||
CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */,
|
||||
CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */,
|
||||
CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
|
||||
CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */,
|
||||
CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */,
|
||||
CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */,
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"location" : "https://github.com/utmapp/SwiftConnect",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "c8c5584be464065688b6674f04510f38d4f4adb0"
|
||||
"revision" : "c6e84abcc1563a1ec6521d6649b5b918494539bc"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue