Skip to content

Commit ec3c039

Browse files
ComicBitCopilotsozercan
authored
Implement Apple Music-style mini player (#246)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sertac Ozercan <sozercan@gmail.com>
1 parent 262a3be commit ec3c039

17 files changed

Lines changed: 1458 additions & 13 deletions

Scripts/build-app.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,21 @@ find "$APP_BUNDLE" -name '._*' -delete 2>/dev/null || true
359359

360360
# ── Code signing ──────────────────────────────────────────────────────────────
361361

362+
if [[ "$SIGNING_MODE" == "unsigned" || "$SIGNING_MODE" == "none" ]]; then
363+
echo "🔓 Skipping code signing."
364+
echo ""
365+
echo "✅ Build complete!"
366+
echo "📍 App location: $APP_BUNDLE"
367+
echo " Version: ${MARKETING_VERSION} (${BUILD_NUMBER})"
368+
echo " Commit: ${GIT_COMMIT}"
369+
echo " Arches: ${ARCH_LIST[*]}"
370+
echo " Signing: unsigned"
371+
echo ""
372+
echo "To run: open $APP_BUNDLE"
373+
echo "To install: cp -r $APP_BUNDLE /Applications/"
374+
exit 0
375+
fi
376+
362377
echo "🔏 Signing app..."
363378
if [[ "$SIGNING_MODE" == "adhoc" ]]; then
364379
CODESIGN_ARGS=(--force --sign -)

Sources/Kaset/AppDelegate.swift

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
8787
func applicationDidBecomeActive(_: Notification) {
8888
// When app becomes active (e.g., dock icon clicked), ensure main window is visible.
8989
// This handles the case where video window is visible but main window is hidden.
90+
if self.isSwitchedToMiniPlayer {
91+
if #available(macOS 26.0, *) {
92+
MiniPlayerWindowController.shared.orderFrontIfVisible()
93+
}
94+
return
95+
}
9096
self.showMainWindowIfNeeded()
9197
}
9298

9399
private func setupWindowDelegate() {
94100
DiagnosticsLogger.app.info("AppDelegate: setupWindowDelegate starting")
95101
for window in NSApplication.shared.windows where window.canBecomeMain {
96-
// Skip if this is the video window (has specific identifier)
97-
if window.identifier?.rawValue == AccessibilityID.VideoWindow.container {
102+
// Skip auxiliary player windows; only the regular app window should be hidden-on-close.
103+
if self.isAuxiliaryPlayerWindow(window) {
98104
continue
99105
}
100106
window.delegate = self
@@ -182,11 +188,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
182188

183189
/// Handle reopen (clicking dock icon) when all windows are closed.
184190
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
191+
if self.isSwitchedToMiniPlayer {
192+
if #available(macOS 26.0, *) {
193+
MiniPlayerWindowController.shared.orderFrontIfVisible()
194+
}
195+
return false
196+
}
197+
185198
// Show main window when dock icon is clicked
186199
self.showMainWindowIfNeeded()
187200
return true
188201
}
189202

203+
private var isSwitchedToMiniPlayer: Bool {
204+
guard let playerService else { return false }
205+
return playerService.isMiniPlayerVisible && playerService.miniPlayerMode == .switchFromMainWindow
206+
}
207+
190208
/// Shows the main window if it's not visible.
191209
private func showMainWindowIfNeeded() {
192210
DiagnosticsLogger.app.info("AppDelegate: showMainWindowIfNeeded")
@@ -207,9 +225,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
207225
return
208226
}
209227

210-
// Last resort: find any main-capable window that's not the video window
228+
// Last resort: find any main-capable window that's not an auxiliary player window.
211229
for window in NSApplication.shared.windows where window.canBecomeMain {
212-
if window.identifier?.rawValue == AccessibilityID.VideoWindow.container {
230+
if self.isAuxiliaryPlayerWindow(window) {
213231
continue
214232
}
215233
self.mainWindow = window
@@ -219,6 +237,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
219237
return
220238
}
221239
}
240+
241+
private func isAuxiliaryPlayerWindow(_ window: NSWindow) -> Bool {
242+
AccessibilityID.isAuxiliaryPlayerWindowIdentifier(window.identifier?.rawValue)
243+
}
222244
}
223245

224246
// MARK: NSWindowDelegate

