From 8a09e7138da86368cf045eaa6ed55a4cf437df3c Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Sat, 28 Oct 2023 20:47:31 -0600 Subject: [PATCH] Add menu bar --- Whisky.xcodeproj/project.pbxproj | 33 ++++++ Whisky/AppDelegate.swift | 2 +- Whisky/Assets.xcassets/MenuBar/Contents.json | 6 ++ .../whisky.glass.symbolset/Contents.json | 12 +++ .../whisky.glass.symbolset/whisky.glass.svg | 101 ++++++++++++++++++ Whisky/Extensions/Bundle+Extensions.swift | 25 +++++ Whisky/Localizable.xcstrings | 30 ++++++ Whisky/Models/Bottle.swift | 35 ++++++ Whisky/Views/Bottle/BottleView.swift | 37 ++----- Whisky/Views/Bottle/Pins/PinsView.swift | 11 +- Whisky/Views/Menu Bar/BottleBarView.swift | 68 ++++++++++++ Whisky/Views/Menu Bar/ProgramBarView.swift | 57 ++++++++++ Whisky/Views/Menu Bar/WhiskyBarView.swift | 38 +++++++ Whisky/Views/Programs/ProgramMenuView.swift | 19 +++- Whisky/Views/WhiskyApp.swift | 12 ++- .../Sources/WhiskyKit/Whisky/Bottle.swift | 8 ++ .../Sources/WhiskyKit/Whisky/Program.swift | 22 ++++ 17 files changed, 473 insertions(+), 43 deletions(-) create mode 100644 Whisky/Assets.xcassets/MenuBar/Contents.json create mode 100644 Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/Contents.json create mode 100644 Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/whisky.glass.svg create mode 100644 Whisky/Extensions/Bundle+Extensions.swift create mode 100644 Whisky/Views/Menu Bar/BottleBarView.swift create mode 100644 Whisky/Views/Menu Bar/ProgramBarView.swift create mode 100644 Whisky/Views/Menu Bar/WhiskyBarView.swift diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj index b94c75396..ed34d512d 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -55,6 +55,10 @@ 8C73E1342AF472FC00B6FB45 /* ProgramMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */; }; 8CB681E52AED7C6F0018D319 /* WhiskyKit in Resources */ = {isa = PBXBuildFile; fileRef = 8CB681E42AED7C6F0018D319 /* WhiskyKit */; }; 8CB681E72AED7CD00018D319 /* WhiskyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB681E62AED7CD00018D319 /* WhiskyKit */; }; + 8CB681EA2AEDEDC20018D319 /* WhiskyBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB681E92AEDEDC20018D319 /* WhiskyBarView.swift */; }; + 8CB681EC2AEDEDE70018D319 /* BottleBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB681EB2AEDEDE70018D319 /* BottleBarView.swift */; }; + 8CB681EE2AEDEE2F0018D319 /* ProgramBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB681ED2AEDEE2F0018D319 /* ProgramBarView.swift */; }; + 8CB681F12AEDF9620018D319 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB681F02AEDF9620018D319 /* Bundle+Extensions.swift */; }; AB66A8642A4195B10006D238 /* Rosetta2.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB66A8632A4195B10006D238 /* Rosetta2.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; }; @@ -150,6 +154,10 @@ 6EFDF6652AAE303300EF622F /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = ""; }; 8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramMenuView.swift; sourceTree = ""; }; 8CB681E42AED7C6F0018D319 /* WhiskyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WhiskyKit; sourceTree = ""; }; + 8CB681E92AEDEDC20018D319 /* WhiskyBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyBarView.swift; sourceTree = ""; }; + 8CB681EB2AEDEDE70018D319 /* BottleBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleBarView.swift; sourceTree = ""; }; + 8CB681ED2AEDEE2F0018D319 /* ProgramBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramBarView.swift; sourceTree = ""; }; + 8CB681F02AEDF9620018D319 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; AB66A8632A4195B10006D238 /* Rosetta2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rosetta2.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -257,6 +265,7 @@ 6E40495429CCA19C006E3F1B /* Whisky */ = { isa = PBXGroup; children = ( + 8CB681EF2AEDF9450018D319 /* Extensions */, EEA5A2452A31DD65008274AE /* AppDelegate.swift */, 6E5197CF29D71FF900CF655E /* Models */, 6E5197D029D7200700CF655E /* Utils */, @@ -290,6 +299,7 @@ 6E5197CD29D71FCD00CF655E /* Views */ = { isa = PBXGroup; children = ( + 8CB681E82AEDED9D0018D319 /* Menu Bar */, 63FFDE822ADEFADF00178665 /* Common */, 6E49E01F2AECB7D000009CAC /* Settings */, 6E6C0CF02A419A5800356232 /* Setup */, @@ -373,6 +383,24 @@ path = WhiskyThumbnail; sourceTree = ""; }; + 8CB681E82AEDED9D0018D319 /* Menu Bar */ = { + isa = PBXGroup; + children = ( + 8CB681E92AEDEDC20018D319 /* WhiskyBarView.swift */, + 8CB681EB2AEDEDE70018D319 /* BottleBarView.swift */, + 8CB681ED2AEDEE2F0018D319 /* ProgramBarView.swift */, + ); + path = "Menu Bar"; + sourceTree = ""; + }; + 8CB681EF2AEDF9450018D319 /* Extensions */ = { + isa = PBXGroup; + children = ( + 8CB681F02AEDF9620018D319 /* Bundle+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -563,6 +591,7 @@ buildActionMask = 2147483647; files = ( EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */, + 8CB681EE2AEDEE2F0018D319 /* ProgramBarView.swift in Sources */, 6E70A4A12A9A280C007799E9 /* WhiskyCmd.swift in Sources */, 6E40495829CCA19C006E3F1B /* ContentView.swift in Sources */, 6EF557982A410599001A4F09 /* SetupView.swift in Sources */, @@ -578,13 +607,16 @@ 6E17B6492AF4118F00831173 /* EnvironmentArgView.swift in Sources */, 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */, 6E6C0CF82A419A8C00356232 /* GPTKInstallView.swift in Sources */, + 8CB681F12AEDF9620018D319 /* Bundle+Extensions.swift in Sources */, 6E40498329CCA91B006E3F1B /* Bottle.swift in Sources */, 6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */, + 8CB681EA2AEDEDC20018D319 /* WhiskyBarView.swift in Sources */, 6E17B6462AF3FDC100831173 /* PinsView.swift in Sources */, 6E064B1429DD331F00D9A2D2 /* SparkleView.swift in Sources */, 6E40495629CCA19C006E3F1B /* WhiskyApp.swift in Sources */, 8C73E1342AF472FC00B6FB45 /* ProgramMenuView.swift in Sources */, 6E50D98329CD6066008C39F6 /* BottleVM.swift in Sources */, + 8CB681EC2AEDEDE70018D319 /* BottleBarView.swift in Sources */, 6E6915452A3265BB0085BBB7 /* Logger.swift in Sources */, 6E49E0212AECB7DB00009CAC /* SettingsView.swift in Sources */, 6E7C07C02AAF570100F6E66B /* FileOpenView.swift in Sources */, @@ -766,6 +798,7 @@ INFOPLIST_KEY_NSCameraUsageDescription = "A Windows application is trying to access the camera."; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Whisky"; INFOPLIST_KEY_NSMicrophoneUsageDescription = "A Windows application is trying to access the microphone."; + INFOPLIST_KEY_UIStatusBarStyle = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", diff --git a/Whisky/AppDelegate.swift b/Whisky/AppDelegate.swift index 25524cc30..98a17272a 100644 --- a/Whisky/AppDelegate.swift +++ b/Whisky/AppDelegate.swift @@ -39,7 +39,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + return false } private static var appUrl: URL? { diff --git a/Whisky/Assets.xcassets/MenuBar/Contents.json b/Whisky/Assets.xcassets/MenuBar/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Whisky/Assets.xcassets/MenuBar/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/Contents.json b/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/Contents.json new file mode 100644 index 000000000..d4b340016 --- /dev/null +++ b/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "whisky.glass.svg", + "idiom" : "universal" + } + ] +} diff --git a/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/whisky.glass.svg b/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/whisky.glass.svg new file mode 100644 index 000000000..9d683161c --- /dev/null +++ b/Whisky/Assets.xcassets/MenuBar/whisky.glass.symbolset/whisky.glass.svg @@ -0,0 +1,101 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.5.0 + Requires Xcode 15 or greater + Generated from whisky.glass + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Whisky/Extensions/Bundle+Extensions.swift b/Whisky/Extensions/Bundle+Extensions.swift new file mode 100644 index 000000000..5d52a1737 --- /dev/null +++ b/Whisky/Extensions/Bundle+Extensions.swift @@ -0,0 +1,25 @@ +// +// Bundle+Extensions.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import Foundation + +extension Bundle { + static var wiskeyBundleIdentifier: String { + return "com.isaacmarovitz.Whisky" + } +} diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 8eacf0433..079a59f5d 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -8144,6 +8144,36 @@ } } }, + "menubar.bottles" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bottles" + } + } + } + }, + "menubar.morePrograms" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "More..." + } + } + } + }, + "menubar.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Whisky" + } + } + } + }, "open.bottle" : { "localizations" : { "da" : { diff --git a/Whisky/Models/Bottle.swift b/Whisky/Models/Bottle.swift index f179867ac..904c16a51 100644 --- a/Whisky/Models/Bottle.swift +++ b/Whisky/Models/Bottle.swift @@ -19,8 +19,14 @@ import Foundation import AppKit import WhiskyKit +import UniformTypeIdentifiers +import os.log extension Bottle { + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? Bundle.wiskeyBundleIdentifier, category: "Bottle" + ) + func openCDrive() { NSWorkspace.shared.open(url.appending(path: "drive_c")) } @@ -180,4 +186,33 @@ extension Bottle { func rename(newName: String) { settings.name = newName } + + @MainActor + /// Open a panel to chose a file for running + /// - Returns: URL of the file we wish to run + public func choseFileForRun() async -> URL? { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + panel.allowedContentTypes = [ + UTType.exe, UTType(exportedAs: "com.microsoft.msi-installer"), + UTType(exportedAs: "com.microsoft.bat") + ] + panel.directoryURL = url.appending(path: "drive_c") + let result = await panel.begin() + guard result == .OK else { return nil } + return panel.urls.first + } + + @MainActor + /// Open an open panel, chose a file and attempt to run it + /// - Returns: true or false if a file was run + public func openFileForRun(url: URL) async throws { + if url.pathExtension == "bat" { + try await Wine.runBatchFile(url: url, bottle: self) + } else { + try await Wine.runExternalProgram(url: url, bottle: self) + } + } } diff --git a/Whisky/Views/Bottle/BottleView.swift b/Whisky/Views/Bottle/BottleView.swift index 0aef4d346..1f5ee6237 100644 --- a/Whisky/Views/Bottle/BottleView.swift +++ b/Whisky/Views/Bottle/BottleView.swift @@ -79,35 +79,18 @@ struct BottleView: View { showWinetricksSheet.toggle() } Button("button.run") { - let panel = NSOpenPanel() - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.canChooseFiles = true - panel.allowedContentTypes = [UTType.exe, - UTType(exportedAs: "com.microsoft.msi-installer"), - UTType(exportedAs: "com.microsoft.bat")] - panel.directoryURL = bottle.url.appending(path: "drive_c") - panel.begin { result in - programLoading = true - Task(priority: .userInitiated) { - if result == .OK { - if let url = panel.urls.first { - do { - if url.pathExtension == "bat" { - try await Wine.runBatchFile(url: url, bottle: bottle) - } else { - try await Wine.runExternalProgram(url: url, bottle: bottle) - } - } catch { - print("Failed to run external program: \(error)") - } - programLoading = false - } - } else { - programLoading = false - } + Task { + guard let fileURL = await bottle.choseFileForRun() else { return } + programLoading = false + + do { + try await bottle.openFileForRun(url: fileURL) updateStartMenu() + } catch { + Bottle.logger.error("Failed to run external program: \(error)") } + + programLoading = false } } .disabled(programLoading) diff --git a/Whisky/Views/Bottle/Pins/PinsView.swift b/Whisky/Views/Bottle/Pins/PinsView.swift index 77869ede3..a9da9e4bc 100644 --- a/Whisky/Views/Bottle/Pins/PinsView.swift +++ b/Whisky/Views/Bottle/Pins/PinsView.swift @@ -25,7 +25,7 @@ struct PinsView: View { @Binding var path: NavigationPath @State private var program: Program? - @State private var image: NSImage? + @State private var image: Image? @State private var showRenameSheet = false @State private var name: String = "" @State private var opening: Bool = false @@ -34,8 +34,7 @@ struct PinsView: View { VStack { Group { if let image = image { - Image(nsImage: image) - .resizable() + image.resizable() } else { Image(systemName: "app.dashed") .resizable() @@ -81,11 +80,7 @@ struct PinsView: View { name = pin.name Task.detached { @MainActor in program = bottle.programs.first(where: { $0.url == pin.url }) - if let program { - if let peFile = program.peFile { - image = peFile.bestIcon() - } - } + image = await program?.loadIcon() } } .onChange(of: name) { diff --git a/Whisky/Views/Menu Bar/BottleBarView.swift b/Whisky/Views/Menu Bar/BottleBarView.swift new file mode 100644 index 000000000..717a1e6cd --- /dev/null +++ b/Whisky/Views/Menu Bar/BottleBarView.swift @@ -0,0 +1,68 @@ +// +// BottleBarView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import WhiskyKit + +/// A menu for a single bottle +struct BottleBarView: View { + @ObservedObject var bottle: Bottle + + var body: some View { + Group { + Button("button.run", systemImage: "play") { + Task { + guard let fileURL = await bottle.choseFileForRun() else { return } + + do { + try await bottle.openFileForRun(url: fileURL) + } catch { + Bottle.logger.error("Failed to run external program: \(error)") + } + } + } + + Section("tab.programs") { + let pinnedPrograms = bottle.pinnedPrograms + let unpinnedPrograms = bottle.programs.unpinned + + ForEach(pinnedPrograms, id: \.pin.url) { pinnedProgram in + ProgramBarView(program: pinnedProgram.program, pin: pinnedProgram.pin) + } + + Menu("menubar.morePrograms") { + ForEach(unpinnedPrograms, id: \.url) { program in + ProgramBarView(program: program, pin: nil) + } + }.badge(unpinnedPrograms.count) + } + } + } +} + +private extension Sequence where Iterator.Element == Program { + /// Filter all pinned programs + var pinned: [Program] { + return self.filter({ $0.pinned }) + } + + /// Filter all unpinned programs + var unpinned: [Program] { + return self.filter({ !$0.pinned }) + } +} diff --git a/Whisky/Views/Menu Bar/ProgramBarView.swift b/Whisky/Views/Menu Bar/ProgramBarView.swift new file mode 100644 index 000000000..3605f814f --- /dev/null +++ b/Whisky/Views/Menu Bar/ProgramBarView.swift @@ -0,0 +1,57 @@ +// +// ProgramBarView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI +import WhiskyKit + +/// A menu for a specific program +struct ProgramBarView: View { + @ObservedObject var program: Program + let pin: PinnedProgram? + @State private var image: Image? + + var body: some View { + Menu { + ProgramMenuView(program: program) + } label: { + HStack { + image + Text(pin?.name ?? program.name) + } + } primaryAction: { + Task { + await program.run() + } + } + .labelStyle(.titleAndIcon) + .onAppear { + guard pin != nil else { return } + Task { + image = await program.loadIcon() + } + } + } +} + +extension Program { + var viewImage: Image? { + guard let peFile = peFile else { return nil } + guard let nsImage = peFile.bestIcon() else { return nil } + return Image(nsImage: nsImage) + } +} diff --git a/Whisky/Views/Menu Bar/WhiskyBarView.swift b/Whisky/Views/Menu Bar/WhiskyBarView.swift new file mode 100644 index 000000000..d0230b084 --- /dev/null +++ b/Whisky/Views/Menu Bar/WhiskyBarView.swift @@ -0,0 +1,38 @@ +// +// WhiskyBarView.swift +// Whisky +// +// This file is part of Whisky. +// +// Whisky is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Whisky. +// If not, see https://www.gnu.org/licenses/. +// + +import SwiftUI + +/// Application wide menu for the menu bar +struct WhiskyBarView: View { + @ObservedObject private var bottleVM = BottleVM.shared + + var body: some View { + Button("kill.bottles", systemImage: "stop.circle.fill") { + WhiskyApp.killBottles() + }.labelStyle(.titleAndIcon) + + Section("menubar.bottles") { + ForEach(bottleVM.bottles) { bottle in + Menu(bottle.settings.name) { + BottleBarView(bottle: bottle) + } + } + }.labelStyle(.titleAndIcon) + } +} diff --git a/Whisky/Views/Programs/ProgramMenuView.swift b/Whisky/Views/Programs/ProgramMenuView.swift index d3a2fc268..0a44a7793 100644 --- a/Whisky/Views/Programs/ProgramMenuView.swift +++ b/Whisky/Views/Programs/ProgramMenuView.swift @@ -21,7 +21,17 @@ import WhiskyKit struct ProgramMenuView: View { @ObservedObject var program: Program - @Binding var path: NavigationPath + @Binding var path: NavigationPath? + + init(program: Program, path: Binding) { + self.program = program + _path = .init(path) + } + + init(program: Program) { + self.program = program + _path = .constant(nil) + } var body: some View { Button("button.run", systemImage: "play") { @@ -31,10 +41,11 @@ struct ProgramMenuView: View { } .labelStyle(.titleAndIcon) Section("program.settings") { - Button("program.config", systemImage: "gearshape") { - path.append(program) + if path != nil { + Button("program.config", systemImage: "gearshape", action: { + path?.append(program) + }).labelStyle(.titleAndIcon) } - .labelStyle(.titleAndIcon) let buttonName = program.pinned ? String(localized: "button.unpin") diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift index 6a08339c0..17bc0f0ff 100644 --- a/Whisky/Views/WhiskyApp.swift +++ b/Whisky/Views/WhiskyApp.swift @@ -18,12 +18,13 @@ import SwiftUI import Sparkle +import WhiskyKit @main struct WhiskyApp: App { - @State var showSetup: Bool = false - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @Environment(\.openURL) var openURL + @State private var showSetup: Bool = false + @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + @Environment(\.openURL) private var openURL private let updaterController: SPUStandardUpdaterController init() { @@ -112,6 +113,11 @@ struct WhiskyApp: App { Settings { SettingsView() } + + // MARK: - Menu bar + MenuBarExtra("menubar.title", image: "whisky.glass") { + WhiskyBarView() + } } static func killBottles() { diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift index 098ac75f2..62b6f509a 100644 --- a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift +++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift @@ -41,6 +41,14 @@ public class Bottle: Hashable, Identifiable, ObservableObject { public var inFlight: Bool = false public var isActive: Bool = false + public var pinnedPrograms: [(pin: PinnedProgram, program: Program)] { + let pins = settings.pins + return programs.compactMap { program in + guard let pin = pins.first(where: { $0.url == program.url }) else { return nil } + return (pin, program) + } + } + public init(bottleUrl: URL, inFlight: Bool = false, isActive: Bool = false) { let metadataURL = bottleUrl.appending(path: "Metadata").appendingPathExtension("plist") self.url = bottleUrl diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift index 8c6e4ee86..59a04f812 100644 --- a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift +++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift @@ -34,6 +34,9 @@ public class Program: Hashable, ObservableObject { public let url: URL public let settingsURL: URL + @MainActor @Published private var icon: Image? + @MainActor private var loadIconTask: Task? + @Published public var settings: ProgramSettings { didSet { saveSettings() } } @@ -104,4 +107,23 @@ public class Program: Hashable, ObservableObject { Logger.wineKit.error("Failed to save settings for `\(self.name)`: \(error)") } } + + @MainActor public func loadIcon() async -> Image? { + guard loadIconTask == nil else { + return await loadIconTask?.value + } + + // Return the icon in-case we set it somewhere else + if let icon = self.icon { return icon } + + loadIconTask = Task.detached { @MainActor in + guard let peFile = self.peFile else { return nil } + guard let image = peFile.bestIcon() else { return nil } + let icon = Image(nsImage: image) + self.icon = icon + return icon + } + + return await loadIconTask?.value + } }