hudini/Sources/HUDini/PreferencesView.swift

229 lines
7.5 KiB
Swift
Raw Normal View History

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)
}
}