Skip to content

Implement Apple Music-style mini player#246

Open
ComicBit wants to merge 12 commits into
sozercan:mainfrom
ComicBit:mini-player
Open

Implement Apple Music-style mini player#246
ComicBit wants to merge 12 commits into
sozercan:mainfrom
ComicBit:mini-player

Conversation

@ComicBit
Copy link
Copy Markdown

@ComicBit ComicBit commented May 7, 2026

Description

Adds a native Apple Music-style mini player for Kaset. The mini player is a separate floating macOS window that reuses the existing PlayerService, hidden playback WebView, artwork loading, lyrics service, queue state, and transport controls instead of embedding YouTube Music UI.

The implementation focuses on the Apple Music interaction model: Switch to Mini Player hides the main Kaset window, closing or toggling back restores it, Dock activation keeps focus on the mini player while switched, and the mini player supports compact, square artwork, and lyrics-expanded presentations.

AI Prompt (Optional)

AI Prompt Used
Build a native macOS Apple Music-style mini player for Kaset.

Requirements:
- Reuse Kaset's existing PlayerService, hidden playback WebView, artwork loading, lyrics service, queue state, and transport controls.
- Add a MiniPlayerWindowController with stable window lifetime, frame autosave, cleanup on close, accessibility identifier, and keep-on-top support.
- Add mini-player state to PlayerService for visibility, switch-mode restore behavior, compact/expanded state, and toggling.
- Add Window menu support for Switch to Mini Player using Apple Music-style shortcuts while preserving standard macOS shortcuts.
- Add a player-bar trigger for the mini player.
- Add a SettingsManager boolean and General Settings checkbox for Keep Mini Player on Top.
- Implement native SwiftUI mini-player UI rather than showing YouTube Music UI.
- Match Apple Music behavior and layout closely:
  - Main window disappears when switching to mini player and reappears when switching back.
  - Use a minimal compact mini player with hover-only window chrome and extra controls.
  - Support a square artwork mode where the current cover art fills the whole frame.
  - Hide playback controls in square artwork mode unless hovered.
  - Add a bottom blur/gradient under hover controls so controls remain readable over artwork.
  - Automatically snap from square artwork mode back to compact mini-mini when the window is resized much smaller.
  - Do not add a manual collapse control for returning from square artwork mode.
  - Lyrics can be toggled on and off, and the lyrics icon must show an active red highlighted state.
  - Remove the separate auxiliary Mini Player command so only Switch to Mini Player remains.
  - Prevent Dock icon activation from reopening the main window while switched to mini player.
- Add tests for PlayerService mini-player state transitions and keep-on-top persistence.
- Update keyboard shortcut documentation.
- Add support for unsigned DMG packaging through KASET_SIGNING=unsigned.
- Do not add third-party frameworks.
- Follow AGENTS.md, Swift 6, SwiftUI, macOS 26, Swift concurrency, and project style rules.
- Do not run UI tests without explicit human permission.

Validation expected:
- swift build
- swift test --filter PlayerServiceTests
- swiftlint --strict
- swiftformat .
- Build refreshed unsigned DMG

AI Tool: ChatGPT / Codex

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • UI/UX improvement
  • Refactoring (no functional changes)
  • Test update
  • Build/CI configuration

Related Issues

N/A

Changes Made

  • Added MiniPlayerWindowController for the floating native mini-player window, including lifecycle cleanup, frame persistence, accessibility identifier, and keep-on-top window level support.
  • Added mini-player state and helpers to PlayerService for opening, closing, toggling, switch-mode restore behavior, compact/expanded panel state, and one-shot main-window restore requests.
  • Added Switch to Mini Player window command and removed the separate auxiliary Mini Player command so there is only one mini-player entry point.
  • Added a player-bar mini-player trigger.
  • Added Keep Mini Player on Top setting and General Settings checkbox.
  • Implemented native SwiftUI mini-player layouts:
    • compact mini-mini mode
    • square artwork mode with full-frame artwork background
    • lyrics-expanded mode
    • hover-only chrome and controls for the square artwork presentation
    • bottom fade/blur behind hover controls
    • active red lyrics state
    • toggle-off lyrics behavior
  • Added automatic resize snapping so shrinking the square artwork player switches back to compact mini-mini mode.
  • Updated app activation behavior so clicking the Dock icon does not reopen the main window while the app is switched to mini-player mode.
  • Reused the existing lyrics rendering/loading path so main-window lyrics and mini-player lyrics do not diverge.
  • Added accessibility identifiers for future UI tests.
  • Added unit tests for mini-player state transitions and keep-on-top persistence.
  • Updated docs/keyboard-shortcuts.md.
  • Added unsigned app/DMG packaging support with KASET_SIGNING=unsigned.

