From 44f3cbb9f3ec9e96ca70fda50b69ce98a8b36093 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 28 Mar 2026 14:09:25 +0100 Subject: [PATCH] Deduplicate time formatting and clean up unused code Extract shared TimeInterval.formattedAsTimestamp replacing 8 identical formatTime/formattedTime implementations across player views. Remove unused currentTime parameter from GestureSeekPreviewView. Consolidate duplicated geometry math in MacOSControlBar into seekPreviewPosition(). --- .../Extensions/TimeInterval+Formatting.swift | 16 ++++ Yattee/Services/Player/PlayerState.swift | 20 +---- .../Player/ControlsSectionRenderer.swift | 15 +--- .../Gestures/GestureSeekPreviewView.swift | 2 - Yattee/Views/Player/PlayerControlsView.swift | 1 - Yattee/Views/Player/SeekPreviewView.swift | 15 +--- Yattee/Views/Player/SeekTimePreviewView.swift | 15 +--- .../Views/Player/macOS/MacOSControlBar.swift | 90 ++++++++----------- .../Player/tvOS/TVPlayerProgressBar.swift | 19 +--- .../Views/Player/tvOS/TVSeekPreviewView.swift | 15 +--- .../RemoteControl/RemoteControlView.swift | 17 +--- Yattee/Views/Video/VideoInfoView.swift | 15 +--- 12 files changed, 68 insertions(+), 172 deletions(-) create mode 100644 Yattee/Extensions/TimeInterval+Formatting.swift diff --git a/Yattee/Extensions/TimeInterval+Formatting.swift b/Yattee/Extensions/TimeInterval+Formatting.swift new file mode 100644 index 00000000..914a9c06 --- /dev/null +++ b/Yattee/Extensions/TimeInterval+Formatting.swift @@ -0,0 +1,16 @@ +import Foundation + +extension TimeInterval { + /// Formats as "M:SS" or "H:MM:SS" when hours > 0. + var formattedAsTimestamp: String { + let totalSeconds = Int(max(0, self)) + let hours = totalSeconds / 3600 + let minutes = (totalSeconds % 3600) / 60 + let seconds = totalSeconds % 60 + + if hours > 0 { + return String(format: "%d:%02d:%02d", hours, minutes, seconds) + } + return String(format: "%d:%02d", minutes, seconds) + } +} diff --git a/Yattee/Services/Player/PlayerState.swift b/Yattee/Services/Player/PlayerState.swift index 3cb9ab6a..b5b73ce1 100644 --- a/Yattee/Services/Player/PlayerState.swift +++ b/Yattee/Services/Player/PlayerState.swift @@ -254,7 +254,7 @@ final class PlayerState { if isLive { return "LIVE" } - return formatTime(currentTime) + return currentTime.formattedAsTimestamp } /// Formatted duration string. @@ -263,12 +263,12 @@ final class PlayerState { if isLive { return "LIVE" } - return formatTime(duration) + return duration.formattedAsTimestamp } /// Formatted remaining time string. var formattedRemainingTime: String { - "-" + formatTime(max(0, duration - currentTime)) + "-" + max(0, duration - currentTime).formattedAsTimestamp } // MARK: - Playback Settings @@ -625,20 +625,6 @@ final class PlayerState { queue.first } - // MARK: - Private - - private func formatTime(_ time: TimeInterval) -> String { - let totalSeconds = Int(time) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } } /// Represents a chapter in a video. diff --git a/Yattee/Views/Player/ControlsSectionRenderer.swift b/Yattee/Views/Player/ControlsSectionRenderer.swift index 29ae3b92..095c0653 100644 --- a/Yattee/Views/Player/ControlsSectionRenderer.swift +++ b/Yattee/Views/Player/ControlsSectionRenderer.swift @@ -687,20 +687,7 @@ struct ControlsSectionRenderer: View { private var formattedRemainingTime: String { let remaining = max(0, actions.playerState.duration - actions.playerState.currentTime) - return formatTime(remaining) - } - - private func formatTime(_ time: TimeInterval) -> String { - let totalSeconds = Int(time) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } + return remaining.formattedAsTimestamp } // MARK: - Volume Controls diff --git a/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift b/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift index 58b76430..3cd233be 100644 --- a/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift +++ b/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift @@ -12,7 +12,6 @@ import SwiftUI /// Shows only the storyboard thumbnail with timestamp overlay. struct GestureSeekPreviewView: View { let storyboard: Storyboard? - let currentTime: TimeInterval let seekTime: TimeInterval let duration: TimeInterval let storyboardService: StoryboardService @@ -70,7 +69,6 @@ struct GestureSeekPreviewView: View { GestureSeekPreviewView( storyboard: nil, - currentTime: 120, seekTime: 180, duration: 600, storyboardService: StoryboardService(), diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index 316e4e22..384280bc 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -1118,7 +1118,6 @@ struct PlayerControlsView: View { VStack { GestureSeekPreviewView( storyboard: playerState.preferredStoryboard, - currentTime: seekGestureStartTime, seekTime: seekGesturePreviewTime, duration: playerState.duration, storyboardService: StoryboardService.shared, diff --git a/Yattee/Views/Player/SeekPreviewView.swift b/Yattee/Views/Player/SeekPreviewView.swift index e4539cb6..ad1b83f3 100644 --- a/Yattee/Views/Player/SeekPreviewView.swift +++ b/Yattee/Views/Player/SeekPreviewView.swift @@ -55,19 +55,6 @@ struct SeekPreviewView: View { @State private var thumbnail: PlatformImage? @State private var loadTask: Task? - private var formattedTime: String { - let totalSeconds = Int(seekTime) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } - private let thumbnailWidth: CGFloat = 160 var body: some View { @@ -95,7 +82,7 @@ struct SeekPreviewView: View { .clipped() // Timestamp overlaid at bottom center - Text(formattedTime) + Text(seekTime.formattedAsTimestamp) .font(.caption) .fontWeight(.medium) .monospacedDigit() diff --git a/Yattee/Views/Player/SeekTimePreviewView.swift b/Yattee/Views/Player/SeekTimePreviewView.swift index 88d73a4f..6d0ffd8e 100644 --- a/Yattee/Views/Player/SeekTimePreviewView.swift +++ b/Yattee/Views/Player/SeekTimePreviewView.swift @@ -13,21 +13,8 @@ struct SeekTimePreviewView: View { let buttonBackground: ButtonBackgroundStyle let theme: ControlsTheme - private var formattedTime: String { - let totalSeconds = Int(seekTime) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } - var body: some View { - Text(formattedTime) + Text(seekTime.formattedAsTimestamp) .font(.system(size: 16, weight: .medium)) .monospacedDigit() .foregroundStyle(.white) diff --git a/Yattee/Views/Player/macOS/MacOSControlBar.swift b/Yattee/Views/Player/macOS/MacOSControlBar.swift index 4e6e3b13..70058139 100644 --- a/Yattee/Views/Player/macOS/MacOSControlBar.swift +++ b/Yattee/Views/Player/macOS/MacOSControlBar.swift @@ -95,61 +95,34 @@ struct MacOSControlBar: View { .shadow(color: .black.opacity(0.3), radius: 8, y: 4) // Seek preview overlay - positioned above the control bar .overlay(alignment: .bottom) { - if let storyboard = playerState.preferredStoryboard, - (isDragging || isHoveringProgress), - !playerState.isLive { + if (isDragging || isHoveringProgress), !playerState.isLive { GeometryReader { geometry in - let previewProgress = isDragging ? dragProgress : hoverProgress - // Progress bar spans full width minus padding and time labels - // Time labels ~50px each, spacing ~16px = ~116px total for labels - let horizontalPadding: CGFloat = 16 - let timeLabelWidth: CGFloat = 50 - let spacing: CGFloat = 8 - let progressBarOffset: CGFloat = horizontalPadding + timeLabelWidth + spacing - let progressBarWidth: CGFloat = geometry.size.width - (2 * horizontalPadding) - (2 * timeLabelWidth) - (2 * spacing) - let previewWidth: CGFloat = 176 // 160 + 16 padding - let xOffset = progressBarOffset + (progressBarWidth * previewProgress) - (previewWidth / 2) - let clampedX = max(0, min(geometry.size.width - previewWidth, xOffset)) + let pos = seekPreviewPosition(geometry: geometry, previewWidth: playerState.preferredStoryboard != nil ? 176 : 80) - let storyboardCenterX = clampedX + previewWidth / 2 + if let storyboard = playerState.preferredStoryboard { + seekPreviewView(storyboard: storyboard) + .offset(x: pos.clampedX, y: -150) - seekPreviewView(storyboard: storyboard) - .offset(x: clampedX, y: -150) + if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= pos.progress * playerState.duration }) { + ChapterCapsuleView(title: chapter.title, buttonBackground: .none) + .positioned(xTarget: pos.centerX, availableWidth: geometry.size.width) + .offset(y: -176) + } + } else { + SeekTimePreviewView( + seekTime: pos.progress * playerState.duration, + buttonBackground: .none, + theme: .dark + ) + .offset(x: pos.clampedX, y: -60) + .transition(.opacity.combined(with: .scale(scale: 0.9))) + .animation(.easeInOut(duration: 0.15), value: isDragging || isHoveringProgress) - if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= previewProgress * playerState.duration }) { - ChapterCapsuleView(title: chapter.title, buttonBackground: .none) - .positioned(xTarget: storyboardCenterX, availableWidth: geometry.size.width) - .offset(y: -176) - } - } - } else if (isDragging || isHoveringProgress), - !playerState.isLive { - GeometryReader { geometry in - let previewProgress = isDragging ? dragProgress : hoverProgress - let horizontalPadding: CGFloat = 16 - let timeLabelWidth: CGFloat = 50 - let spacing: CGFloat = 8 - let progressBarOffset: CGFloat = horizontalPadding + timeLabelWidth + spacing - let progressBarWidth: CGFloat = geometry.size.width - (2 * horizontalPadding) - (2 * timeLabelWidth) - (2 * spacing) - let previewWidth: CGFloat = 80 - let xOffset = progressBarOffset + (progressBarWidth * previewProgress) - (previewWidth / 2) - let clampedX = max(0, min(geometry.size.width - previewWidth, xOffset)) - - let timeCenterX = clampedX + previewWidth / 2 - - SeekTimePreviewView( - seekTime: previewProgress * playerState.duration, - buttonBackground: .none, - theme: .dark - ) - .offset(x: clampedX, y: -60) - .transition(.opacity.combined(with: .scale(scale: 0.9))) - .animation(.easeInOut(duration: 0.15), value: isDragging || isHoveringProgress) - - if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= previewProgress * playerState.duration }) { - ChapterCapsuleView(title: chapter.title, buttonBackground: .none) - .positioned(xTarget: timeCenterX, availableWidth: geometry.size.width) - .offset(y: -86) + if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= pos.progress * playerState.duration }) { + ChapterCapsuleView(title: chapter.title, buttonBackground: .none) + .positioned(xTarget: pos.centerX, availableWidth: geometry.size.width) + .offset(y: -86) + } } } } @@ -336,6 +309,21 @@ struct MacOSControlBar: View { .frame(height: 20) } + private func seekPreviewPosition( + geometry: GeometryProxy, + previewWidth: CGFloat + ) -> (progress: Double, clampedX: CGFloat, centerX: CGFloat) { + let previewProgress = isDragging ? dragProgress : hoverProgress + let horizontalPadding: CGFloat = 16 + let timeLabelWidth: CGFloat = 50 + let spacing: CGFloat = 8 + let progressBarOffset = horizontalPadding + timeLabelWidth + spacing + let progressBarWidth = geometry.size.width - (2 * horizontalPadding) - (2 * timeLabelWidth) - (2 * spacing) + let xOffset = progressBarOffset + (progressBarWidth * previewProgress) - (previewWidth / 2) + let clampedX = max(0, min(geometry.size.width - previewWidth, xOffset)) + return (previewProgress, clampedX, clampedX + previewWidth / 2) + } + @ViewBuilder private func seekPreviewView(storyboard: Storyboard) -> some View { let previewProgress = isDragging ? dragProgress : hoverProgress diff --git a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift index 24fe7c96..0ded841e 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift @@ -160,7 +160,7 @@ struct TVPlayerProgressBar: View { .foregroundStyle(.red) } } else { - Text(formatTime(displayTime)) + Text(displayTime.formattedAsTimestamp) .monospacedDigit() .font(.subheadline) .fontWeight(isScrubbing ? .semibold : .regular) @@ -184,7 +184,7 @@ struct TVPlayerProgressBar: View { // Remaining time (only for non-live) if !isLive { - Text("-\(formatTime(max(0, duration - displayTime)))") + Text("-\(max(0, duration - displayTime).formattedAsTimestamp)") .monospacedDigit() .font(.subheadline) .foregroundStyle(.white.opacity(0.7)) @@ -202,7 +202,7 @@ struct TVPlayerProgressBar: View { .transition(.scale.combined(with: .opacity)) } else { // Fallback when no storyboard available - Text(formatTime(scrubTime ?? currentTime)) + Text((scrubTime ?? currentTime).formattedAsTimestamp) .font(.system(size: 48, weight: .medium)) .monospacedDigit() .foregroundStyle(.white) @@ -329,19 +329,6 @@ struct TVPlayerProgressBar: View { } } - // MARK: - Formatting - - private func formatTime(_ time: TimeInterval) -> String { - let totalSeconds = Int(max(0, time)) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } - return String(format: "%d:%02d", minutes, seconds) - } } // MARK: - Pan Gesture View diff --git a/Yattee/Views/Player/tvOS/TVSeekPreviewView.swift b/Yattee/Views/Player/tvOS/TVSeekPreviewView.swift index aa3f3f75..08aa8e2e 100644 --- a/Yattee/Views/Player/tvOS/TVSeekPreviewView.swift +++ b/Yattee/Views/Player/tvOS/TVSeekPreviewView.swift @@ -23,19 +23,6 @@ struct TVSeekPreviewView: View { chapters.last { $0.startTime <= seekTime } } - private var formattedTime: String { - let totalSeconds = Int(seekTime) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } - private let thumbnailWidth: CGFloat = 320 var body: some View { @@ -69,7 +56,7 @@ struct TVSeekPreviewView: View { } // Timestamp overlaid at bottom center (larger for TV) - Text(formattedTime) + Text(seekTime.formattedAsTimestamp) .font(.system(size: 36, weight: .medium)) .monospacedDigit() .foregroundStyle(.white) diff --git a/Yattee/Views/RemoteControl/RemoteControlView.swift b/Yattee/Views/RemoteControl/RemoteControlView.swift index 7bffae18..2fa56525 100644 --- a/Yattee/Views/RemoteControl/RemoteControlView.swift +++ b/Yattee/Views/RemoteControl/RemoteControlView.swift @@ -427,9 +427,9 @@ struct RemoteControlView: View { #endif HStack { - Text(formatTime(displayCurrentTime)) + Text(displayCurrentTime.formattedAsTimestamp) Spacer() - Text("-" + formatTime(remoteState.duration - displayCurrentTime)) + Text("-" + (remoteState.duration - displayCurrentTime).formattedAsTimestamp) } .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) @@ -683,19 +683,6 @@ struct RemoteControlView: View { isConnected = false } - private func formatTime(_ time: TimeInterval) -> String { - let totalSeconds = Int(time) - let hours = totalSeconds / 3600 - let minutes = (totalSeconds % 3600) / 60 - let seconds = totalSeconds % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } - /// Returns the previous playback rate, or nil if at minimum. private func previousRate() -> PlaybackRate? { let allRates = PlaybackRate.allCases diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index a50b7833..5c03cdf7 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -238,24 +238,11 @@ struct VideoInfoView: View { let resumeAction = appEnvironment?.settingsManager.resumeAction ?? .continueWatching switch resumeAction { case .continueWatching, .ask: - return String(localized: "resume.action.continueAt \(formatTime(savedProgress))") + return String(localized: "resume.action.continueAt \(savedProgress.formattedAsTimestamp)") case .startFromBeginning: return String(localized: "video.context.play") } } - - /// Formats a time interval as MM:SS or H:MM:SS. - private func formatTime(_ time: TimeInterval) -> String { - let hours = Int(time) / 3600 - let minutes = (Int(time) % 3600) / 60 - let seconds = Int(time) % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } var body: some View { Group {