mirror of
https://github.com/yattee/yattee.git
synced 2026-04-10 01:26:57 +00:00
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().
This commit is contained in:
16
Yattee/Extensions/TimeInterval+Formatting.swift
Normal file
16
Yattee/Extensions/TimeInterval+Formatting.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -254,7 +254,7 @@ final class PlayerState {
|
|||||||
if isLive {
|
if isLive {
|
||||||
return "LIVE"
|
return "LIVE"
|
||||||
}
|
}
|
||||||
return formatTime(currentTime)
|
return currentTime.formattedAsTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formatted duration string.
|
/// Formatted duration string.
|
||||||
@@ -263,12 +263,12 @@ final class PlayerState {
|
|||||||
if isLive {
|
if isLive {
|
||||||
return "LIVE"
|
return "LIVE"
|
||||||
}
|
}
|
||||||
return formatTime(duration)
|
return duration.formattedAsTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formatted remaining time string.
|
/// Formatted remaining time string.
|
||||||
var formattedRemainingTime: String {
|
var formattedRemainingTime: String {
|
||||||
"-" + formatTime(max(0, duration - currentTime))
|
"-" + max(0, duration - currentTime).formattedAsTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Playback Settings
|
// MARK: - Playback Settings
|
||||||
@@ -625,20 +625,6 @@ final class PlayerState {
|
|||||||
queue.first
|
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.
|
/// Represents a chapter in a video.
|
||||||
|
|||||||
@@ -687,20 +687,7 @@ struct ControlsSectionRenderer: View {
|
|||||||
|
|
||||||
private var formattedRemainingTime: String {
|
private var formattedRemainingTime: String {
|
||||||
let remaining = max(0, actions.playerState.duration - actions.playerState.currentTime)
|
let remaining = max(0, actions.playerState.duration - actions.playerState.currentTime)
|
||||||
return formatTime(remaining)
|
return remaining.formattedAsTimestamp
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Volume Controls
|
// MARK: - Volume Controls
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import SwiftUI
|
|||||||
/// Shows only the storyboard thumbnail with timestamp overlay.
|
/// Shows only the storyboard thumbnail with timestamp overlay.
|
||||||
struct GestureSeekPreviewView: View {
|
struct GestureSeekPreviewView: View {
|
||||||
let storyboard: Storyboard?
|
let storyboard: Storyboard?
|
||||||
let currentTime: TimeInterval
|
|
||||||
let seekTime: TimeInterval
|
let seekTime: TimeInterval
|
||||||
let duration: TimeInterval
|
let duration: TimeInterval
|
||||||
let storyboardService: StoryboardService
|
let storyboardService: StoryboardService
|
||||||
@@ -70,7 +69,6 @@ struct GestureSeekPreviewView: View {
|
|||||||
|
|
||||||
GestureSeekPreviewView(
|
GestureSeekPreviewView(
|
||||||
storyboard: nil,
|
storyboard: nil,
|
||||||
currentTime: 120,
|
|
||||||
seekTime: 180,
|
seekTime: 180,
|
||||||
duration: 600,
|
duration: 600,
|
||||||
storyboardService: StoryboardService(),
|
storyboardService: StoryboardService(),
|
||||||
|
|||||||
@@ -1118,7 +1118,6 @@ struct PlayerControlsView: View {
|
|||||||
VStack {
|
VStack {
|
||||||
GestureSeekPreviewView(
|
GestureSeekPreviewView(
|
||||||
storyboard: playerState.preferredStoryboard,
|
storyboard: playerState.preferredStoryboard,
|
||||||
currentTime: seekGestureStartTime,
|
|
||||||
seekTime: seekGesturePreviewTime,
|
seekTime: seekGesturePreviewTime,
|
||||||
duration: playerState.duration,
|
duration: playerState.duration,
|
||||||
storyboardService: StoryboardService.shared,
|
storyboardService: StoryboardService.shared,
|
||||||
|
|||||||
@@ -55,19 +55,6 @@ struct SeekPreviewView: View {
|
|||||||
@State private var thumbnail: PlatformImage?
|
@State private var thumbnail: PlatformImage?
|
||||||
@State private var loadTask: Task<Void, Never>?
|
@State private var loadTask: Task<Void, Never>?
|
||||||
|
|
||||||
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
|
private let thumbnailWidth: CGFloat = 160
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -95,7 +82,7 @@ struct SeekPreviewView: View {
|
|||||||
.clipped()
|
.clipped()
|
||||||
|
|
||||||
// Timestamp overlaid at bottom center
|
// Timestamp overlaid at bottom center
|
||||||
Text(formattedTime)
|
Text(seekTime.formattedAsTimestamp)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
|
|||||||
@@ -13,21 +13,8 @@ struct SeekTimePreviewView: View {
|
|||||||
let buttonBackground: ButtonBackgroundStyle
|
let buttonBackground: ButtonBackgroundStyle
|
||||||
let theme: ControlsTheme
|
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 {
|
var body: some View {
|
||||||
Text(formattedTime)
|
Text(seekTime.formattedAsTimestamp)
|
||||||
.font(.system(size: 16, weight: .medium))
|
.font(.system(size: 16, weight: .medium))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|||||||
@@ -95,61 +95,34 @@ struct MacOSControlBar: View {
|
|||||||
.shadow(color: .black.opacity(0.3), radius: 8, y: 4)
|
.shadow(color: .black.opacity(0.3), radius: 8, y: 4)
|
||||||
// Seek preview overlay - positioned above the control bar
|
// Seek preview overlay - positioned above the control bar
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
if let storyboard = playerState.preferredStoryboard,
|
if (isDragging || isHoveringProgress), !playerState.isLive {
|
||||||
(isDragging || isHoveringProgress),
|
|
||||||
!playerState.isLive {
|
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
let previewProgress = isDragging ? dragProgress : hoverProgress
|
let pos = seekPreviewPosition(geometry: geometry, previewWidth: playerState.preferredStoryboard != nil ? 176 : 80)
|
||||||
// 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 storyboardCenterX = clampedX + previewWidth / 2
|
if let storyboard = playerState.preferredStoryboard {
|
||||||
|
seekPreviewView(storyboard: storyboard)
|
||||||
|
.offset(x: pos.clampedX, y: -150)
|
||||||
|
|
||||||
seekPreviewView(storyboard: storyboard)
|
if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= pos.progress * playerState.duration }) {
|
||||||
.offset(x: clampedX, y: -150)
|
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 }) {
|
if showChapters, let chapter = playerState.chapters.last(where: { $0.startTime <= pos.progress * playerState.duration }) {
|
||||||
ChapterCapsuleView(title: chapter.title, buttonBackground: .none)
|
ChapterCapsuleView(title: chapter.title, buttonBackground: .none)
|
||||||
.positioned(xTarget: storyboardCenterX, availableWidth: geometry.size.width)
|
.positioned(xTarget: pos.centerX, availableWidth: geometry.size.width)
|
||||||
.offset(y: -176)
|
.offset(y: -86)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,6 +309,21 @@ struct MacOSControlBar: View {
|
|||||||
.frame(height: 20)
|
.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
|
@ViewBuilder
|
||||||
private func seekPreviewView(storyboard: Storyboard) -> some View {
|
private func seekPreviewView(storyboard: Storyboard) -> some View {
|
||||||
let previewProgress = isDragging ? dragProgress : hoverProgress
|
let previewProgress = isDragging ? dragProgress : hoverProgress
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ struct TVPlayerProgressBar: View {
|
|||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(formatTime(displayTime))
|
Text(displayTime.formattedAsTimestamp)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.fontWeight(isScrubbing ? .semibold : .regular)
|
.fontWeight(isScrubbing ? .semibold : .regular)
|
||||||
@@ -184,7 +184,7 @@ struct TVPlayerProgressBar: View {
|
|||||||
|
|
||||||
// Remaining time (only for non-live)
|
// Remaining time (only for non-live)
|
||||||
if !isLive {
|
if !isLive {
|
||||||
Text("-\(formatTime(max(0, duration - displayTime)))")
|
Text("-\(max(0, duration - displayTime).formattedAsTimestamp)")
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.white.opacity(0.7))
|
.foregroundStyle(.white.opacity(0.7))
|
||||||
@@ -202,7 +202,7 @@ struct TVPlayerProgressBar: View {
|
|||||||
.transition(.scale.combined(with: .opacity))
|
.transition(.scale.combined(with: .opacity))
|
||||||
} else {
|
} else {
|
||||||
// Fallback when no storyboard available
|
// Fallback when no storyboard available
|
||||||
Text(formatTime(scrubTime ?? currentTime))
|
Text((scrubTime ?? currentTime).formattedAsTimestamp)
|
||||||
.font(.system(size: 48, weight: .medium))
|
.font(.system(size: 48, weight: .medium))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.white)
|
.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
|
// MARK: - Pan Gesture View
|
||||||
|
|||||||
@@ -23,19 +23,6 @@ struct TVSeekPreviewView: View {
|
|||||||
chapters.last { $0.startTime <= seekTime }
|
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
|
private let thumbnailWidth: CGFloat = 320
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -69,7 +56,7 @@ struct TVSeekPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp overlaid at bottom center (larger for TV)
|
// Timestamp overlaid at bottom center (larger for TV)
|
||||||
Text(formattedTime)
|
Text(seekTime.formattedAsTimestamp)
|
||||||
.font(.system(size: 36, weight: .medium))
|
.font(.system(size: 36, weight: .medium))
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|||||||
@@ -427,9 +427,9 @@ struct RemoteControlView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(formatTime(displayCurrentTime))
|
Text(displayCurrentTime.formattedAsTimestamp)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("-" + formatTime(remoteState.duration - displayCurrentTime))
|
Text("-" + (remoteState.duration - displayCurrentTime).formattedAsTimestamp)
|
||||||
}
|
}
|
||||||
.font(.caption.monospacedDigit())
|
.font(.caption.monospacedDigit())
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -683,19 +683,6 @@ struct RemoteControlView: View {
|
|||||||
isConnected = false
|
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.
|
/// Returns the previous playback rate, or nil if at minimum.
|
||||||
private func previousRate() -> PlaybackRate? {
|
private func previousRate() -> PlaybackRate? {
|
||||||
let allRates = PlaybackRate.allCases
|
let allRates = PlaybackRate.allCases
|
||||||
|
|||||||
@@ -238,25 +238,12 @@ struct VideoInfoView: View {
|
|||||||
let resumeAction = appEnvironment?.settingsManager.resumeAction ?? .continueWatching
|
let resumeAction = appEnvironment?.settingsManager.resumeAction ?? .continueWatching
|
||||||
switch resumeAction {
|
switch resumeAction {
|
||||||
case .continueWatching, .ask:
|
case .continueWatching, .ask:
|
||||||
return String(localized: "resume.action.continueAt \(formatTime(savedProgress))")
|
return String(localized: "resume.action.continueAt \(savedProgress.formattedAsTimestamp)")
|
||||||
case .startFromBeginning:
|
case .startFromBeginning:
|
||||||
return String(localized: "video.context.play")
|
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 {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if isLoadingInitialVideo {
|
if isLoadingInitialVideo {
|
||||||
|
|||||||
Reference in New Issue
Block a user