Testing

  • Unit tests pass (swift test --filter PlayerServiceTests)
  • Manual testing performed
  • UI tested on macOS 26+

Also verified with:

  • swift build
  • swiftlint --strict
  • swiftformat .
  • refreshed unsigned DMG build

UI tests were not run because the repository requires explicit human permission before launching UI tests.

Checklist

  • My code follows the project's style guidelines
  • I have run swiftlint --strict && swiftformat .
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally
  • I have updated documentation if needed
  • I have checked for any performance implications
  • My changes generate no new warnings

Screenshots

Screenshot 2026-05-08 at 00 02 43 Screenshot 2026-05-08 at 00 03 03

Additional Notes

The implementation intentionally keeps playback in the existing singleton hidden WebView for DRM compatibility and builds the mini-player as native SwiftUI window chrome and controls on top of current playback state.

Copilot AI review requested due to automatic review settings May 7, 2026 22:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a native Apple Music–style Mini Player window and integrates it with app/window state, settings, keyboard/menu commands, and tests.

Changes:

  • Introduces a new MiniPlayerWindow UI and MiniPlayerWindowController to manage a floating window with compact/expanded/lyrics layouts.
  • Extends PlayerService and PlayerServiceProtocol with mini-player visibility/mode/panel state and restore-intent handling, plus new tests/mocks.
  • Adds a “Keep Mini Player on Top” setting and a ⇧⌘M keyboard shortcut/menu item to switch to the Mini Player.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
docs/keyboard-shortcuts.md Documents new ⇧⌘M shortcut for Mini Player.
Tests/KasetTests/PlayerServiceTests.swift Adds tests for mini-player visibility/mode/panel and keep-on-top persistence.
Tests/KasetTests/Helpers/MockPlayerService.swift Updates mock to support new mini-player protocol surface.
Sources/Kaset/Views/PlayerBar.swift Adds toolbar button to switch/toggle Mini Player.
Sources/Kaset/Views/MiniPlayerViews.swift Adds Mini Player window UI (controls, lyrics/queue panes, hover chrome).
Sources/Kaset/Views/LyricsView.swift Makes header and width configurable for reuse inside Mini Player.
Sources/Kaset/Views/GeneralSettingsView.swift Adds “Keep Mini Player on Top” toggle.
Sources/Kaset/Utilities/AccessibilityIdentifiers.swift Adds identifiers for Mini Player UI and PlayerBar button.
Sources/Kaset/Services/SettingsManager.swift Persists keepMiniPlayerOnTop setting.
Sources/Kaset/Services/Protocols.swift Extends PlayerServiceProtocol with mini-player state and actions.
Sources/Kaset/Services/Player/PlayerService.swift Adds mini-player state types and observable properties.
Sources/Kaset/Services/Player/PlayerService+PlaybackControls.swift Implements open/toggle/close and panel toggling behavior.
Sources/Kaset/MiniPlayerWindowController.swift New controller managing the floating mini-player NSWindow lifecycle, sizing, and z-level.
Sources/Kaset/KasetApp.swift Wires mini-player state changes to window show/hide and adds menu command + shortcut.
Sources/Kaset/AppDelegate.swift Ensures Dock/reopen actions foreground mini player when switched.
Scripts/build-app.sh Exits early when building unsigned, skipping signing steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/MiniPlayerWindowController.swift Outdated
Comment thread Sources/Kaset/MiniPlayerWindowController.swift
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift
Comment thread Tests/KasetTests/PlayerServiceTests.swift Outdated
ComicBit and others added 2 commits May 8, 2026 00:15
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@ComicBit
Copy link
Copy Markdown
Author

