mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
many improvements (#107)
* few player bug fixes (#104)
* icloud safe checking
* more tests
* removed ffmpeg sorry
* test
* Revert "test"
This reverts commit cbf7412d47.
* custom player stuffs idk if it builds
* fire Seiike moment
* ok my fault this time
* Create banner1.png
* seiike ahh moment
* added light mode banner
* Update EpisodeCell.swift
* seiike ahh moment x2
* ops
* fixed intros skipper buttons
* fixed pan crashes
* added speed indicator for hold speed
---------
Co-authored-by: Seiike <122684677+Seeike@users.noreply.github.com>
This commit is contained in:
parent
cefd996115
commit
8c73798195
11 changed files with 414 additions and 514 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
let introSegments: [ClosedRange<T>] // Changed
|
||||
let outroSegments: [ClosedRange<T>] // Changed
|
||||
let introSegments: [ClosedRange<T>]
|
||||
let outroSegments: [ClosedRange<T>]
|
||||
let introColor: Color
|
||||
let outroColor: Color
|
||||
|
||||
|
|
@ -57,10 +57,10 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Rest of the existing code...
|
||||
Capsule()
|
||||
.fill(emptyColor)
|
||||
}
|
||||
.clipShape(Capsule())
|
||||
|
||||
Capsule()
|
||||
.fill(isActive ? activeFillColor : fillColor)
|
||||
|
|
|
|||
|
|
@ -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<Double>] = []
|
||||
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
|
||||
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,
|
||||
constant: -20
|
||||
)
|
||||
subtitleBottomToSliderConstraint = subtitleLabel.bottomAnchor.constraint(
|
||||
equalTo: sliderHostingController?.view.topAnchor ?? view.bottomAnchor,
|
||||
constant: -20
|
||||
)
|
||||
|
||||
subtitleBottomToSafeAreaConstraint = subtitleLabel.bottomAnchor.constraint(
|
||||
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
|
||||
constant: -subtitleBottomPadding
|
||||
)
|
||||
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)
|
||||
])
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<Any>) || (value is Dictionary<String, Any>) {
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = "<group>"; };
|
||||
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
|
||||
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
|
||||
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
|
||||
13E62FC12DABC5830007E259 /* Trakt-Login.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trakt-Login.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>";
|
||||
};
|
||||
13DB7CEA2D7DED50004371D3 /* DownloadManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */,
|
||||
);
|
||||
path = DownloadManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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" */;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
BIN
assets/banner1.png
Normal file
BIN
assets/banner1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
Loading…
Reference in a new issue