diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index df19672..f84bc5f 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -17793,6 +17793,10 @@ } } }, + "Nothing Playing" : { + "comment" : "A label displayed when there is nothing playing in the music player.", + "isCommentAutoGenerated" : true + }, "Notify before" : { "comment" : "A label describing the slider that allows the user to set a reminder lead time.", "isCommentAutoGenerated" : true, diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index e7ffd82..fcc7afc 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -111,7 +111,7 @@ struct LockScreenMusicPanel: View { } var body: some View { - if isActive { + if isActive && musicManager.hasActiveSession { panelContent } else { Color.clear diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift index 6fa3009..b62b94b 100644 --- a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -45,72 +45,98 @@ struct MinimalisticMusicPlayerView: View { private let skipMagnitude: CGFloat = 8 var body: some View { - 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) + if !musicManager.hasActiveSession { + // Nothing playing state + VStack(spacing: 0) { + Spacer(minLength: 0) + + VStack(spacing: 8) { + Image(systemName: "music.note.slash") + .font(.system(size: 24, weight: .light)) + .foregroundColor(.gray) + 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) - .font(.system(size: 10, weight: .regular)) - .foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: musicManager.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray) - .lineLimit(1) - - } - .frame(width: textWidth, alignment: .leading) - - if useMusicVisualizer { - visualizer - .frame(width: visualizerWidth) + if useMusicVisualizer { + visualizer + .frame(width: visualizerWidth) + } } } - } - .frame(height: 50) - - // Compact progress bar - progressBar - .padding(.top, 6) - - // Compact playback controls - if shouldShowControlHUDRow { - controlHUDRow - .padding(.top, 4) - } else { - playbackControls - .padding(.top, 4) - } + .frame(height: 50) + + // Compact progress bar + progressBar + .padding(.top, 6) + + // Compact playback controls + if shouldShowControlHUDRow { + controlHUDRow + .padding(.top, 4) + } else { + playbackControls + .padding(.top, 4) + } - if enableLyrics { - lyricsView - .padding(.top, 10) + if enableLyrics { + lyricsView + .padding(.top, 10) + } + + timerCountdownSection + + reminderList } - - timerCountdownSection - - reminderList + .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) } - .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 diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index 1b3be04..1fe1925 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -521,8 +521,14 @@ struct NotchHomeView: View { @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared + @ObservedObject private var musicManager = MusicManager.shared @Default(.showStandardMediaControls) private var showStandardMediaControls 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 { Group { @@ -546,14 +552,14 @@ struct NotchHomeView: View { } } else { // Normal mode: Show full music player with optional calendar and webcam - if showStandardMediaControls { + if shouldShowMusicPlayer { MusicPlayerView(albumArtNamespace: albumArtNamespace) .frame(maxWidth: .infinity, alignment: .leading) } if Defaults[.showCalendar] { Group { - if showStandardMediaControls { + if shouldShowMusicPlayer { CalendarView() } else { StandaloneCalendarView() diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 418e01b..188132f 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -78,6 +78,28 @@ class MusicManager: ObservableObject { @Published var isPlaying = false @Published var album: String = "Self Love" @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 = [ + "i'm handsome", "unknown", "not playing" + ] + private static let placeholderArtists: Set = [ + "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 avgColor: NSColor = .white @Published var bundleIdentifier: String? = nil