Sources/Kaset/KasetApp.swift

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extension EnvironmentValues {
2525
struct KasetApp: App {
2626
/// App delegate for lifecycle management (background playback).
2727
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
28+
@Environment(\.openWindow) private var openWindow
2829

2930
@State private var authService = AuthService()
3031
@State private var webKitManager = WebKitManager.shared
@@ -171,6 +172,29 @@ struct KasetApp: App {
171172
self.equalizerService.retryStartIfEnabled()
172173
}
173174
}
175+
.onChange(of: self.playerService.isMiniPlayerVisible) { _, isVisible in
176+
if isVisible {
177+
MiniPlayerWindowController.shared.show(
178+
playerService: self.playerService,
179+
client: self.sharedClient,
180+
syncedLyricsService: self.syncedLyricsService
181+
)
182+
if self.playerService.miniPlayerMode == .switchFromMainWindow {
183+
self.hideMainWindow()
184+
}
185+
} else {
186+
MiniPlayerWindowController.shared.close()
187+
if self.playerService.consumeMiniPlayerMainWindowRestoreRequest() {
188+
self.showMainWindow()
189+
}
190+
}
191+
}
192+
.onChange(of: self.playerService.miniPlayerPanel) { _, _ in
193+
MiniPlayerWindowController.shared.syncWindowState()
194+
}
195+
.onChange(of: self.settings.keepMiniPlayerOnTop) { _, _ in
196+
MiniPlayerWindowController.shared.syncWindowState()
197+
}
174198
}
175199
}
176200

@@ -314,6 +338,19 @@ struct KasetApp: App {
314338

315339
// Window menu - show main window
316340
CommandGroup(after: .windowArrangement) {
341+
Button("Switch to Mini Player") {
342+
if self.playerService.isMiniPlayerVisible,
343+
self.playerService.miniPlayerMode == .switchFromMainWindow
344+
{
345+
_ = self.playerService.closeMiniPlayer()
346+
} else {
347+
self.playerService.openMiniPlayer(mode: .switchFromMainWindow)
348+
}
349+
}
350+
.keyboardShortcut("m", modifiers: [.command, .shift])
351+
352+
Divider()
353+
317354
Button("Kaset") {
318355
self.showMainWindow()
319356
}
@@ -332,20 +369,61 @@ struct KasetApp: App {
332369

333370
/// Shows the main window.
334371
private func showMainWindow() {
372+
guard !self.focusExistingMainWindow() else { return }
373+
374+
self.openWindow(id: "main")
375+
NSApplication.shared.activate(ignoringOtherApps: true)
376+
377+
Task { @MainActor in
378+
try? await Task.sleep(for: .milliseconds(100))
379+
_ = self.focusExistingMainWindow()
380+
}
381+
}
382+
383+
@discardableResult
384+
private func focusExistingMainWindow() -> Bool {
335385
// Find and show the main window
336386
for window in NSApplication.shared.windows where window.frameAutosaveName == "KasetMainWindow" {
337387
window.makeKeyAndOrderFront(nil)
338388
NSApplication.shared.activate(ignoringOtherApps: true)
339-
return
389+
return true
390+
}
391+
392+
for window in NSApplication.shared.windows where window.title == "Kaset" && !Self.isAuxiliaryPlayerWindow(window) {
393+
window.makeKeyAndOrderFront(nil)
394+
NSApplication.shared.activate(ignoringOtherApps: true)
395+
return true
340396
}
341397

342-
// Fallback: find any main-capable window that's not the video window
398+
// Fallback: find any main-capable window that's not an auxiliary player window.
343399
for window in NSApplication.shared.windows where window.canBecomeMain {
344-
if window.identifier?.rawValue == AccessibilityID.VideoWindow.container {
400+
if Self.isAuxiliaryPlayerWindow(window) {
345401
continue
346402
}
347403
window.makeKeyAndOrderFront(nil)
348404
NSApplication.shared.activate(ignoringOtherApps: true)
405+
return true
406+
}
407+
408+
return false
409+
}
410+
411+
private static func isAuxiliaryPlayerWindow(_ window: NSWindow) -> Bool {
412+
AccessibilityID.isAuxiliaryPlayerWindowIdentifier(window.identifier?.rawValue)
413+
}
414+
415+
/// Hides the main window while keeping playback and auxiliary windows alive.
416+
private func hideMainWindow() {
417+
for window in NSApplication.shared.windows where window.frameAutosaveName == "KasetMainWindow" {
418+
window.orderOut(nil)
419+
return
420+
}
421+
422+
for window in NSApplication.shared.windows where window.canBecomeMain {
423+
if Self.isAuxiliaryPlayerWindow(window) {
424+
continue
425+
}
426+
window.orderOut(nil)
349427
return
350428
}
351429
}

0 commit comments

Comments
 (0)