import SwiftUI import AppKit // NSImage wrapper that actually renders properly in SwiftUI struct AppIconView: NSViewRepresentable { let nsImage: NSImage func makeNSView(context: Context) -> NSImageView { let view = NSImageView() view.imageScaling = .scaleProportionallyUpOrDown view.image = nsImage return view } func updateNSView(_ nsView: NSImageView, context: Context) { nsView.image = nsImage } } struct PreferencesView: View { @State private var selectedBundleID: String @State private var customAppName: String? @State private var customAppIcon: NSImage? @State private var startAtLogin: Bool let onSave: (String) -> Void let onStartAtLoginChanged: (Bool) -> Void init( currentBundleID: String, startAtLogin: Bool, onSave: @escaping (String) -> Void, onStartAtLoginChanged: @escaping (Bool) -> Void ) { _selectedBundleID = State(initialValue: currentBundleID) _startAtLogin = State(initialValue: startAtLogin) self.onSave = onSave self.onStartAtLoginChanged = onStartAtLoginChanged if !MediaPlayer.known.contains(where: { $0.bundleID == currentBundleID }) { if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: currentBundleID) { _customAppName = State(initialValue: url.deletingPathExtension().lastPathComponent) _customAppIcon = State(initialValue: NSWorkspace.shared.icon(forFile: url.path)) } } } var body: some View { VStack(spacing: 0) { Text("HUDini Preferences") .font(.headline) .padding(.top, 16) .padding(.bottom, 12) Divider() VStack(alignment: .leading, spacing: 8) { Text("Launch on Play key:") .font(.subheadline) .foregroundColor(.secondary) VStack(spacing: 0) { ForEach(Array(MediaPlayer.known.enumerated()), id: \.element.bundleID) { index, player in PlayerRow( player: player, isSelected: selectedBundleID == player.bundleID, isInstalled: NSWorkspace.shared.urlForApplication( withBundleIdentifier: player.bundleID) != nil ) { selectedBundleID = player.bundleID customAppName = nil customAppIcon = nil onSave(player.bundleID) } if index < MediaPlayer.known.count - 1 { Divider().padding(.leading, 46) } } if let name = customAppName { Divider().padding(.leading, 46) CustomPlayerRow( name: name, icon: customAppIcon, isSelected: !MediaPlayer.known.contains(where: { $0.bundleID == selectedBundleID }) ) { onSave(selectedBundleID) } } } .background(Color(nsColor: .controlBackgroundColor)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color(nsColor: .separatorColor), lineWidth: 0.5) ) Button("Choose Other App...") { chooseCustomApp() } .padding(.top, 4) } .padding(16) Divider() HStack { Toggle("Start at login", isOn: Binding( get: { startAtLogin }, set: { newValue in startAtLogin = newValue onStartAtLoginChanged(newValue) } )) Spacer() } .padding(.horizontal, 16) .padding(.vertical, 12) Spacer() } .frame(width: 320, height: 440) } private func chooseCustomApp() { let panel = NSOpenPanel() panel.title = "Choose Application" panel.allowedContentTypes = [.application] panel.directoryURL = URL(fileURLWithPath: "/Applications") panel.canChooseDirectories = false panel.canChooseFiles = true panel.allowsMultipleSelection = false guard panel.runModal() == .OK, let url = panel.url, let bundle = Bundle(url: url), let bid = bundle.bundleIdentifier else { return } selectedBundleID = bid customAppName = url.deletingPathExtension().lastPathComponent customAppIcon = NSWorkspace.shared.icon(forFile: url.path) onSave(bid) } } struct PlayerRow: View { let player: MediaPlayer let isSelected: Bool let isInstalled: Bool let onSelect: () -> Void var body: some View { Button(action: onSelect) { HStack(spacing: 10) { iconView .frame(width: 30, height: 30) Text(player.name) .foregroundColor(isInstalled ? .primary : .secondary) Spacer() if !isInstalled { Text("not installed") .font(.caption) .foregroundColor(.secondary) } if isSelected { Image(systemName: "checkmark") .foregroundColor(.accentColor) .fontWeight(.semibold) } } .padding(.horizontal, 12) .padding(.vertical, 5) .contentShape(Rectangle()) } .buttonStyle(.plain) .background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear) } @ViewBuilder private var iconView: some View { if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: player.bundleID) { AppIconView(nsImage: NSWorkspace.shared.icon(forFile: url.path)) } else { Image(systemName: "music.note.house") .font(.system(size: 20)) .foregroundColor(.secondary) .frame(width: 30, height: 30) } } } struct CustomPlayerRow: View { let name: String let icon: NSImage? let isSelected: Bool let onSelect: () -> Void var body: some View { Button(action: onSelect) { HStack(spacing: 10) { if let icon = icon { AppIconView(nsImage: icon) .frame(width: 30, height: 30) } else { Image(systemName: "app.fill") .font(.system(size: 20)) .foregroundColor(.secondary) .frame(width: 30, height: 30) } Text(name) Spacer() if isSelected { Image(systemName: "checkmark") .foregroundColor(.accentColor) .fontWeight(.semibold) } } .padding(.horizontal, 12) .padding(.vertical, 5) .contentShape(Rectangle()) } .buttonStyle(.plain) .background(isSelected ? Color.accentColor.opacity(0.1) : Color.clear) } }