From e6aa95b65693da280de2401da5e7c92a82302116 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 9 Jul 2026 23:52:43 +0200 Subject: [PATCH] Handle live videos in mini player bar Live streams have no meaningful duration, so the mini player's bottom progress line collapsed to nothing (or showed a misleading partial fill on DVR streams). Hide the line for live playback and show a red dot + LIVE indicator next to the author name instead, matching the expanded player controls. Applies to both the macOS capsule and iOS overlay layouts. --- Yattee/Views/Player/MiniPlayerView.swift | 78 ++++++++++++++++-------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index c22c73ca..b67d85e9 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -236,13 +236,7 @@ struct MiniPlayerView: View { foregroundStyle: .primary ) - if let authorName = currentVideo?.author.name, !authorName.isEmpty { - MarqueeText( - text: authorName, - font: .caption, - foregroundStyle: .secondary - ) - } + authorLine(font: .caption) } .contentShape(Rectangle()) .onTapGesture { @@ -265,12 +259,7 @@ struct MiniPlayerView: View { .padding(.vertical, 8) .background(.ultraThinMaterial) .overlay(alignment: .bottom) { - GeometryReader { geo in - Rectangle() - .fill(.red) - .frame(width: geo.size.width * (playerState?.progress ?? 0), height: 2) - } - .frame(height: 2) + progressLine } } @@ -299,13 +288,7 @@ struct MiniPlayerView: View { foregroundStyle: .primary ) - if let authorName = currentVideo?.author.name, !authorName.isEmpty { - MarqueeText( - text: authorName, - font: .caption, - foregroundStyle: .secondary - ) - } + authorLine(font: .caption) } .contentShape(Rectangle()) .onTapGesture { @@ -320,12 +303,7 @@ struct MiniPlayerView: View { .frame(maxWidth: 420) .glassBackground(.regular, in: .capsule, fallback: .regularMaterial) .overlay(alignment: .bottom) { - GeometryReader { geo in - Rectangle() - .fill(.red) - .frame(width: geo.size.width * (playerState?.progress ?? 0), height: 2) - } - .frame(height: 2) + progressLine } .clipShape(Capsule()) .shadow(color: .black.opacity(0.25), radius: 8, y: 4) @@ -334,6 +312,54 @@ struct MiniPlayerView: View { // MARK: - Shared Components + /// Author line with a red LIVE indicator for live streams. + /// MarqueeText draws its text at the top of a fixed-height frame, so the badge + /// is top-aligned with it (dot stays centered against the LIVE text only). + @ViewBuilder + private func authorLine(font: Font) -> some View { + let authorName = currentVideo?.author.name ?? "" + if playerState?.isLive == true { + HStack(alignment: .top, spacing: 4) { + HStack(spacing: 4) { + Circle() + .fill(.red) + .frame(width: 6, height: 6) + Text(String(localized: "player.live")) + .font(font.weight(.semibold)) + .foregroundStyle(.red) + } + .fixedSize() + + if !authorName.isEmpty { + MarqueeText( + text: authorName, + font: font, + foregroundStyle: .secondary + ) + } + } + } else if !authorName.isEmpty { + MarqueeText( + text: authorName, + font: font, + foregroundStyle: .secondary + ) + } + } + + /// Bottom progress line; hidden for live streams where progress is meaningless. + @ViewBuilder + private var progressLine: some View { + if playerState?.isLive != true { + GeometryReader { geo in + Rectangle() + .fill(.red) + .frame(width: geo.size.width * (playerState?.progress ?? 0), height: 2) + } + .frame(height: 2) + } + } + /// Video preview that shows live video when available, or thumbnail as fallback. @ViewBuilder private var videoPreviewView: some View {