diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index 02e0162..3d4e01b 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -454,12 +454,12 @@ AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; - DEVELOPMENT_TEAM = 9Y64TRM77N; + DEVELOPMENT_TEAM = RPC964BP8A; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -526,13 +526,13 @@ AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; DEVELOPMENT_TEAM = ""; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 9Y64TRM77N; + "DEVELOPMENT_TEAM[sdk=macosx*]" = RPC964BP8A; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 1a40e0d..b2cbf86 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -1139,6 +1139,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 7d726a5..00deefe 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -5622,9 +5622,6 @@ } } } - }, - "Amazon Music" : { - }, "Animation" : { "extractionState" : "stale", @@ -38036,6 +38033,9 @@ } } } + }, + "Show song title and artist on non-notch displays" : { + }, "Show start time after event begins" : { "localizations" : { 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 2a4464b..27c6d1a 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2633,6 +2633,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 198cd85..a440805 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -1291,4 +1291,5 @@ extension Defaults.Keys { let fallback = MusicAuxiliaryControl.alternative(excluding: current) Defaults[.musicAuxRightControl] = fallback } + static let showSongMetadataInClosedNotch = Key("showSongMetadataInClosedNotch", default: false) }