Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ let package = Package(
name: "PhemyNative",
dependencies: ["CPhemyCore"],
path: "Sources/PhemyNative",
resources: [
.copy("Resources")
],
// Resources loaded via custom BundleAccessor.swift (not SwiftPM auto-bundling)
exclude: ["Resources"],
linkerSettings: [
.unsafeFlags([
"-L", ".", // libphemy_core.dylib in project root
Expand Down
27 changes: 27 additions & 0 deletions Sources/PhemyNative/BundleAccessor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

extension Foundation.Bundle {
/// Custom resource bundle accessor that works in both .app bundles and SwiftPM build directories.
static let module: Bundle = {
let bundleName = "PhemyNative_PhemyNative"

let candidates = [
// Inside .app bundle: Contents/Resources/
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/\(bundleName).bundle"),
// Alongside executable (SwiftPM build output)
Bundle.main.bundleURL.appendingPathComponent("\(bundleName).bundle"),
// Alongside the binary directly
Bundle(for: BundleFinder.self).bundleURL.appendingPathComponent("\(bundleName).bundle"),
]

for candidate in candidates {
if let bundle = Bundle(path: candidate.path) {
return bundle
}
}

fatalError("Unable to find resource bundle \(bundleName)")
}()
}

private class BundleFinder {}
48 changes: 27 additions & 21 deletions Sources/PhemyNative/PhemyNativeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ struct PhemyNativeApp: App {
guard let url = Bundle.module.url(forResource: "MenuBarIcon", withExtension: "png"),
let image = NSImage(contentsOf: url) else { return nil }
image.isTemplate = true
// 44px source is @2x — set point size to 22 so it renders at correct menu bar size
image.size = NSSize(width: 22, height: 22)
// 44px source is @2x — set point size to 18 for standard menu bar icon size
image.size = NSSize(width: 18, height: 18)
return image
}()

Expand All @@ -27,10 +27,6 @@ struct PhemyNativeApp: App {
}()

init() {
// Set applicationIconImage early for About dialogs and window icons
if let icon = Self.appIcon {
NSApplication.shared.applicationIconImage = icon
}
}

var body: some Scene {
Expand All @@ -48,9 +44,12 @@ struct PhemyNativeApp: App {

MenuBarExtra {
Button("Show Settings") {
NSApplication.shared.activate(ignoringOtherApps: true)
if let window = NSApplication.shared.windows.first {
window.makeKeyAndOrderFront(nil)
NSApp.setActivationPolicy(.regular)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
NSApplication.shared.activate(ignoringOtherApps: true)
if let window = NSApplication.shared.windows.first {
window.makeKeyAndOrderFront(nil)
}
}
}
.keyboardShortcut(",", modifiers: .command)
Expand Down Expand Up @@ -83,19 +82,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// when running as an unbundled executable
UserDefaults.standard.set(false, forKey: "NSQuitAlwaysKeepsWindows")

// Ensure app appears in Dock (MenuBarExtra can default to accessory mode)
// Default: don't hide from dock on close
if UserDefaults.standard.object(forKey: "hideFromDockOnClose") == nil {
UserDefaults.standard.set(false, forKey: "hideFromDockOnClose")
}

// Show in Dock on launch
NSApp.setActivationPolicy(.regular)

// Set custom Dock tile icon (unbundled executables need explicit Dock tile update)
if let icon = PhemyNativeApp.appIcon {
NSApp.applicationIconImage = icon
let dockTile = NSApp.dockTile
let imageView = NSImageView(frame: NSRect(x: 0, y: 0, width: dockTile.size.width, height: dockTile.size.height))
imageView.image = icon
imageView.imageScaling = .scaleProportionallyUpOrDown
dockTile.contentView = imageView
dockTile.display()
}
// Dock icon handled by CFBundleIconFile in Info.plist

overlayPanel = RecordingOverlayPanel(recordingManager: recordingManager)

Expand Down Expand Up @@ -161,7 +156,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
return error
}

/// Handle app re-activation (Dock click) — show settings window instead of default behavior.
/// When last window closes: if "hide from dock on close" is on, hide from Dock but stay in menu bar
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
let hideOnClose = UserDefaults.standard.bool(forKey: "hideFromDockOnClose")
if hideOnClose {
DispatchQueue.main.async {
NSApp.setActivationPolicy(.accessory)
}
}
return false
}

/// Handle app re-activation (Dock click) — show settings window.
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
if let window = NSApplication.shared.windows.first {
Expand Down
4 changes: 4 additions & 0 deletions Sources/PhemyNative/Views/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct GeneralSettingsView: View {
@ObservedObject var vm: SettingsViewModel
@EnvironmentObject var theme: ThemeManager
@AppStorage("hideFromDockOnClose") private var hideFromDockOnClose = false

var body: some View {
VStack(alignment: .leading, spacing: Spacing.sectionGap) {
Expand Down Expand Up @@ -59,6 +60,9 @@ struct GeneralSettingsView: View {
.toggleStyle(.switch)
.tint(theme.primary)
.onChange(of: vm.settings.launchAtStartup) { vm.autoSave() }
Toggle("Hide from Dock on close", isOn: $hideFromDockOnClose)
.toggleStyle(.switch)
.tint(theme.primary)
}

// Reset
Expand Down