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
47 changes: 41 additions & 6 deletions LyricFever/LyricFever.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,18 +185,53 @@ struct LyricFever: App {
.preferredColorScheme(.dark)
.environment(viewmodel)
.onAppear {
// Block "Esc" button
// Block "Esc" only when NOT in native fullscreen (otherwise Esc is the
// user's standard escape hatch out of native fullscreen).
// Bind Cmd+Ctrl+F to toggle native fullscreen — as an agent app there's no
// standard View menu, so the OS shortcut has nothing to fire unless wired here.
// This shortcut is what lets a user in Windowed-fullscreen mode opt into
// (and back out of) native fullscreen.
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
if aEvent.keyCode == 53 { // if esc pressed
let fullscreenWindow = NSApp.windows.first(where: { $0.identifier?.rawValue == "fullscreen" })
let inNativeFullscreen = fullscreenWindow?.styleMask.contains(.fullScreen) ?? false
if aEvent.keyCode == 53 && !inNativeFullscreen { // esc, only in windowed mode
return nil
}
if aEvent.keyCode == 3 // F
&& aEvent.modifierFlags.contains(.command)
&& aEvent.modifierFlags.contains(.control) {
fullscreenWindow?.toggleFullScreen(nil)
return nil
}
return aEvent
}
Task { @MainActor in
let window = NSApp.windows.first {$0.identifier?.rawValue == "fullscreen"}
window?.collectionBehavior = .fullScreenPrimary
if window?.styleMask.rawValue != 49167 {
window?.toggleFullScreen(true)
guard let window = NSApp.windows.first(where: { $0.identifier?.rawValue == "fullscreen" }) else { return }
if viewmodel.userDefaultStorage.useWindowedFullscreen {
// Windowed-fullscreen: a movable/resizable borderless-style window on the
// current Space. If macOS restored prior native-fullscreen state, exit first
// so the styleMask change can take effect. .fullScreenPrimary keeps the
// native-fullscreen capability available via Cmd+Ctrl+F as an opt-in.
guard let screen = NSScreen.main else { return }
if window.styleMask.contains(.fullScreen) {
window.toggleFullScreen(nil)
}
window.isRestorable = false
window.collectionBehavior = .fullScreenPrimary
window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView]
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.isMovable = true
window.isMovableByWindowBackground = true
window.level = .normal
window.setFrame(screen.visibleFrame, display: true)
window.makeKeyAndOrderFront(nil)
} else {
// Native fullscreen (default, existing behavior).
window.collectionBehavior = .fullScreenPrimary
if window.styleMask.rawValue != 49167 {
window.toggleFullScreen(true)
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions LyricFever/Support Files/UserDefaultStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class UserDefaultStorage {
@ObservationIgnored var blurFullscreen: Bool
@ObservableUserDefault(.init(key: "animateOnStartupFullscreen", defaultValue: true, store: .standard))
@ObservationIgnored var animateOnStartupFullscreen: Bool
#if os(macOS)
// When true, the fullscreen window is a movable/resizable borderless window
// on the current Space instead of macOS native fullscreen (which swipes to a
// new Space and hides the menubar/Dock).
@ObservableUserDefault(.init(key: "useWindowedFullscreen", defaultValue: false, store: .standard))
@ObservationIgnored var useWindowedFullscreen: Bool
#endif
@ObservableUserDefault(.init(key: "romanize", defaultValue: false, store: .standard))
@ObservationIgnored var romanize: Bool
@ObservableUserDefault(.init(key: "romanizeMetadata", defaultValue: true, store: .standard))
Expand Down
17 changes: 12 additions & 5 deletions LyricFever/Views/FullscreenView/FullscreenView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ struct FullscreenView: View {
VStack(spacing: 7) {
Toggle("Blur surrounding lyrics", isOn: $viewmodel.userDefaultStorage.blurFullscreen)
Toggle("Animate on startup", isOn: $viewmodel.userDefaultStorage.animateOnStartupFullscreen)
Toggle("Windowed fullscreen (stays on current Space)", isOn: $viewmodel.userDefaultStorage.useWindowedFullscreen)
Button("Reset to default") {

}
Expand All @@ -158,7 +159,13 @@ struct FullscreenView: View {
.font(.system(size: 12))
}

@ViewBuilder var albumArt: some View {
@ViewBuilder func albumArt(containerSize: CGSize) -> some View {
// Derive the art's side length from the available container so it scales with the
// window — important when "Windowed fullscreen" is on and the user resizes the window.
// The column is half-width when lyrics are visible, full-width otherwise. Cap by height
// so the text/controls stack below the art still fits.
let columnWidth = viewmodel.canDisplayLyrics ? containerSize.width * 0.5 : containerSize.width
let artSide = max(120, min(columnWidth * 0.85, containerSize.height * 0.55))
VStack {
Spacer()
if let artworkImage = viewmodel.artworkImage {
Expand All @@ -167,13 +174,13 @@ struct FullscreenView: View {
.resizable()
.clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10)))
.shadow(radius: 5)
.frame(width: viewmodel.canDisplayLyrics ? 550 : 700, height: viewmodel.canDisplayLyrics ? 550 : 700)
.frame(width: artSide, height: artSide)
#else
Image(uiImage: artworkImage)
.resizable()
.clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10)))
.shadow(radius: 5)
.frame(width: canDisplayLyrics ? 550 : 700, height: canDisplayLyrics ? 550 : 700)
.frame(width: artSide, height: artSide)
#endif
}
else {
Expand All @@ -184,7 +191,7 @@ struct FullscreenView: View {
.background(.gray)
.clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10, bottomTrailing: 10, topTrailing: 10)))
.shadow(radius: 5)
.frame(width: viewmodel.canDisplayLyrics ? 550 : 650, height: viewmodel.canDisplayLyrics ? 550 : 650)
.frame(width: artSide, height: artSide)
}
Group {
Text(verbatim: viewmodel.currentlyPlayingName ?? "")
Expand Down Expand Up @@ -267,7 +274,7 @@ struct FullscreenView: View {
var body: some View {
GeometryReader { geo in
HStack {
albumArt
albumArt(containerSize: geo.size)
.frame(minWidth: 0.50*(geo.size.width), maxWidth: viewmodel.canDisplayLyrics ? 0.50*(geo.size.width) : .infinity)
if viewmodel.canDisplayLyrics {
lyrics(padding: 0.5*(geo.size.height))
Expand Down