diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 1a73dbe..164d2a0 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -1249,6 +1249,14 @@ struct ContentView: View { .foregroundStyle(timerManager.timerColor) .padding(.trailing, 8) .opacity((coordinator.expandingView.show && coordinator.expandingView.type == .timer && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0) + } else if Defaults[.showSongMetadataInClosedNotch] && isNonNotchScreen && !musicManager.songTitle.isEmpty { + MarqueeText( + .constant("\(musicManager.songTitle) • \(musicManager.artistName)"), + textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray, + minDuration: 3, + frameWidth: max(0, effectiveCenterWidth - 16) + ) + .padding(.horizontal, 8) } } .clipped() diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 3f26472..ee2d4af 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2551,7 +2551,6 @@ }, "±10 Seconds" : { "comment" : "Display name for the \"±10 Seconds\" music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -3555,7 +3554,6 @@ }, "Accent" : { "comment" : "Display name for the caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -4179,7 +4177,6 @@ }, "Adaptive gradient" : { "comment" : "Display name for the timer icon color mode: adaptive gradient.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -6299,7 +6296,6 @@ } }, "Amazon Music" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -6738,7 +6734,6 @@ }, "Apple Music" : { "comment" : "Media controller type for Apple Music", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -7796,7 +7791,6 @@ }, "Bar" : { "comment" : "Name of the timer progress style that shows a horizontal bar.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -12933,6 +12927,10 @@ } } }, + "Compact" : { + "comment" : "Title for the compact battery notification style.", + "isCommentAutoGenerated" : true + }, "Compact keeps the alert inline. Standard uses the taller full-charge HUD with the charging animation." : { }, @@ -16517,7 +16515,6 @@ }, "Digital" : { "comment" : "The display name for the \"Digital\" reminder presentation style.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -23770,7 +23767,6 @@ }, "Green" : { "comment" : "Display name for the green caps lock indicator tint mode.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -34015,7 +34011,6 @@ }, "Now Playing" : { "comment" : "Name of the media controller type for the \"Now Playing\" option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -34928,7 +34923,6 @@ }, "Panel" : { "comment" : "Display name for the clipboard display mode \"Panel\".", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -39206,7 +39200,6 @@ }, "Ring" : { "comment" : "The name of the timer progress style that shows a ring shape.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -40959,7 +40952,6 @@ }, "Separate Tab" : { "comment" : "Display name for the clipboard display mode option to open the clipboard in a separate tab.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -43612,6 +43604,9 @@ } } } + }, + "Show song title and artist on non-notch displays" : { + }, "Show start time after event begins" : { "localizations" : { @@ -43803,7 +43798,6 @@ }, "Shows screen assistant as a dropdown attached to the AI button" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant as a dropdown attached to the AI button.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -43834,7 +43828,6 @@ }, "Shows screen assistant in a floating panel near the notch" : { "comment" : "Description of the screen assistant display mode option that shows the screen assistant in a floating panel near the notch.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -44124,7 +44117,6 @@ }, "Skip forward or backward by ten seconds" : { "comment" : "Description of a music skip behavior option.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -44900,7 +44892,6 @@ }, "Spotify" : { "comment" : "Spotify media controller type.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -45181,7 +45172,6 @@ }, "Standard previous/next track controls" : { "comment" : "Description of the music skip behavior option to use standard previous/next track controls.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -48514,7 +48504,6 @@ }, "Track Skip" : { "comment" : "Display name for the \"Track Skip\" music skip behavior.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { @@ -52390,7 +52379,6 @@ }, "Youtube Music" : { "comment" : "Spotify music service.", - "extractionState" : "stale", "isCommentAutoGenerated" : true, "localizations" : { "ko" : { diff --git a/DynamicIsland/components/Live activities/MarqueeTextView.swift b/DynamicIsland/components/Live activities/MarqueeTextView.swift index 2a39706..a20be48 100644 --- a/DynamicIsland/components/Live activities/MarqueeTextView.swift +++ b/DynamicIsland/components/Live activities/MarqueeTextView.swift @@ -46,9 +46,9 @@ struct MarqueeText: View { let minDuration: Double let frameWidth: CGFloat - @State private var animate = false @State private var textSize: CGSize = .zero @State private var offset: CGFloat = 0 + @State private var isAnimating: Bool = false init(_ text: Binding, font: Font = .body, nsFont: NSFont.TextStyle = .body, textColor: Color = .primary, backgroundColor: Color = .clear, minDuration: Double = 3.0, frameWidth: CGFloat = 200) { _text = text @@ -65,55 +65,80 @@ struct MarqueeText: View { } var body: some View { - GeometryReader { geometry in - ZStack(alignment: .leading) { - HStack(spacing: 20) { - Text(text) - Text(text) - .opacity(needsScrolling ? 1 : 0) - } - .id(text) - .font(font) - .foregroundColor(textColor) - .fixedSize(horizontal: true, vertical: false) - .offset(x: self.animate ? offset : 0) - .animation( - self.animate ? - .linear(duration: Double(textSize.width / 30)) - .delay(minDuration) - .repeatForever(autoreverses: false) : .none, - value: self.animate - ) - .background(backgroundColor) - .modifier(MeasureSizeModifier()) - .onPreferenceChange(SizePreferenceKey.self) { size in - self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize) - self.animate = false - self.offset = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02){ - if needsScrolling { - self.animate = true - self.offset = -(textSize.width + 20) - } - } - } - .onChange(of: text) { _, _ in - // Reset animation when text changes - self.animate = false - self.offset = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.02){ - if needsScrolling { - self.animate = true - self.offset = -(textSize.width + 20) - } - } - } + ZStack(alignment: .leading) { + HStack(spacing: 20) { + Text(text) + Text(text) + .opacity(needsScrolling ? 1 : 0) + } + .id(text) + .font(font) + .foregroundColor(textColor) + .fixedSize(horizontal: true, vertical: false) + .offset(x: offset) + .background(backgroundColor) + .modifier(MeasureSizeModifier()) + .onPreferenceChange(SizePreferenceKey.self) { size in + self.textSize = CGSize(width: size.width / 2, height: NSFont.preferredFont(forTextStyle: nsFont).pointSize) + resetAndStart() + } + .onChange(of: text) { _, _ in + resetAndStart() + } + .onAppear { + resetAndStart() + } + .onDisappear { + isAnimating = false } - .frame(width: frameWidth, alignment: .leading) - .clipped() } + .frame(width: frameWidth, alignment: .leading) + .clipped() .frame(height: textSize.height * 1.3) + } + + private func resetAndStart() { + isAnimating = false + withAnimation(.none) { + offset = 0 + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if needsScrolling { + isAnimating = true + startAnimationLoop() + } + } + } + + private func startAnimationLoop() { + guard isAnimating && needsScrolling else { return } + + // Duration based on speed (approx 30 pts per second) + let duration = Double(textSize.width / 30) + + // 1. Initial/Restart Pause + DispatchQueue.main.asyncAfter(deadline: .now() + minDuration) { + guard isAnimating else { return } + + // 2. Linear Move + withAnimation(.linear(duration: duration)) { + offset = -(textSize.width + 20) // Text width + spacing + } + + // 3. Wait for move to finish + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + guard isAnimating else { return } + + // 4. Snap back instantly + withAnimation(.none) { + offset = 0 + } + + // 5. Repeat + startAnimationLoop() + } + } } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index b68b227..4c23d3d 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2937,6 +2937,11 @@ struct Media: View { } } } + + Defaults.Toggle(key: .showSongMetadataInClosedNotch) { + Text("Show song title and artist on non-notch displays") + } + .settingsHighlight(id: highlightID("Show song title and artist in closed notch")) } header: { Text("Media playback live activity") } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 9d8662f..6e557b7 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -1332,4 +1332,5 @@ extension Defaults.Keys { let fallback = MusicAuxiliaryControl.alternative(excluding: current) Defaults[.musicAuxRightControl] = fallback } + static let showSongMetadataInClosedNotch = Key("showSongMetadataInClosedNotch", default: false) }