diff --git a/Sora/Utils/DownloadManager/DownloadManager.swift b/Sora/Utils/DownloadManager/DownloadManager.swift deleted file mode 100644 index 78d2b08..0000000 --- a/Sora/Utils/DownloadManager/DownloadManager.swift +++ /dev/null @@ -1,211 +0,0 @@ -// -// DownloadManager.swift -// Sulfur -// -// Created by Francesco on 09/03/25. -// - -import Foundation -import FFmpegSupport -import UIKit - -class DownloadManager { - static let shared = DownloadManager() - - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - private var activeConversions = [String: Bool]() - - private init() { - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) - } - - @objc private func applicationWillResignActive() { - if !activeConversions.isEmpty { - backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { [weak self] in - self?.endBackgroundTask() - } - } - } - - private func endBackgroundTask() { - if backgroundTaskIdentifier != .invalid { - UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) - backgroundTaskIdentifier = .invalid - } - } - - func downloadAndConvertHLS(from url: URL, title: String, episode: Int, subtitleURL: URL? = nil, module: ScrapingModule, completion: @escaping (Bool, URL?) -> Void) { - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - completion(false, nil) - return - } - - let folderURL = documentsDirectory.appendingPathComponent(title + "-" + module.metadata.sourceName) - if (!FileManager.default.fileExists(atPath: folderURL.path)) { - do { - try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) - } catch { - Logger.shared.log("Error creating folder: \(error)") - completion(false, nil) - return - } - } - - let outputFileName = "\(title)_Episode\(episode)_\(module.metadata.sourceName).mp4" - let outputFileURL = folderURL.appendingPathComponent(outputFileName) - - let fileExtension = url.pathExtension.lowercased() - - if fileExtension == "mp4" { - NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ - "title": title, - "episode": episode, - "type": "mp4", - "status": "Downloading", - "progress": 0.0 - ]) - - let task = URLSession.custom.downloadTask(with: url) { tempLocalURL, response, error in - if let tempLocalURL = tempLocalURL { - do { - try FileManager.default.moveItem(at: tempLocalURL, to: outputFileURL) - NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ - "title": title, - "episode": episode, - "type": "mp4", - "status": "Completed", - "progress": 1.0 - ]) - DispatchQueue.main.async { - Logger.shared.log("Download successful: \(outputFileURL)") - completion(true, outputFileURL) - } - } catch { - DispatchQueue.main.async { - Logger.shared.log("Download failed: \(error)") - completion(false, nil) - } - } - } else { - DispatchQueue.main.async { - Logger.shared.log("Download failed: \(error?.localizedDescription ?? "Unknown error")") - completion(false, nil) - } - } - } - task.resume() - } else if fileExtension == "m3u8" { - let conversionKey = "\(title)_\(episode)_\(module.metadata.sourceName)" - activeConversions[conversionKey] = true - - if UIApplication.shared.applicationState != .active && backgroundTaskIdentifier == .invalid { - backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { [weak self] in - self?.endBackgroundTask() - } - } - - DispatchQueue.global(qos: .background).async { - NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ - "title": title, - "episode": episode, - "type": "hls", - "status": "Converting", - "progress": 0.0 - ]) - - let processorCount = ProcessInfo.processInfo.processorCount - let physicalMemory = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) - - var ffmpegCommand = ["ffmpeg", "-y"] - - ffmpegCommand.append(contentsOf: ["-protocol_whitelist", "file,http,https,tcp,tls"]) - - ffmpegCommand.append(contentsOf: ["-fflags", "+genpts"]) - ffmpegCommand.append(contentsOf: ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5"]) - ffmpegCommand.append(contentsOf: ["-headers", "Referer: \(module.metadata.baseUrl)\nOrigin: \(module.metadata.baseUrl)"]) - - let multiThreads = UserDefaults.standard.bool(forKey: "multiThreads") - if multiThreads { - let threadCount = max(2, processorCount - 1) - ffmpegCommand.append(contentsOf: ["-threads", "\(threadCount)"]) - } else { - ffmpegCommand.append(contentsOf: ["-threads", "2"]) - } - - let bufferSize = min(32, max(8, Int(physicalMemory) / 256)) - ffmpegCommand.append(contentsOf: ["-bufsize", "\(bufferSize)M"]) - ffmpegCommand.append(contentsOf: ["-i", url.absoluteString]) - - if let subtitleURL = subtitleURL { - do { - let subtitleData = try Data(contentsOf: subtitleURL) - let subtitleFileExtension = subtitleURL.pathExtension.lowercased() - if subtitleFileExtension != "srt" && subtitleFileExtension != "vtt" { - Logger.shared.log("Unsupported subtitle format: \(subtitleFileExtension)") - } - let subtitleFileName = "\(title)_Episode\(episode).\(subtitleFileExtension)" - let subtitleLocalURL = folderURL.appendingPathComponent(subtitleFileName) - try subtitleData.write(to: subtitleLocalURL) - ffmpegCommand.append(contentsOf: ["-i", subtitleLocalURL.path]) - - ffmpegCommand.append(contentsOf: [ - "-c:v", "copy", - "-c:a", "copy", - "-c:s", "mov_text", - "-disposition:s:0", "default+forced", - "-metadata:s:s:0", "handler_name=English", - "-metadata:s:s:0", "language=eng" - ]) - - ffmpegCommand.append(outputFileURL.path) - } catch { - Logger.shared.log("Subtitle download failed: \(error)") - ffmpegCommand.append(contentsOf: ["-c:v", "copy", "-c:a", "copy"]) - ffmpegCommand.append(contentsOf: ["-movflags", "+faststart"]) - ffmpegCommand.append(outputFileURL.path) - } - } else { - ffmpegCommand.append(contentsOf: ["-c:v", "copy", "-c:a", "copy"]) - ffmpegCommand.append(contentsOf: ["-movflags", "+faststart"]) - ffmpegCommand.append(outputFileURL.path) - } - Logger.shared.log("FFmpeg command: \(ffmpegCommand.joined(separator: " "))", type: "Debug") - - NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ - "title": title, - "episode": episode, - "type": "hls", - "status": "Converting", - "progress": 0.5 - ]) - - let success = ffmpeg(ffmpegCommand) - DispatchQueue.main.async { [weak self] in - if success == 0 { - NotificationCenter.default.post(name: .DownloadManagerStatusUpdate, object: nil, userInfo: [ - "title": title, - "episode": episode, - "type": "hls", - "status": "Completed", - "progress": 1.0 - ]) - Logger.shared.log("Conversion successful: \(outputFileURL)") - completion(true, outputFileURL) - } else { - Logger.shared.log("Conversion failed") - completion(false, nil) - } - - self?.activeConversions[conversionKey] = nil - - if self?.activeConversions.isEmpty ?? true { - self?.endBackgroundTask() - } - } - } - } else { - Logger.shared.log("Unsupported file type: \(fileExtension)") - completion(false, nil) - } - } -} diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 3183628..5c97e1d 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -18,8 +18,8 @@ struct MusicProgressSlider: View { let emptyColor: Color let height: CGFloat let onEditingChanged: (Bool) -> Void - let introSegments: [ClosedRange] // Changed - let outroSegments: [ClosedRange] // Changed + let introSegments: [ClosedRange] + let outroSegments: [ClosedRange] let introColor: Color let outroColor: Color @@ -57,10 +57,10 @@ struct MusicProgressSlider: View { } } - // Rest of the existing code... Capsule() .fill(emptyColor) } + .clipShape(Capsule()) Capsule() .fill(isActive ? activeFillColor : fillColor) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 3ed4e21..f824db4 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -11,7 +11,6 @@ import AVKit import SwiftUI import AVFoundation import MediaPlayer -// MARK: - CustomMediaPlayerViewController class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate { let module: ScrapingModule @@ -94,6 +93,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele var speedButton: UIButton! var skip85Button: UIButton! var qualityButton: UIButton! + var holdSpeedIndicator: UIButton! var isHLSStream: Bool = false var qualities: [(String, String)] = [] @@ -116,6 +116,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } } + private var wasPlayingBeforeSeek = false + private var malID: Int? private var skipIntervals: (op: CMTimeRange?, ed: CMTimeRange?) = (nil, nil) @@ -123,6 +125,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele private var skipOutroButton: UIButton! private let skipButtonBaseAlpha: CGFloat = 0.9 @Published var segments: [ClosedRange] = [] + private var skipIntroLeading: NSLayoutConstraint! + private var skipOutroLeading: NSLayoutConstraint! + private var originalIntroLeading: CGFloat = 0 + private var originalOutroLeading: CGFloat = 0 + private var skipIntroDismissedInSession = false + private var skipOutroDismissedInSession = false private var playerItemKVOContext = 0 private var loadedTimeRangesObservation: NSKeyValueObservation? @@ -214,7 +222,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele loadSubtitleSettings() setupPlayerViewController() setupControls() - setupSkipAndDismissGestures() addInvisibleControlOverlays() setupWatchNextButton() setupSubtitleLabel() @@ -227,11 +234,15 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele setupMarqueeLabel() setupSkip85Button() setupSkipButtons() + setupSkipAndDismissGestures() addTimeObserver() startUpdateTimer() setupAudioSession() updateSkipButtonsVisibility() + setupHoldSpeedIndicator() + view.bringSubviewToFront(subtitleLabel) + view.bringSubviewToFront(topSubtitleLabel) AniListMutation().fetchMalID(animeId: aniListID) { [weak self] result in switch result { @@ -240,7 +251,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self?.fetchSkipTimes(type: "op") self?.fetchSkipTimes(type: "ed") case .failure(let error): - Logger.shared.log("⚠️ Unable to fetch MAL ID: \(error)",type:"Error") + Logger.shared.log("Unable to fetch MAL ID: \(error)",type:"Error") } } @@ -270,7 +281,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } } - if #available(iOS 16.0, *) { playerViewController.allowsVideoFrameAnalysis = false } @@ -374,14 +384,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele DispatchQueue.main.async { [weak self] in guard let self = self else { return } if self.qualityButton.isHidden && self.isHLSStream { - // 1) reveal the quality button self.qualityButton.isHidden = false self.qualityButton.menu = self.qualitySelectionMenu() - // 2) update the trailing constraint for the menuButton self.updateMenuButtonConstraints() - // 3) animate the shift UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) { self.view.layoutIfNeeded() } @@ -523,11 +530,19 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele onEditingChanged: { editing in if editing { self.isSliderEditing = true + + self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing) + self.originalRate = self.player.rate + + self.player.pause() } else { - let wasPlaying = self.isPlaying - let targetTime = CMTime(seconds: self.sliderViewModel.sliderValue, - preferredTimescale: 600) - self.player.seek(to: targetTime) { [weak self] finished in + let target = CMTime(seconds: self.sliderViewModel.sliderValue, + preferredTimescale: 600) + self.player.seek( + to: target, + toleranceBefore: .zero, + toleranceAfter: .zero + ) { [weak self] _ in guard let self = self else { return } let final = self.player.currentTime().seconds @@ -535,16 +550,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.currentTimeVal = final self.isSliderEditing = false - if wasPlaying { - self.player.play() + if self.wasPlayingBeforeSeek { + self.player.playImmediately(atRate: self.originalRate) } } } }, - introSegments: sliderViewModel.introSegments, // Added - outroSegments: sliderViewModel.outroSegments, // Added - introColor: segmentsColor, // Add your colors here - outroColor: segmentsColor // Or use settings.accentColor + introSegments: sliderViewModel.introSegments, + outroSegments: sliderViewModel.outroSegments, + introColor: segmentsColor, + outroColor: segmentsColor ) sliderHostingController = UIHostingController(rootView: sliderView) @@ -615,6 +630,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + if let introSwipe = skipIntroButton.gestureRecognizers?.first( + where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left } + ), + let outroSwipe = skipOutroButton.gestureRecognizers?.first( + where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left } + ) { + panGesture.require(toFail: introSwipe) + panGesture.require(toFail: outroSwipe) + } + view.addGestureRecognizer(panGesture) } @@ -693,46 +718,50 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele func setupSubtitleLabel() { subtitleLabel = UILabel() - subtitleLabel.textAlignment = .center - subtitleLabel.numberOfLines = 0 - subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - view.addSubview(subtitleLabel) - subtitleLabel.translatesAutoresizingMaskIntoConstraints = false - - subtitleBottomToSliderConstraint = subtitleLabel.bottomAnchor.constraint( - equalTo: sliderHostingController!.view.topAnchor, - constant: -20 - ) - - subtitleBottomToSafeAreaConstraint = subtitleLabel.bottomAnchor.constraint( - equalTo: view.safeAreaLayoutGuide.bottomAnchor, - constant: -subtitleBottomPadding - ) - - NSLayoutConstraint.activate([ - subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36), - subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36) - ]) - - subtitleBottomToSafeAreaConstraint?.isActive = true + subtitleLabel?.textAlignment = .center + subtitleLabel?.numberOfLines = 0 + subtitleLabel?.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + if let subtitleLabel = subtitleLabel { + view.addSubview(subtitleLabel) + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + + subtitleBottomToSliderConstraint = subtitleLabel.bottomAnchor.constraint( + equalTo: sliderHostingController?.view.topAnchor ?? view.bottomAnchor, + constant: -20 + ) + + subtitleBottomToSafeAreaConstraint = subtitleLabel.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -subtitleBottomPadding + ) + + NSLayoutConstraint.activate([ + subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36), + subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36) + ]) + + subtitleBottomToSafeAreaConstraint?.isActive = true + } topSubtitleLabel = UILabel() - topSubtitleLabel.textAlignment = .center - topSubtitleLabel.numberOfLines = 0 - topSubtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - topSubtitleLabel.isHidden = true - view.addSubview(topSubtitleLabel) - topSubtitleLabel.translatesAutoresizingMaskIntoConstraints = false + topSubtitleLabel?.textAlignment = .center + topSubtitleLabel?.numberOfLines = 0 + topSubtitleLabel?.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + topSubtitleLabel?.isHidden = true + if let topSubtitleLabel = topSubtitleLabel { + view.addSubview(topSubtitleLabel) + topSubtitleLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + topSubtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + topSubtitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30), + topSubtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36), + topSubtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36) + ]) + } updateSubtitleLabelAppearance() - - NSLayoutConstraint.activate([ - topSubtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - topSubtitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30), - topSubtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36), - topSubtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36) - ]) } func updateSubtitleLabelConstraints() { @@ -780,10 +809,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele marqueeLabel.textColor = .white marqueeLabel.font = UIFont.systemFont(ofSize: 14, weight: .heavy) - marqueeLabel.speed = .rate(35) // Adjust scrolling speed as needed - marqueeLabel.fadeLength = 10.0 // Fading at the label’s edges - marqueeLabel.leadingBuffer = 1.0 // Left inset for scrolling - marqueeLabel.trailingBuffer = 16.0 // Right inset for scrolling + marqueeLabel.speed = .rate(35) + marqueeLabel.fadeLength = 10.0 + marqueeLabel.leadingBuffer = 1.0 + marqueeLabel.trailingBuffer = 16.0 marqueeLabel.animationDelay = 2.5 marqueeLabel.layer.shadowColor = UIColor.black.cgColor @@ -798,33 +827,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele controlsContainerView.addSubview(marqueeLabel) marqueeLabel.translatesAutoresizingMaskIntoConstraints = false - // 1. Portrait mode with button visible - portraitButtonVisibleConstraints = [ - marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8), - marqueeLabel.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -16), - marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) - ] - - // 2. Portrait mode with button hidden - portraitButtonHiddenConstraints = [ - marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 12), - marqueeLabel.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16), - marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) - ] - - // 3. Landscape mode with button visible (using smaller margins) - landscapeButtonVisibleConstraints = [ - marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8), - marqueeLabel.trailingAnchor.constraint(equalTo: menuButton.leadingAnchor, constant: -8), - marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) - ] - - // 4. Landscape mode with button hidden - landscapeButtonHiddenConstraints = [ - marqueeLabel.leadingAnchor.constraint(equalTo: dismissButton.trailingAnchor, constant: 8), - marqueeLabel.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -8), - marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor) - ] updateMarqueeConstraints() } @@ -853,9 +855,44 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele ]) } + private func setupHoldSpeedIndicator() { + let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold) + let image = UIImage(systemName: "forward.fill", withConfiguration: config) + let speed = UserDefaults.standard.float(forKey: "holdSpeedPlayer") + + holdSpeedIndicator = UIButton(type: .system) + holdSpeedIndicator.setTitle(" \(speed)", for: .normal) + holdSpeedIndicator.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold) + holdSpeedIndicator.setImage(image, for: .normal) + + holdSpeedIndicator.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) + holdSpeedIndicator.tintColor = .white + holdSpeedIndicator.setTitleColor(.white, for: .normal) + holdSpeedIndicator.layer.cornerRadius = 21 + holdSpeedIndicator.alpha = 0 + + holdSpeedIndicator.layer.shadowColor = UIColor.black.cgColor + holdSpeedIndicator.layer.shadowOffset = CGSize(width: 0, height: 2) + holdSpeedIndicator.layer.shadowOpacity = 0.6 + holdSpeedIndicator.layer.shadowRadius = 4 + holdSpeedIndicator.layer.masksToBounds = false + + view.addSubview(holdSpeedIndicator) + holdSpeedIndicator.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + holdSpeedIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + holdSpeedIndicator.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + holdSpeedIndicator.heightAnchor.constraint(equalToConstant: 40), + holdSpeedIndicator.widthAnchor.constraint(greaterThanOrEqualToConstant: 85) + ]) + + holdSpeedIndicator.isUserInteractionEnabled = false + } + private func updateSkipButtonsVisibility() { - let t = currentTimeVal - let controlsShowing = isControlsVisible // true ⇒ main UI is on‑screen + let t = currentTimeVal + let controlsShowing = isControlsVisible func handle(_ button: UIButton, range: CMTimeRange?) { guard let r = range else { button.isHidden = true; return } @@ -885,6 +922,17 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele handle(skipIntroButton, range: skipIntervals.op) handle(skipOutroButton, range: skipIntervals.ed) + + if skipIntroDismissedInSession { + skipIntroButton.isHidden = true + } else { + handle(skipIntroButton, range: skipIntervals.op) + } + if skipOutroDismissedInSession { + skipOutroButton.isHidden = true + } else { + handle(skipOutroButton, range: skipIntervals.ed) + } } private func updateSegments() { @@ -918,17 +966,38 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele emptyColor: .white.opacity(0.3), height: 33, onEditingChanged: { editing in - if !editing { - let targetTime = CMTime( - seconds: self.sliderViewModel.sliderValue, - preferredTimescale: 600 - ) - self.player.seek(to: targetTime) + if editing { + self.isSliderEditing = true + + self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing) + self.originalRate = self.player.rate + + self.player.pause() + } else { + + let target = CMTime(seconds: self.sliderViewModel.sliderValue, + preferredTimescale: 600) + self.player.seek( + to: target, + toleranceBefore: .zero, + toleranceAfter: .zero + ) { [weak self] _ in + guard let self = self else { return } + + let final = self.player.currentTime().seconds + self.sliderViewModel.sliderValue = final + self.currentTimeVal = final + self.isSliderEditing = false + + if self.wasPlayingBeforeSeek { + self.player.playImmediately(atRate: self.originalRate) + } + } } }, introSegments: self.sliderViewModel.introSegments, outroSegments: self.sliderViewModel.outroSegments, - introColor: segmentsColor, // Match your color choices + introColor: segmentsColor, outroColor: segmentsColor ) } @@ -953,7 +1022,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } else { self.skipIntervals.ed = range } - // Update segments only if duration is available if self.duration > 0 { self.updateSegments() } @@ -961,22 +1029,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele }.resume() } - private func setupSkipButtons() { + func setupSkipButtons() { let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold) let introImage = UIImage(systemName: "forward.frame", withConfiguration: introConfig) - skipIntroButton = UIButton(type: .system) - skipIntroButton.setImage(introImage, for: .normal) skipIntroButton.setTitle(" Skip Intro", for: .normal) skipIntroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold) + skipIntroButton.setImage(introImage, for: .normal) - // match skip85Button styling: - skipIntroButton.backgroundColor = UIColor(red: 51/255, green: 51/255, blue: 51/255, alpha: 0.8) + skipIntroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) skipIntroButton.tintColor = .white skipIntroButton.setTitleColor(.white, for: .normal) - skipIntroButton.layer.cornerRadius = 15 + skipIntroButton.layer.cornerRadius = 21 skipIntroButton.alpha = skipButtonBaseAlpha - skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) + skipIntroButton.layer.shadowColor = UIColor.black.cgColor skipIntroButton.layer.shadowOffset = CGSize(width: 0, height: 2) skipIntroButton.layer.shadowOpacity = 0.6 @@ -984,50 +1050,47 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele skipIntroButton.layer.masksToBounds = false skipIntroButton.addTarget(self, action: #selector(skipIntro), for: .touchUpInside) + view.addSubview(skipIntroButton) skipIntroButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - skipIntroButton.leadingAnchor.constraint( - equalTo: sliderHostingController!.view.leadingAnchor), - skipIntroButton.bottomAnchor.constraint( - equalTo: sliderHostingController!.view.topAnchor, constant: -5) + skipIntroButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor), + skipIntroButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), + skipIntroButton.heightAnchor.constraint(equalToConstant: 40), + skipIntroButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 104) ]) - // MARK: – Skip Outro Button let outroConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold) let outroImage = UIImage(systemName: "forward.frame", withConfiguration: outroConfig) - skipOutroButton = UIButton(type: .system) - skipOutroButton.setImage(outroImage, for: .normal) skipOutroButton.setTitle(" Skip Outro", for: .normal) skipOutroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold) + skipOutroButton.setImage(outroImage, for: .normal) - // same styling as above - skipOutroButton.backgroundColor = skipIntroButton.backgroundColor - skipOutroButton.tintColor = skipIntroButton.tintColor + skipOutroButton.backgroundColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 0.8) + skipOutroButton.tintColor = .white skipOutroButton.setTitleColor(.white, for: .normal) - skipOutroButton.layer.cornerRadius = skipIntroButton.layer.cornerRadius - skipOutroButton.alpha = skipIntroButton.alpha - skipOutroButton.contentEdgeInsets = skipIntroButton.contentEdgeInsets - skipOutroButton.layer.shadowColor = skipIntroButton.layer.shadowColor - skipOutroButton.layer.shadowOffset = skipIntroButton.layer.shadowOffset - skipOutroButton.layer.shadowOpacity = skipIntroButton.layer.shadowOpacity - skipOutroButton.layer.shadowRadius = skipIntroButton.layer.shadowRadius + skipOutroButton.layer.cornerRadius = 21 + skipOutroButton.alpha = skipButtonBaseAlpha + + skipOutroButton.layer.shadowColor = UIColor.black.cgColor + skipOutroButton.layer.shadowOffset = CGSize(width: 0, height: 2) + skipOutroButton.layer.shadowOpacity = 0.6 + skipOutroButton.layer.shadowRadius = 4 skipOutroButton.layer.masksToBounds = false skipOutroButton.addTarget(self, action: #selector(skipOutro), for: .touchUpInside) + view.addSubview(skipOutroButton) skipOutroButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - skipOutroButton.leadingAnchor.constraint( - equalTo: sliderHostingController!.view.leadingAnchor), - skipOutroButton.bottomAnchor.constraint( - equalTo: sliderHostingController!.view.topAnchor, constant: -5) + skipOutroButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor), + skipOutroButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5), + skipOutroButton.heightAnchor.constraint(equalToConstant: 40), + skipOutroButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 104) ]) - - view.bringSubviewToFront(skipOutroButton) } private func setupDimButton() { @@ -1046,20 +1109,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele dimButton.layer.masksToBounds = false NSLayoutConstraint.activate([ - dimButton.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor), + dimButton.topAnchor.constraint(equalTo: volumeSliderHostingView!.bottomAnchor, constant: 15), + dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor), dimButton.widthAnchor.constraint(equalToConstant: 24), - dimButton.heightAnchor.constraint(equalToConstant: 24), + dimButton.heightAnchor.constraint(equalToConstant: 24) ]) - dimButtonToSlider = dimButton.trailingAnchor.constraint( - equalTo: volumeSliderHostingView!.leadingAnchor, - constant: -8 - ) - dimButtonToRight = dimButton.trailingAnchor.constraint( - equalTo: controlsContainerView.trailingAnchor, - constant: -16 - ) - + dimButtonToSlider = dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor) + dimButtonToRight = dimButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16) dimButtonToSlider.isActive = true } @@ -1069,7 +1126,9 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele let leftSpacing: CGFloat = 2 let rightSpacing: CGFloat = 6 - let trailingAnchor: NSLayoutXAxisAnchor = dimButton.leadingAnchor + let trailingAnchor: NSLayoutXAxisAnchor = (volumeSliderHostingView?.isHidden == false) + ? volumeSliderHostingView!.leadingAnchor + : view.safeAreaLayoutGuide.trailingAnchor currentMarqueeConstraints = [ marqueeLabel.leadingAnchor.constraint( @@ -1126,6 +1185,12 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele speedButton.showsMenuAsPrimaryAction = true speedButton.menu = speedChangerMenu() + speedButton.layer.shadowColor = UIColor.black.cgColor + speedButton.layer.shadowOffset = CGSize(width: 0, height: 2) + speedButton.layer.shadowOpacity = 0.6 + speedButton.layer.shadowRadius = 4 + speedButton.layer.masksToBounds = false + controlsContainerView.addSubview(speedButton) speedButton.translatesAutoresizingMaskIntoConstraints = false @@ -1182,8 +1247,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele skip85Button.layer.cornerRadius = 21 skip85Button.alpha = 0.7 - skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10) - skip85Button.layer.shadowColor = UIColor.black.cgColor skip85Button.layer.shadowOffset = CGSize(width: 0, height: 2) skip85Button.layer.shadowOpacity = 0.6 @@ -1235,42 +1298,32 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } func updateSubtitleLabelAppearance() { - // subtitleLabel always exists here: - subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - subtitleLabel.textColor = subtitleUIColor() - subtitleLabel.backgroundColor = subtitleBackgroundEnabled - ? UIColor.black.withAlphaComponent(0.6) - : .clear - subtitleLabel.layer.cornerRadius = 5 - subtitleLabel.clipsToBounds = true - subtitleLabel.layer.shadowColor = UIColor.black.cgColor - subtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) - subtitleLabel.layer.shadowOpacity = 1.0 - subtitleLabel.layer.shadowOffset = .zero + if let subtitleLabel = subtitleLabel { + subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + subtitleLabel.textColor = subtitleUIColor() + subtitleLabel.backgroundColor = subtitleBackgroundEnabled + ? UIColor.black.withAlphaComponent(0.6) + : .clear + subtitleLabel.layer.cornerRadius = 5 + subtitleLabel.clipsToBounds = true + subtitleLabel.layer.shadowColor = UIColor.black.cgColor + subtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) + subtitleLabel.layer.shadowOpacity = 1.0 + subtitleLabel.layer.shadowOffset = .zero + } - // only style it if it’s been created already - topSubtitleLabel?.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - topSubtitleLabel?.textColor = subtitleUIColor() - topSubtitleLabel?.backgroundColor = subtitleBackgroundEnabled - ? UIColor.black.withAlphaComponent(0.6) - : .clear - topSubtitleLabel?.layer.cornerRadius = 5 - topSubtitleLabel?.clipsToBounds = true - topSubtitleLabel?.layer.shadowColor = UIColor.black.cgColor - topSubtitleLabel?.layer.shadowRadius = CGFloat(subtitleShadowRadius) - topSubtitleLabel?.layer.shadowOpacity = 1.0 - topSubtitleLabel?.layer.shadowOffset = .zero - } - - func subtitleUIColor() -> UIColor { - switch subtitleForegroundColor { - case "white": return .white - case "yellow": return .yellow - case "green": return .green - case "purple": return .purple - case "blue": return .blue - case "red": return .red - default: return .white + if let topSubtitleLabel = topSubtitleLabel { + topSubtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + topSubtitleLabel.textColor = subtitleUIColor() + topSubtitleLabel.backgroundColor = subtitleBackgroundEnabled + ? UIColor.black.withAlphaComponent(0.6) + : .clear + topSubtitleLabel.layer.cornerRadius = 5 + topSubtitleLabel.clipsToBounds = true + topSubtitleLabel.layer.shadowColor = UIColor.black.cgColor + topSubtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) + topSubtitleLabel.layer.shadowOpacity = 1.0 + topSubtitleLabel.layer.shadowOffset = .zero } } @@ -1294,6 +1347,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.sliderViewModel.sliderValue = max(0, min(self.currentTimeVal, self.duration)) } + self.updateSkipButtonsVisibility() + UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)") UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)") @@ -1320,8 +1375,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.topSubtitleLabel.isHidden = true } - let current = self.currentTimeVal - let segmentsColor = self.getSegmentsColor() DispatchQueue.main.async { @@ -1368,17 +1421,37 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele emptyColor: .white.opacity(0.3), height: 33, onEditingChanged: { editing in - if !editing { - let targetTime = CMTime( - seconds: self.sliderViewModel.sliderValue, - preferredTimescale: 600 - ) - self.player.seek(to: targetTime) + if editing { + self.isSliderEditing = true + + self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing) + self.originalRate = self.player.rate + + self.player.pause() + } else { + let target = CMTime(seconds: self.sliderViewModel.sliderValue, + preferredTimescale: 600) + self.player.seek( + to: target, + toleranceBefore: .zero, + toleranceAfter: .zero + ) { [weak self] _ in + guard let self = self else { return } + + let final = self.player.currentTime().seconds + self.sliderViewModel.sliderValue = final + self.currentTimeVal = final + self.isSliderEditing = false + + if self.wasPlayingBeforeSeek { + self.player.playImmediately(atRate: self.originalRate) + } + } } }, introSegments: self.sliderViewModel.introSegments, outroSegments: self.sliderViewModel.outroSegments, - introColor: segmentsColor, // Match your color choices + introColor: segmentsColor, outroColor: segmentsColor ) } @@ -1388,7 +1461,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele @objc private func skipIntro() { if let range = skipIntervals.op { player.seek(to: range.end) - // optionally hide button immediately: skipIntroButton.isHidden = true } } @@ -1409,10 +1481,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } func updateMenuButtonConstraints() { - // tear down last one currentMenuButtonTrailing.isActive = false - // pick the “next” visible control let anchor: NSLayoutXAxisAnchor if !qualityButton.isHidden { anchor = qualityButton.leadingAnchor @@ -1524,6 +1594,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.controlsContainerView.alpha = 1.0 self.skip85Button.alpha = 0.8 }) + self.updateSkipButtonsVisibility() } } } else { @@ -2093,11 +2164,20 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele guard let player = player else { return } originalRate = player.rate let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer") - player.rate = holdSpeed > 0 ? holdSpeed : 2.0 + let speed = holdSpeed > 0 ? holdSpeed : 2.0 + player.rate = speed + + UIView.animate(withDuration: 0.1) { + self.holdSpeedIndicator.alpha = 0.8 + } } - + private func endHoldSpeed() { player?.rate = originalRate + + UIView.animate(withDuration: 0.2) { + self.holdSpeedIndicator.alpha = 0 + } } private func setInitialPlayerRate() { @@ -2143,6 +2223,18 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele .shadow(color: Color.black.opacity(0.6), radius: 4, x: 0, y: 2) } } + + func subtitleUIColor() -> UIColor { + switch subtitleForegroundColor { + case "white": return .white + case "yellow": return .yellow + case "green": return .green + case "purple": return .purple + case "blue": return .blue + case "red": return .red + default: return .white + } + } } // yes? Like the plural of the famous american rapper ye? -IBHRAD diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index 595bee5..e42748a 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -10,6 +10,7 @@ import UIKit class iCloudSyncManager { static let shared = iCloudSyncManager() + private let syncQueue = DispatchQueue(label: "me.cranci.sora.icloud-sync", qos: .utility) private let defaultsToSync: [String] = [ "externalPlayer", "alwaysLandscape", @@ -47,16 +48,103 @@ class iCloudSyncManager { } private func setupSync() { - NSUbiquitousKeyValueStore.default.synchronize() - syncFromiCloud() - syncModulesFromiCloud() - NotificationCenter.default.addObserver(self, selector: #selector(iCloudDidChangeExternally), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default) - NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) + syncQueue.async { [weak self] in + guard let self = self else { return } + + NSUbiquitousKeyValueStore.default.synchronize() + self.syncFromiCloud() + self.syncModulesFromiCloud() + + DispatchQueue.main.async { + NotificationCenter.default.addObserver(self, selector: #selector(self.iCloudDidChangeExternally), name: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default) + NotificationCenter.default.addObserver(self, selector: #selector(self.userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) + } + } + } + + @objc private func iCloudDidChangeExternally(_ notification: NSNotification) { + guard let iCloud = notification.object as? NSUbiquitousKeyValueStore, + let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] else { + Logger.shared.log("Invalid iCloud notification data", type: "Error") + return + } + + syncQueue.async { [weak self] in + guard let self = self else { return } + + let defaults = UserDefaults.standard + for key in changedKeys { + if let value = iCloud.object(forKey: key), self.isValidValueType(value) { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + + defaults.synchronize() + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .iCloudSyncDidComplete, object: nil) + } + } + } + + @objc private func userDefaultsDidChange(_ notification: Notification) { + syncQueue.async { [weak self] in + self?.syncToiCloud() + } + } + + private func syncToiCloud() { + let iCloud = NSUbiquitousKeyValueStore.default + let defaults = UserDefaults.standard + + do { + for key in allKeysToSync() { + if let value = defaults.object(forKey: key) { + if isValidValueType(value) { + iCloud.set(value, forKey: key) + } + } + } + + iCloud.synchronize() + } + } + + private func syncFromiCloud() { + let iCloud = NSUbiquitousKeyValueStore.default + let defaults = UserDefaults.standard + + for key in allKeysToSync() { + if let value = iCloud.object(forKey: key) { + if isValidValueType(value) { + defaults.set(value, forKey: key) + } + } + } + + defaults.synchronize() + NotificationCenter.default.post(name: .iCloudSyncDidComplete, object: nil) + } + + private func isValidValueType(_ value: Any) -> Bool { + return value is String || + value is Bool || + value is Int || + value is Float || + value is Double || + value is Data || + value is Date || + value is [Any] || + value is [String: Any] } @objc private func willEnterBackground() { - syncToiCloud() - syncModulesToiCloud() + syncQueue.async { [weak self] in + self?.syncToiCloud() + self?.syncModulesToiCloud() + } } private func allProgressKeys() -> [String] { @@ -80,60 +168,6 @@ class iCloudSyncManager { return Array(keys) } - private func syncFromiCloud() { - let iCloud = NSUbiquitousKeyValueStore.default - let defaults = UserDefaults.standard - - for key in allKeysToSync() { - if let value = iCloud.object(forKey: key) { - if (value is String) || (value is Bool) || (value is Int) || (value is Float) || (value is Double) || (value is Data) || (value is Date) || (value is Array) || (value is Dictionary) { - defaults.set(value, forKey: key) - } else { - Logger.shared.log("Skipped syncing invalid value type for key: \(key)", type: "Error") - } - } - } - - defaults.synchronize() - NotificationCenter.default.post(name: .iCloudSyncDidComplete, object: nil) - } - - private func syncToiCloud() { - let iCloud = NSUbiquitousKeyValueStore.default - let defaults = UserDefaults.standard - - for key in allKeysToSync() { - if let value = defaults.object(forKey: key) { - iCloud.set(value, forKey: key) - } - } - - iCloud.synchronize() - } - - @objc private func iCloudDidChangeExternally(_ notification: Notification) { - do { - guard let userInfo = notification.userInfo, - let reason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else { - return - } - - if reason == NSUbiquitousKeyValueStoreServerChange || - reason == NSUbiquitousKeyValueStoreInitialSyncChange { - DispatchQueue.main.async { [weak self] in - self?.syncFromiCloud() - self?.syncModulesFromiCloud() - } - } - } catch { - Logger.shared.log("Error handling iCloud sync: \(error.localizedDescription)", type: "Error") - } - } - - @objc private func userDefaultsDidChange(_ notification: Notification) { - syncToiCloud() - } - func syncModulesToiCloud() { DispatchQueue.global(qos: .background).async { [weak self] in guard let self = self, let iCloudURL = self.ubiquityContainerURL else { return } diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index 6e8e3d3..4f18ef2 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -69,7 +69,7 @@ class LibraryManager: ObservableObject { let encoded = try JSONEncoder().encode(bookmarks) UserDefaults.standard.set(encoded, forKey: bookmarksKey) } catch { - Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)", type: "Error") + Logger.shared.log("Failed to save bookmarks: \(error)", type: "Error") } } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index 99eb462..0f7a233 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -17,6 +17,9 @@ struct LibraryView: View { @Environment(\.verticalSizeClass) var verticalSizeClass + @State private var selectedBookmark: LibraryItem? = nil + @State private var isDetailActive: Bool = false + @State private var continueWatchingItems: [ContinueWatchingItem] = [] @State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape @@ -98,7 +101,10 @@ struct LibraryView: View { LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: columnsCount), spacing: 12) { ForEach(libraryManager.bookmarks) { item in if let module = moduleManager.modules.first(where: { $0.id.uuidString == item.moduleId }) { - NavigationLink(destination: MediaInfoView(title: item.title, imageUrl: item.imageUrl, href: item.href, module: module)) { + Button(action: { + selectedBookmark = item + isDetailActive = true + }) { VStack(alignment: .leading) { ZStack { KFImage(URL(string: item.imageUrl)) @@ -141,6 +147,22 @@ struct LibraryView: View { } } .padding(.horizontal, 20) + NavigationLink( + destination: Group { + if let bookmark = selectedBookmark, + let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) { + MediaInfoView(title: bookmark.title, + imageUrl: bookmark.imageUrl, + href: bookmark.href, + module: module) + } else { + Text("No Data Available") + } + }, + isActive: $isDetailActive + ) { + EmptyView() + } .onAppear { updateOrientation() } diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 17aa875..bf41b8b 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -29,6 +29,16 @@ struct EpisodeCell: View { @State private var isLoading: Bool = true @State private var currentProgress: Double = 0.0 + @Environment(\.colorScheme) private var colorScheme + @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system + + var defaultBannerImage: String { + let isLightMode = selectedAppearance == .light || (selectedAppearance == .system && colorScheme == .light) + return isLightMode + ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" + : "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png" + } + init(episodeIndex: Int, episode: String, episodeID: Int, progress: Double, itemID: Int, onTap: @escaping (String) -> Void, onMarkAllPrevious: @escaping () -> Void) { self.episodeIndex = episodeIndex @@ -43,7 +53,7 @@ struct EpisodeCell: View { var body: some View { HStack { ZStack { - KFImage(URL(string: episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl)) + KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl)) .resizable() .aspectRatio(16/9, contentMode: .fill) .frame(width: 100, height: 56) @@ -98,7 +108,7 @@ struct EpisodeCell: View { updateProgress() } .onTapGesture { - let imageUrl = episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl + let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl onTap(imageUrl) } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index a4e7d34..d07110d 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -54,7 +54,7 @@ struct SettingsViewPlayer: View { Spacer() Stepper( value: $holdSpeedPlayer, - in: 0.25...2.0, + in: 0.25...2.5, step: 0.25 ) { Text(String(format: "%.2f", holdSpeedPlayer)) diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 8e19b85..c4617a0 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; }; 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; 132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; }; - 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */; }; 132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; @@ -56,7 +55,6 @@ 13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; }; 13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; - 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; 13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; }; 13E62FC22DABC5830007E259 /* Trakt-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E62FC12DABC5830007E259 /* Trakt-Login.swift */; }; 13E62FC42DABC58C0007E259 /* Trakt-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13E62FC32DABC58C0007E259 /* Trakt-Token.swift */; }; @@ -118,7 +116,6 @@ 13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; - 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 13E62FC12DABC5830007E259 /* Trakt-Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Login.swift"; sourceTree = ""; }; @@ -140,7 +137,6 @@ files = ( 13B77E192DA44F8300126FDF /* MarqueeLabel in Frameworks */, 132E35232D959E410007800E /* Kingfisher in Frameworks */, - 132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */, 132E351D2D959DDB0007800E /* Drops in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -262,7 +258,6 @@ isa = PBXGroup; children = ( 136BBE7C2DB102BE00906B5E /* iCloudSyncManager */, - 13DB7CEA2D7DED50004371D3 /* DownloadManager */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, 13103E8C2D58E037000F0673 /* SkeletonCells */, 13DC0C442D302C6A00D0F966 /* MediaPlayer */, @@ -398,14 +393,6 @@ path = Auth; sourceTree = ""; }; - 13DB7CEA2D7DED50004371D3 /* DownloadManager */ = { - isa = PBXGroup; - children = ( - 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */, - ); - path = DownloadManager; - sourceTree = ""; - }; 13DC0C442D302C6A00D0F966 /* MediaPlayer */ = { isa = PBXGroup; children = ( @@ -480,7 +467,6 @@ name = Sulfur; packageProductDependencies = ( 132E351C2D959DDB0007800E /* Drops */, - 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */, 132E35222D959E410007800E /* Kingfisher */, 13B77E182DA44F8300126FDF /* MarqueeLabel */, ); @@ -513,7 +499,6 @@ mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */, - 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */, 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */, 13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */, ); @@ -563,7 +548,6 @@ 133D7C932D2BE2640075467E /* Modules.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, - 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */, 13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */, @@ -840,14 +824,6 @@ kind = branch; }; }; - 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/kewlbear/FFmpeg-iOS-Lame"; - requirement = { - branch = main; - kind = branch; - }; - }; 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; @@ -872,11 +848,6 @@ package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */; productName = Drops; }; - 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */ = { - isa = XCSwiftPackageProductDependency; - package = 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */; - productName = "FFmpeg-iOS-Lame"; - }; 132E35222D959E410007800E /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5fe78fe..a843cd1 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,24 +10,6 @@ "version": null } }, - { - "package": "FFmpeg-iOS-Lame", - "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Lame", - "state": { - "branch": "main", - "revision": "1808fa5a1263c5e216646cd8421fc7dcb70520cc", - "version": null - } - }, - { - "package": "FFmpeg-iOS-Support", - "repositoryURL": "https://github.com/kewlbear/FFmpeg-iOS-Support", - "state": { - "branch": null, - "revision": "be3bd9149ac53760e8725652eee99c405b2be47a", - "version": "0.0.2" - } - }, { "package": "Kingfisher", "repositoryURL": "https://github.com/onevcat/Kingfisher.git", diff --git a/assets/banner1.png b/assets/banner1.png new file mode 100644 index 0000000..72ae268 Binary files /dev/null and b/assets/banner1.png differ