Nothing Playing

This commit is contained in:
Hariharan Mudaliar 2026-03-03 12:25:02 +05:30
commit b46d88c79a
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
5 changed files with 118 additions and 60 deletions

View file

@ -17793,6 +17793,10 @@
} }
} }
}, },
"Nothing Playing" : {
"comment" : "A label displayed when there is nothing playing in the music player.",
"isCommentAutoGenerated" : true
},
"Notify before" : { "Notify before" : {
"comment" : "A label describing the slider that allows the user to set a reminder lead time.", "comment" : "A label describing the slider that allows the user to set a reminder lead time.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,

View file

@ -111,7 +111,7 @@ struct LockScreenMusicPanel: View {
} }
var body: some View { var body: some View {
if isActive { if isActive && musicManager.hasActiveSession {
panelContent panelContent
} else { } else {
Color.clear Color.clear

View file

@ -45,72 +45,98 @@ struct MinimalisticMusicPlayerView: View {
private let skipMagnitude: CGFloat = 8 private let skipMagnitude: CGFloat = 8
var body: some View { var body: some View {
VStack(spacing: 0) { if !musicManager.hasActiveSession {
// Header area with album art (matching DynamicIslandHeader height of 24pt) // Nothing playing state
GeometryReader { headerGeo in VStack(spacing: 0) {
let albumArtWidth: CGFloat = 50 Spacer(minLength: 0)
let spacing: CGFloat = 10
let visualizerWidth: CGFloat = useMusicVisualizer ? 24 : 0 VStack(spacing: 8) {
let textWidth = max(0, headerGeo.size.width - albumArtWidth - spacing - (useMusicVisualizer ? (visualizerWidth + spacing) : 0)) Image(systemName: "music.note.slash")
HStack(alignment: .center, spacing: spacing) { .font(.system(size: 24, weight: .light))
MinimalisticAlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace) .foregroundColor(.gray)
.frame(width: albumArtWidth, height: albumArtWidth) Text("Nothing Playing")
.font(.system(size: 12, weight: .medium))
.foregroundColor(.gray)
}
Spacer(minLength: 0)
timerCountdownSection
reminderList
}
.padding(.horizontal, 12)
.frame(maxWidth: .infinity)
.frame(height: calculateDynamicHeight())
.animation(.smooth(duration: 0.3), value: dynamicHeightSignature)
} else {
VStack(spacing: 0) {
// Header area with album art (matching DynamicIslandHeader height of 24pt)
GeometryReader { headerGeo in
let albumArtWidth: CGFloat = 50
let spacing: CGFloat = 10
let visualizerWidth: CGFloat = useMusicVisualizer ? 24 : 0
let textWidth = max(0, headerGeo.size.width - albumArtWidth - spacing - (useMusicVisualizer ? (visualizerWidth + spacing) : 0))
HStack(alignment: .center, spacing: spacing) {
MinimalisticAlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace)
.frame(width: albumArtWidth, height: albumArtWidth)
VStack(alignment: .leading, spacing: 1) {
if !musicManager.songTitle.isEmpty {
MarqueeText(
$musicManager.songTitle,
font: .system(size: 12, weight: .semibold),
nsFont: .subheadline,
textColor: .white,
frameWidth: textWidth
)
}
Text(musicManager.artistName)
.font(.system(size: 10, weight: .regular))
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray)
.lineLimit(1)
VStack(alignment: .leading, spacing: 1) {
if !musicManager.songTitle.isEmpty {
MarqueeText(
$musicManager.songTitle,
font: .system(size: 12, weight: .semibold),
nsFont: .subheadline,
textColor: .white,
frameWidth: textWidth
)
} }
.frame(width: textWidth, alignment: .leading)
Text(musicManager.artistName) if useMusicVisualizer {
.font(.system(size: 10, weight: .regular)) visualizer
.foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray) .frame(width: visualizerWidth)
.lineLimit(1) }
}
.frame(width: textWidth, alignment: .leading)
if useMusicVisualizer {
visualizer
.frame(width: visualizerWidth)
} }
} }
} .frame(height: 50)
.frame(height: 50)
// Compact progress bar
// Compact progress bar progressBar
progressBar .padding(.top, 6)
.padding(.top, 6)
// Compact playback controls
// Compact playback controls if shouldShowControlHUDRow {
if shouldShowControlHUDRow { controlHUDRow
controlHUDRow .padding(.top, 4)
.padding(.top, 4) } else {
} else { playbackControls
playbackControls .padding(.top, 4)
.padding(.top, 4) }
}
if enableLyrics { if enableLyrics {
lyricsView lyricsView
.padding(.top, 10) .padding(.top, 10)
}
timerCountdownSection
reminderList
} }
.padding(.horizontal, 12)
timerCountdownSection .padding(.top, -15)
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
reminderList .frame(maxWidth: .infinity)
.frame(height: calculateDynamicHeight(), alignment: .top)
.animation(.smooth(duration: 0.3), value: dynamicHeightSignature)
} }
.padding(.horizontal, 12)
.padding(.top, -15)
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
.frame(maxWidth: .infinity)
.frame(height: calculateDynamicHeight(), alignment: .top)
.animation(.smooth(duration: 0.3), value: dynamicHeightSignature)
} }
// MARK: - TypingLyricView // MARK: - TypingLyricView

View file

@ -521,8 +521,14 @@ struct NotchHomeView: View {
@ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared @ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared
@ObservedObject private var musicManager = MusicManager.shared
@Default(.showStandardMediaControls) private var showStandardMediaControls @Default(.showStandardMediaControls) private var showStandardMediaControls
let albumArtNamespace: Namespace.ID let albumArtNamespace: Namespace.ID
/// Whether the music player should actively display (enabled AND has real content).
private var shouldShowMusicPlayer: Bool {
showStandardMediaControls && musicManager.hasActiveSession
}
var body: some View { var body: some View {
Group { Group {
@ -546,14 +552,14 @@ struct NotchHomeView: View {
} }
} else { } else {
// Normal mode: Show full music player with optional calendar and webcam // Normal mode: Show full music player with optional calendar and webcam
if showStandardMediaControls { if shouldShowMusicPlayer {
MusicPlayerView(albumArtNamespace: albumArtNamespace) MusicPlayerView(albumArtNamespace: albumArtNamespace)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
if Defaults[.showCalendar] { if Defaults[.showCalendar] {
Group { Group {
if showStandardMediaControls { if shouldShowMusicPlayer {
CalendarView() CalendarView()
} else { } else {
StandaloneCalendarView() StandaloneCalendarView()

View file

@ -78,6 +78,28 @@ class MusicManager: ObservableObject {
@Published var isPlaying = false @Published var isPlaying = false
@Published var album: String = "Self Love" @Published var album: String = "Self Love"
@Published var isPlayerIdle: Bool = true @Published var isPlayerIdle: Bool = true
/// Whether there is an active music session with real metadata.
/// Returns `false` only when the metadata is still placeholder/fallback defaults
/// (i.e. nothing has been played since app launch, or the controller returned
/// unknown/not-playing placeholders). Paused music with real metadata is still
/// considered an active session.
private static let placeholderTitles: Set<String> = [
"i'm handsome", "unknown", "not playing"
]
private static let placeholderArtists: Set<String> = [
"me", "unknown"
]
var hasActiveSession: Bool {
if isPlaying { return true }
let trimmedTitle = songTitle.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let trimmedArtist = artistName.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let hasRealTitle = !trimmedTitle.isEmpty && !Self.placeholderTitles.contains(trimmedTitle)
let hasRealArtist = !trimmedArtist.isEmpty && !Self.placeholderArtists.contains(trimmedArtist)
return hasRealTitle || hasRealArtist
}
@Published var animations: DynamicIslandAnimations = .init() @Published var animations: DynamicIslandAnimations = .init()
@Published var avgColor: NSColor = .white @Published var avgColor: NSColor = .white
@Published var bundleIdentifier: String? = nil @Published var bundleIdentifier: String? = nil