1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-15 14:50:32 +05:30
yattee/Shared/Player/Controls/OSD/Seek.swift
Toni Förster b5ac760af2
SponsorBlock set colors for each category
Default colors are defined in alignment to upstream. These can be changed by the user.
The colors are also used in the Timeline and the seek view.
2024-04-23 17:16:31 +02:00

155 lines
5.8 KiB
Swift

import Defaults
import SwiftUI
struct Seek: View {
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
#endif
@ObservedObject private var controls = PlayerControlsModel.shared
@StateObject private var model = SeekModel.shared
private var updateThrottle = Throttle(interval: 2)
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@Default(.sponsorBlockColors) private var sponsorBlockColors
private func getColor(for category: String) -> Color {
if let hexString = sponsorBlockColors[category], let rgbValue = Int(hexString.dropFirst(), radix: 16) {
let r = Double((rgbValue >> 16) & 0xFF) / 255.0
let g = Double((rgbValue >> 8) & 0xFF) / 255.0
let b = Double(rgbValue & 0xFF) / 255.0
return Color(red: r, green: g, blue: b)
}
return Color("AppRedColor") // Fallback color if no match found
}
var body: some View {
Group {
#if os(tvOS)
content
.shadow(radius: 3)
#else
Button(action: model.restoreTime) { content }
.buttonStyle(.plain)
#endif
}
.opacity(visible || YatteeApp.isForPreviews ? 1 : 0)
}
var content: some View {
VStack(spacing: playerControlsLayout.osdSpacing) {
ProgressBar(value: model.progress)
.frame(maxHeight: playerControlsLayout.osdProgressBarHeight)
timeline
if model.isSeeking {
Divider()
gestureSeekTime
.foregroundColor(.secondary)
.font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit())
.frame(height: playerControlsLayout.chapterFontSize + 5)
if let chapter = projectedChapter {
Divider()
Text(chapter.title)
.multilineTextAlignment(.center)
.font(.system(size: playerControlsLayout.chapterFontSize))
.fixedSize(horizontal: false, vertical: true)
}
if let segment = projectedSegment {
Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor")
.font(.system(size: playerControlsLayout.segmentFontSize))
.foregroundColor(getColor(for: segment.category))
.padding(.bottom, 3)
}
} else {
#if !os(tvOS)
if !model.restoreSeekTime.isNil {
Divider()
Label(model.restoreSeekPlaybackTime, systemImage: "arrow.counterclockwise")
.foregroundColor(.secondary)
.font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit())
.frame(height: playerControlsLayout.chapterFontSize + 5)
}
#endif
Group {
switch model.lastSeekType {
case let .segmentSkip(category):
Divider()
Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor")
.font(.system(size: playerControlsLayout.segmentFontSize))
.foregroundColor(getColor(for: category))
.padding(.bottom, 3)
default:
EmptyView()
}
}
}
}
.frame(maxWidth: playerControlsLayout.seekOSDWidth)
#if os(tvOS)
.padding(30)
#else
.padding(2)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 3))
#endif
.foregroundColor(.primary)
}
var timeline: some View {
let text = model.isSeeking ?
"\(model.gestureSeekDestinationPlaybackTime)/\(model.durationPlaybackTime)" :
"\(model.lastSeekPlaybackTime)/\(model.durationPlaybackTime)"
return Text(text)
.fontWeight(.bold)
.font(.system(size: playerControlsLayout.projectedTimeFontSize).monospacedDigit())
}
var gestureSeekTime: some View {
var seek = model.gestureSeekDestinationTime - model.currentTime.seconds
if seek > 0 {
seek = min(seek, model.duration.seconds - model.currentTime.seconds)
} else {
seek = min(seek, model.currentTime.seconds)
}
let timeText = abs(seek)
.formattedAsPlaybackTime(allowZero: true, forceHours: model.forceHours) ?? ""
return Label(
timeText,
systemImage: seek >= 0 ? "goforward.plus" : "gobackward.minus"
)
}
var visible: Bool {
guard !(model.lastSeekTime.isNil && !model.isSeeking) else { return false }
if let type = model.lastSeekType, !type.presentable { return false }
return !controls.presentingControls && !controls.presentingOverlays && model.presentingOSD
}
var projectedChapter: Chapter? {
(model.player?.currentVideo?.chapters ?? []).last { $0.start <= model.gestureSeekDestinationTime }
}
var projectedSegment: Segment? {
(model.player?.sponsorBlock.segments ?? []).first { $0.timeInSegment(.secondsInDefaultTimescale(model.gestureSeekDestinationTime)) }
}
var playerControlsLayout: PlayerControlsLayout {
(model.player?.playingFullScreen ?? false) ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
}
}
struct Seek_Previews: PreviewProvider {
static var previews: some View {
Seek()
}
}