add an option to see the title and artist name on non notch display

This commit is contained in:
delli 2026-04-29 16:44:31 +02:00
commit 2524bc13c3
6 changed files with 92 additions and 53 deletions

View file

@ -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<String>, 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()
}
}
}
}