mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 12:41:57 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			157 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			157 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| import AVFoundation
 | |
| import Foundation
 | |
| import SwiftUI
 | |
| 
 | |
| final class SeekModel: ObservableObject {
 | |
|     static let shared = SeekModel()
 | |
| 
 | |
|     @Published var currentTime = CMTime.zero
 | |
|     @Published var duration = CMTime.zero
 | |
| 
 | |
|     @Published var lastSeekTime: CMTime? { didSet { onSeek() } }
 | |
|     @Published var lastSeekType: SeekType?
 | |
|     @Published var restoreSeekTime: CMTime?
 | |
| 
 | |
|     @Published var gestureSeek: Double?
 | |
|     @Published var gestureStart: Double?
 | |
| 
 | |
|     @Published var presentingOSD = false
 | |
| 
 | |
|     var player: PlayerModel! { .shared }
 | |
| 
 | |
|     var dismissTimer: Timer?
 | |
| 
 | |
|     var isSeeking: Bool {
 | |
|         gestureSeek != nil
 | |
|     }
 | |
| 
 | |
|     var progress: Double {
 | |
|         let seconds = duration.seconds
 | |
|         guard seconds.isFinite, seconds > 0 else { return 0 }
 | |
| 
 | |
|         if isSeeking {
 | |
|             return gestureSeekDestinationTime / seconds
 | |
|         }
 | |
| 
 | |
|         guard let seekTime = lastSeekTime else {
 | |
|             return currentTime.seconds / seconds
 | |
|         }
 | |
| 
 | |
|         return seekTime.seconds / seconds
 | |
|     }
 | |
| 
 | |
|     var lastSeekPlaybackTime: String {
 | |
|         guard let time = lastSeekTime else { return 0.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder }
 | |
|         return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
 | |
|     }
 | |
| 
 | |
|     var restoreSeekPlaybackTime: String {
 | |
|         guard let time = restoreSeekTime else { return PlayerTimeModel.timePlaceholder }
 | |
|         return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
 | |
|     }
 | |
| 
 | |
|     var gestureSeekDestinationTime: Double {
 | |
|         guard let gestureSeek, let gestureStart else { return -1 }
 | |
|         return min(duration.seconds, max(0, gestureStart + gestureSeek))
 | |
|     }
 | |
| 
 | |
|     var gestureSeekDestinationPlaybackTime: String {
 | |
|         guard gestureSeek != 0 else { return PlayerTimeModel.timePlaceholder }
 | |
|         return gestureSeekDestinationTime.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
 | |
|     }
 | |
| 
 | |
|     var durationPlaybackTime: String {
 | |
|         if player?.currentItem.isNil ?? true {
 | |
|             return PlayerTimeModel.timePlaceholder
 | |
|         }
 | |
| 
 | |
|         return duration.seconds.formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder
 | |
|     }
 | |
| 
 | |
|     func showOSD() {
 | |
|         guard !presentingOSD else { return }
 | |
| 
 | |
|         withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = true }
 | |
|     }
 | |
| 
 | |
|     func hideOSD() {
 | |
|         guard presentingOSD else { return }
 | |
| 
 | |
|         withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = false }
 | |
|     }
 | |
| 
 | |
|     func hideOSDWithDelay() {
 | |
|         dismissTimer?.invalidate()
 | |
|         dismissTimer = Delay.by(3) { self.hideOSD() }
 | |
|     }
 | |
| 
 | |
|     func updateCurrentTime(completionHandler: (() -> Void?)? = nil) {
 | |
|         player.backend.getTimeUpdates()
 | |
|         DispatchQueue.main.async {
 | |
|             self.currentTime = self.player.backend.currentTime ?? .zero
 | |
|             self.duration = self.player.backend.playerItemDuration ?? .zero
 | |
|             completionHandler?()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func onSeekGestureStart() {
 | |
|         updateCurrentTime {
 | |
|             self.gestureStart = self.currentTime.seconds
 | |
|             self.dismissTimer?.invalidate()
 | |
|             self.showOSD()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func onSeekGestureEnd() {
 | |
|         dismissTimer?.invalidate()
 | |
|         dismissTimer = Delay.by(3) { self.hideOSD() }
 | |
|         player.backend.seek(to: gestureSeekDestinationTime, seekType: .userInteracted)
 | |
|     }
 | |
| 
 | |
|     func onSeek() {
 | |
|         guard !lastSeekTime.isNil else { return }
 | |
|         gestureSeek = nil
 | |
|         gestureStart = nil
 | |
|         showOSD()
 | |
|         hideOSDWithDelay()
 | |
|     }
 | |
| 
 | |
|     func registerSeek(at time: CMTime, type: SeekType, restore restoreTime: CMTime? = nil) {
 | |
|         updateCurrentTime {
 | |
|             withAnimation {
 | |
|                 self.lastSeekTime = time
 | |
|                 self.lastSeekType = type
 | |
|                 self.restoreSeekTime = restoreTime
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func restoreTime() {
 | |
|         guard let time = restoreSeekTime else { return }
 | |
|         switch lastSeekType {
 | |
|         case .segmentSkip:
 | |
|             player.restoreLastSkippedSegment()
 | |
|         default:
 | |
|             player.backend.seek(to: time, seekType: .userInteracted)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func resetSeek() {
 | |
|         withAnimation {
 | |
|             lastSeekTime = nil
 | |
|             lastSeekType = nil
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     func reset() {
 | |
|         currentTime = .zero
 | |
|         duration = .zero
 | |
|         resetSeek()
 | |
|         gestureSeek = nil
 | |
|     }
 | |
| 
 | |
|     var forceHours: Bool {
 | |
|         duration.seconds >= 60 * 60
 | |
|     }
 | |
| }
 | 
