@@ -177,6 +177,21 @@ final class PlayerViewController: UIViewController {
177177 return b
178178 } ( )
179179
180+ private let skipSegmentButton : UIButton = {
181+ let b = UIButton ( type: . system)
182+ b. translatesAutoresizingMaskIntoConstraints = false
183+ b. setTitle ( " Skip " , for: . normal)
184+ b. setTitleColor ( . white, for: . normal)
185+ b. titleLabel? . font = . systemFont( ofSize: 14 , weight: . semibold)
186+ b. backgroundColor = UIColor ( white: 0.2 , alpha: 0.55 )
187+ b. layer. cornerRadius = 18
188+ b. layer. cornerCurve = . continuous
189+ b. contentEdgeInsets = UIEdgeInsets ( top: 8 , left: 12 , bottom: 8 , right: 12 )
190+ b. alpha = 0.0
191+ b. isHidden = true
192+ return b
193+ } ( )
194+
180195 private let progressContainer : UIView = {
181196 let v = UIView ( )
182197 v. translatesAutoresizingMaskIntoConstraints = false
@@ -189,6 +204,7 @@ final class PlayerViewController: UIViewController {
189204 class ProgressModel : ObservableObject {
190205 @Published var position : Double = 0
191206 @Published var duration : Double = 1
207+ @Published var highlights : [ ProgressHighlight ] = [ ]
192208 }
193209 private var progressModel = ProgressModel ( )
194210
@@ -296,6 +312,8 @@ final class PlayerViewController: UIViewController {
296312 private var controlsHideWorkItem : DispatchWorkItem ?
297313 private var controlsVisible : Bool = true
298314 private var pendingSeekTime : Double ?
315+ private var introDBSegments : [ IntroDBSegment ] = [ ]
316+ private var activeSkipSegmentID : String ?
299317
300318 override func viewDidLoad( ) {
301319 super. viewDidLoad ( )
@@ -399,6 +417,7 @@ final class PlayerViewController: UIViewController {
399417 renderer. load ( url: url, with: preset, headers: headers)
400418 if let info = mediaInfo {
401419 prepareSeekToLastPosition ( for: info)
420+ fetchIntroDBSegments ( for: info)
402421 }
403422
404423 if let subs = initialSubtitles, !subs. isEmpty {
@@ -466,6 +485,7 @@ final class PlayerViewController: UIViewController {
466485 videoContainer. addSubview ( skipForwardButton)
467486 videoContainer. addSubview ( speedIndicatorLabel)
468487 videoContainer. addSubview ( subtitleButton)
488+ videoContainer. addSubview ( skipSegmentButton)
469489
470490 NSLayoutConstraint . activate ( [
471491 videoContainer. topAnchor. constraint ( equalTo: view. topAnchor) ,
@@ -526,7 +546,11 @@ final class PlayerViewController: UIViewController {
526546 subtitleButton. trailingAnchor. constraint ( equalTo: progressContainer. trailingAnchor, constant: 0 ) ,
527547 subtitleButton. bottomAnchor. constraint ( equalTo: progressContainer. topAnchor, constant: - 8 ) ,
528548 subtitleButton. widthAnchor. constraint ( equalToConstant: 32 ) ,
529- subtitleButton. heightAnchor. constraint ( equalToConstant: 32 )
549+ subtitleButton. heightAnchor. constraint ( equalToConstant: 32 ) ,
550+
551+ skipSegmentButton. trailingAnchor. constraint ( equalTo: progressContainer. trailingAnchor) ,
552+ skipSegmentButton. bottomAnchor. constraint ( equalTo: progressContainer. topAnchor, constant: - 14 ) ,
553+ skipSegmentButton. heightAnchor. constraint ( greaterThanOrEqualToConstant: 36 )
530554 ] )
531555 }
532556
@@ -536,6 +560,7 @@ final class PlayerViewController: UIViewController {
536560 pipButton. addTarget ( self , action: #selector( pipTapped) , for: . touchUpInside)
537561 skipBackwardButton. addTarget ( self , action: #selector( skipBackwardTapped) , for: . touchUpInside)
538562 skipForwardButton. addTarget ( self , action: #selector( skipForwardTapped) , for: . touchUpInside)
563+ skipSegmentButton. addTarget ( self , action: #selector( skipSegmentTapped) , for: . touchUpInside)
539564 let tap = UITapGestureRecognizer ( target: self , action: #selector( containerTapped) )
540565 videoContainer. addGestureRecognizer ( tap)
541566 }
@@ -856,7 +881,7 @@ final class PlayerViewController: UIViewController {
856881 @ObservedObject var model : ProgressModel
857882 var onEditingChanged : ( Bool ) -> Void
858883 var body : some View {
859- MusicProgressSlider ( value: Binding ( get: { model. position } , set: { model. position = $0 } ) , inRange: 0 ... max ( model. duration, 1.0 ) , activeFillColor: . white, fillColor: . white, textColor: . white. opacity ( 0.7 ) , emptyColor: . white. opacity ( 0.3 ) , height: 33 , onEditingChanged: onEditingChanged)
884+ MusicProgressSlider ( value: Binding ( get: { model. position } , set: { model. position = $0 } ) , inRange: 0 ... max ( model. duration, 1.0 ) , activeFillColor: . white, fillColor: . white, textColor: . white. opacity ( 0.7 ) , emptyColor: . white. opacity ( 0.3 ) , height: 33 , highlights : model . highlights , onEditingChanged: onEditingChanged)
860885 }
861886 }
862887
@@ -1016,6 +1041,13 @@ final class PlayerViewController: UIViewController {
10161041 }
10171042 }
10181043
1044+ @objc private func skipSegmentTapped( ) {
1045+ guard let segment = currentActiveSegment ( at: cachedPosition) else { return }
1046+ guard let target = resolvedEnd ( for: segment, duration: cachedDuration) else { return }
1047+ renderer. seek ( to: max ( 0 , target) )
1048+ showControlsTemporarily ( )
1049+ }
1050+
10191051 private func showControlsTemporarily( ) {
10201052 controlsHideWorkItem? . cancel ( )
10211053 controlsVisible = true
@@ -1090,9 +1122,11 @@ final class PlayerViewController: UIViewController {
10901122 self . cachedPosition = position
10911123 if duration > 0 {
10921124 self . updateProgressHostingController ( )
1125+ self . updateProgressHighlights ( duration: duration)
10931126 }
10941127 self . progressModel. position = position
10951128 self . progressModel. duration = max ( duration, 1.0 )
1129+ self . updateActiveSkipSegment ( at: position, duration: duration)
10961130
10971131 if self . pipController? . isPictureInPictureActive == true {
10981132 self . pipController? . updatePlaybackState ( )
@@ -1121,6 +1155,73 @@ final class PlayerViewController: UIViewController {
11211155 return String ( format: " %02d:%02d " , m, s)
11221156 }
11231157 }
1158+
1159+ private func fetchIntroDBSegments( for mediaInfo: MediaInfo ) {
1160+ IntroDBService . shared. fetchSegments ( for: mediaInfo) { [ weak self] result in
1161+ guard let self = self else { return }
1162+
1163+ switch result {
1164+ case . success( let segments) :
1165+ DispatchQueue . main. async {
1166+ self . introDBSegments = segments
1167+ self . updateProgressHighlights ( duration: self . cachedDuration)
1168+ self . updateActiveSkipSegment ( at: self . cachedPosition, duration: self . cachedDuration)
1169+ }
1170+ Logger . shared. log ( " Loaded \( segments. count) IntroDB segments " , type: " Info " )
1171+ case . failure( let error) :
1172+ Logger . shared. log ( " IntroDB request failed: \( error. localizedDescription) " , type: " Warn " )
1173+ }
1174+ }
1175+ }
1176+
1177+ private func updateProgressHighlights( duration: Double ) {
1178+ let highlights = IntroDBService . shared. highlights ( for: introDBSegments, duration: duration)
1179+ progressModel. highlights = highlights. map {
1180+ ProgressHighlight ( start: $0. start, end: $0. end, color: Color ( $0. color) , label: $0. label)
1181+ }
1182+ }
1183+
1184+ private func currentActiveSegment( at position: Double , duration: Double ? = nil ) -> IntroDBSegment ? {
1185+ return IntroDBService . shared. activeSegment ( at: position, in: introDBSegments, duration: duration ?? cachedDuration)
1186+ }
1187+
1188+ private func updateActiveSkipSegment( at position: Double , duration: Double ) {
1189+ let active = currentActiveSegment ( at: position, duration: duration)
1190+ let newID = active? . id
1191+ guard newID != activeSkipSegmentID else { return }
1192+ activeSkipSegmentID = newID
1193+
1194+ if let active {
1195+ showSkipButton ( for: active)
1196+ } else {
1197+ hideSkipButton ( )
1198+ }
1199+ }
1200+
1201+ private func showSkipButton( for segment: IntroDBSegment ) {
1202+ let title = " Skip \( segment. db. title) "
1203+ skipSegmentButton. setTitle ( title, for: . normal)
1204+ skipSegmentButton. backgroundColor = segment. db. uiColor. withAlphaComponent ( 0.55 )
1205+
1206+ guard skipSegmentButton. isHidden || skipSegmentButton. alpha < 1.0 else { return }
1207+ skipSegmentButton. isHidden = false
1208+ UIView . animate ( withDuration: 0.2 ) {
1209+ self . skipSegmentButton. alpha = 1.0
1210+ }
1211+ }
1212+
1213+ private func hideSkipButton( ) {
1214+ guard !skipSegmentButton. isHidden else { return }
1215+ UIView . animate ( withDuration: 0.2 , animations: {
1216+ self . skipSegmentButton. alpha = 0.0
1217+ } , completion: { _ in
1218+ self . skipSegmentButton. isHidden = true
1219+ } )
1220+ }
1221+
1222+ private func resolvedEnd( for segment: IntroDBSegment , duration: Double ) -> Double ? {
1223+ return segment. resolvedEnd ( duration: duration)
1224+ }
11241225}
11251226
11261227// MARK: - MPVSoftwareRendererDelegate
0 commit comments