229 lines
7.5 KiB
Swift
229 lines
7.5 KiB
Swift
|
|
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)
|
||
|
|
}
|
||
|
|
}
|