ComicBit commented May 7, 2026

Addressed the Copilot review feedback in 9f093ea.

What changed:

  • Added a distinct miniPlayer.lyricsView accessibility identifier for the lyrics content view, keeping miniPlayer.lyrics for the button.
  • Changed the mini-player queue ForEach identity to \.offset so duplicate videoId values do not collide.
  • Removed the direct main-window restore call from MiniPlayerWindowController; KasetApp is now the single restore authority through the existing restore-request path.
  • Cleaned up both NSWindow.willCloseNotification and NSWindow.didEndLiveResizeNotification observers during mini-player cleanup.
  • Added accessibility identifiers for the mini-player traffic controls, including the expand control.
  • Exposed SettingsManager.Keys internally and updated tests to use SettingsManager.Keys.keepMiniPlayerOnTop instead of a hard-coded string.

Validation:

  • swiftformat .
  • swift test --filter PlayerServiceTests
  • swift build
  • swiftlint --strict

@ComicBit
Copy link
Copy Markdown
Author

ComicBit commented May 8, 2026

@copilot what's left for merging? Is it a manual process?

@ComicBit
Copy link
Copy Markdown
Author

@sozercan could you please check this PR?

@sozercan
Copy link
Copy Markdown
Owner

@ComicBit thanks for the pr! i will review when i can

# Conflicts:
#	Sources/Kaset/Views/GeneralSettingsView.swift
Copilot AI review requested due to automatic review settings May 16, 2026 05:43
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 16/16 changed files
  • Comments generated: 5

Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
@ComicBit
Copy link
Copy Markdown
Author

Addressed the latest review feedback in 392c558.

Changes:

  • Replaced mini-player hover fade animations with the shared AppAnimation.quick constant.
  • Made the mini-player library menu label state-aware: Add to Library / Remove from Library.
  • Made the dislike menu label state-aware: Dislike / Remove Dislike.

Verification:

  • swiftformat .
  • swift test --filter PlayerServiceTests
  • swift build
  • swiftlint --strict
  • git diff --check

Copilot AI review requested due to automatic review settings May 20, 2026 01:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 4

Comment thread Tests/KasetTests/Helpers/MockPlayerService.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Comment thread Sources/Kaset/KasetApp.swift Outdated
Comment thread Sources/Kaset/Views/MiniPlayerViews.swift Outdated
Copilot AI review requested due to automatic review settings May 20, 2026 05:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 1

Comment on lines +241 to +244
private func isAuxiliaryPlayerWindow(_ window: NSWindow) -> Bool {
window.identifier?.rawValue == AccessibilityID.VideoWindow.container ||
window.identifier?.rawValue == AccessibilityID.MiniPlayer.container
}
Copilot AI review requested due to automatic review settings May 20, 2026 06:14
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 17/17 changed files
  • Comments generated: 2

Comment on lines +101 to +114
let restoreMainWindow = self.restoreMainWindow
let shouldRestoreMainWindow = self.playerService?.closeMiniPlayer() ?? false
self.close()

if shouldRestoreMainWindow {
restoreMainWindow?()
}
}

func returnToMainWindowFromUserAction() {
let restoreMainWindow = self.restoreMainWindow
_ = self.playerService?.closeMiniPlayer(restoringMainWindow: true) ?? false
self.close()
restoreMainWindow?()
static let lyricsView = "miniPlayer.lyricsView"
static let queueButton = "miniPlayer.queue"
static let airplayButton = "miniPlayer.airplay"
static let volumeSlider = "miniPlayer.volumeSlider"
@ComicBit
Copy link
Copy Markdown
Author

Addressed the new review comments in 36a5272.

Changes:

  • Centralized auxiliary player window detection through AccessibilityID.isAuxiliaryPlayerWindowIdentifier while preserving the newer main-window fallback logic.
  • Removed the direct mini-player main-window restore callback path so KasetApp's restore request consumer remains the single restore authority.
  • Renamed the mini-player mute control accessibility identifier from volumeSlider to volumeButton and updated call sites.

Verification:

  • swiftformat .
  • swift build
  • swift test --filter PlayerServiceTests
  • swiftlint --strict
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants