diff --git a/Meetingnotes.xcodeproj/project.pbxproj b/Meetingnotes.xcodeproj/project.pbxproj index 0932896..a91855a 100644 --- a/Meetingnotes.xcodeproj/project.pbxproj +++ b/Meetingnotes.xcodeproj/project.pbxproj @@ -276,7 +276,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 22; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -290,7 +290,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; ONLY_ACTIVE_ARCH = NO; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; @@ -312,7 +312,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 22; + CURRENT_PROJECT_VERSION = 23; DEVELOPMENT_ASSET_PATHS = "\"meetingnotes/Preview Content\""; DEVELOPMENT_TEAM = G9LVHZAJNX; ENABLE_HARDENED_RUNTIME = YES; @@ -326,7 +326,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.1.10; + MARKETING_VERSION = 1.1.11; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D ENABLE_TCC_SPI"; PRODUCT_BUNDLE_IDENTIFIER = net.jamesbone.meetingnotes; diff --git a/meetingnotes/Info.plist b/meetingnotes/Info.plist index 4094c80..650169b 100644 --- a/meetingnotes/Info.plist +++ b/meetingnotes/Info.plist @@ -2,6 +2,8 @@ + LSUIElement + NSAppTransportSecurity NSAllowsLocalNetworking diff --git a/meetingnotes/MeetingnotesApp.swift b/meetingnotes/MeetingnotesApp.swift index eb91479..2f4ee35 100644 --- a/meetingnotes/MeetingnotesApp.swift +++ b/meetingnotes/MeetingnotesApp.swift @@ -6,12 +6,14 @@ // import SwiftUI +import AppKit import Sparkle import PostHog @main struct MeetingnotesApp: App { private let updaterController: SPUStandardUpdaterController + @StateObject private var recordingSessionManager = RecordingSessionManager.shared init() { updaterController = SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil) @@ -34,9 +36,10 @@ struct MeetingnotesApp: App { } var body: some Scene { - WindowGroup { + WindowGroup("Meetingnotes", id: "main") { ContentView() .frame(minWidth: 700, minHeight: 400) + .background(MainWindowConfigurator()) } .windowResizability(.automatic) .defaultSize(width: 1000, height: 600) @@ -45,6 +48,123 @@ struct MeetingnotesApp: App { CheckForUpdatesView(updater: updaterController.updater) } } + + MenuBarExtra { + MeetingnotesMenu( + recordingSessionManager: recordingSessionManager, + updater: updaterController.updater + ) + } label: { + Image(systemName: recordingSessionManager.isRecording ? "record.circle.fill" : "waveform") + .accessibilityLabel(recordingSessionManager.isRecording ? "Meetingnotes recording" : "Meetingnotes") + } + } +} + +private struct MeetingnotesMenu: View { + @ObservedObject var recordingSessionManager: RecordingSessionManager + let updater: SPUUpdater + @Environment(\.openWindow) private var openWindow + + var body: some View { + if recordingSessionManager.isRecording { + Label("Recording", systemImage: "record.circle.fill") + } else if recordingSessionManager.isProcessing { + Label("Processing audio", systemImage: "hourglass") + } + + Button { + MainWindowController.shared.show(using: openWindow) + } label: { + Label("Open Meetingnotes", systemImage: "macwindow") + } + + Divider() + + Button { + updater.checkForUpdates() + } label: { + Label("Check for Updates...", systemImage: "arrow.triangle.2.circlepath") + } + .keyboardShortcut("u") + + Button { + NSApplication.shared.terminate(nil) + } label: { + Label("Quit Meetingnotes", systemImage: "power") + } + .keyboardShortcut("q") + } +} + +@MainActor +private final class MainWindowController: NSObject { + static let shared = MainWindowController() + + private weak var window: NSWindow? + + func configure(_ window: NSWindow) { + if self.window !== window { + if let previousWindow = self.window { + NotificationCenter.default.removeObserver( + self, + name: NSWindow.willMiniaturizeNotification, + object: previousWindow + ) + } + self.window = window + NotificationCenter.default.addObserver( + self, + selector: #selector(windowWillMiniaturize(_:)), + name: NSWindow.willMiniaturizeNotification, + object: window + ) + } + + if let minimizeButton = window.standardWindowButton(.miniaturizeButton) { + minimizeButton.target = self + minimizeButton.action = #selector(hideWindow(_:)) + } + } + + func show(using openWindow: OpenWindowAction) { + if let window { + window.makeKeyAndOrderFront(nil) + } else { + openWindow(id: "main") + } + NSApplication.shared.activate(ignoringOtherApps: true) + } + + @objc private func hideWindow(_ sender: NSButton) { + sender.window?.orderOut(nil) + } + + @objc private func windowWillMiniaturize(_ notification: Notification) { + guard let window = notification.object as? NSWindow else { return } + DispatchQueue.main.async { + window.deminiaturize(nil) + window.orderOut(nil) + } + } +} + +private struct MainWindowConfigurator: NSViewRepresentable { + func makeNSView(context: Context) -> NSView { + let view = NSView() + configureWindow(for: view) + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { + configureWindow(for: nsView) + } + + private func configureWindow(for view: NSView) { + DispatchQueue.main.async { + guard let window = view.window else { return } + MainWindowController.shared.configure(window) + } } }