From 77da4d61b8984f373031a198bcbe93cfc9c65826 Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Sun, 26 Nov 2023 20:09:23 -0500 Subject: [PATCH 1/2] Fix Localizable --- Whisky/Localizable.xcstrings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings index 4fd9510b..686390f5 100644 --- a/Whisky/Localizable.xcstrings +++ b/Whisky/Localizable.xcstrings @@ -13253,7 +13253,7 @@ }, "fr" : { "stringUnit" : { - "state" : "translated, + "state" : "translated", "value" : "Réessayer" } }, From 4304725d1db6c7a1855085a6ec520383dd724e0a Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Sat, 28 Oct 2023 20:47:31 -0600 Subject: [PATCH 2/2] 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 | 39 ++----- Whisky/Views/Bottle/Pins/PinView.swift | 10 +- 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 | 11 +- .../Sources/WhiskyKit/Whisky/Program.swift | 22 ++++ 17 files changed, 471 insertions(+), 49 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 13af208a..81fdcb94 100644 --- a/Whisky.xcodeproj/project.pbxproj +++ b/Whisky.xcodeproj/project.pbxproj @@ -57,6 +57,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 */; }; DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */; }; EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; }; @@ -155,6 +159,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 = ""; }; DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; }; EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -266,6 +274,7 @@ 6E40495429CCA19C006E3F1B /* Whisky */ = { isa = PBXGroup; children = ( + 8CB681EF2AEDF9450018D319 /* Extensions */, EEA5A2452A31DD65008274AE /* AppDelegate.swift */, 6E5197CF29D71FF900CF655E /* Models */, 6E5197D029D7200700CF655E /* Utils */, @@ -299,6 +308,7 @@ 6E5197CD29D71FCD00CF655E /* Views */ = { isa = PBXGroup; children = ( + 8CB681E82AEDED9D0018D319 /* Menu Bar */, 63FFDE822ADEFADF00178665 /* Common */, 6E49E01F2AECB7D000009CAC /* Settings */, 6E6C0CF02A419A5800356232 /* Setup */, @@ -382,6 +392,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 */ @@ -572,6 +600,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 */, @@ -589,13 +618,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 /* PinView.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 */, 6E2B25C12B0E20F50084A67A /* PinAddView.swift in Sources */, 6E49E0212AECB7DB00009CAC /* SettingsView.swift in Sources */, @@ -778,6 +810,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 25524cc3..98a17272 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 00000000..73c00596 --- /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 00000000..d4b34001 --- /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 00000000..9d683161 --- /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 00000000..5d52a173 --- /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 686390f5..88a9548b 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 514e3169..fcbd3f03 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")) } @@ -174,4 +180,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 d68af8cc..e7575d92 100644 --- a/Whisky/Views/Bottle/BottleView.swift +++ b/Whisky/Views/Bottle/BottleView.swift @@ -37,7 +37,7 @@ struct BottleView: View { NavigationStack(path: $path) { ScrollView { LazyVGrid(columns: gridLayout, alignment: .center) { - ForEach(bottle.pinnedPrograms, id: \.id) { pinnedProgram in + ForEach(bottle.pinnedPrograms, id: \.pin.url) { pinnedProgram in PinView( bottle: bottle, program: pinnedProgram.program, pin: pinnedProgram.pin, path: $path ) @@ -77,35 +77,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/PinView.swift b/Whisky/Views/Bottle/Pins/PinView.swift index 0cdbd134..d56d2d37 100644 --- a/Whisky/Views/Bottle/Pins/PinView.swift +++ b/Whisky/Views/Bottle/Pins/PinView.swift @@ -24,8 +24,7 @@ struct PinView: View { @ObservedObject var program: Program @State var pin: PinnedProgram @Binding var path: NavigationPath - - @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 +33,7 @@ struct PinView: View { VStack { Group { if let image = image { - Image(nsImage: image) - .resizable() + image.resizable() } else { Image(systemName: "app.dashed") .resizable() @@ -78,9 +76,7 @@ struct PinView: View { .onAppear { name = pin.name Task.detached { @MainActor in - 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 00000000..717a1e6c --- /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 00000000..3605f814 --- /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 00000000..d0230b08 --- /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 d3a2fc26..0a44a779 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 6a08339c..17bc0f0f 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 75457436..0b50a414 100644 --- a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift +++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle.swift @@ -41,12 +41,11 @@ public class Bottle: Hashable, Identifiable, ObservableObject { public var inFlight: Bool = false public var isActive: Bool = false - /// All pins with their associated programs - public var pinnedPrograms: [(pin: PinnedProgram, program: Program, // swiftlint:disable:this large_tuple - id: String)] { - return settings.pins.compactMap { pin in - guard let program = programs.first(where: { $0.url == pin.url }) else { return nil } - return (pin, program, "\(pin.name)//\(program.url)") + 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) } } diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift index 40f87d20..b56bf0a6 100644 --- a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift +++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift @@ -33,6 +33,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? + public var name: String { url.lastPathComponent } @@ -107,4 +110,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 + } }