TestFlight update (#36)
|
|
@ -11,10 +11,6 @@ import Kingfisher
|
|||
struct ContentView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeView()
|
||||
.tabItem {
|
||||
Label("Home", systemImage: "house")
|
||||
}
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "books.vertical")
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>processing</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class TMDBSeasonal {
|
|||
|
||||
var request = URLRequest(url: components.url!)
|
||||
let token = TMBDRequest.getToken()
|
||||
print(token)
|
||||
|
||||
request.allHTTPHeaderFields = [
|
||||
"accept": "application/json",
|
||||
|
|
|
|||
215
Sora/Utils/DownloadManager/DownloadManager.swift
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
//
|
||||
// DownloadManager.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 09/03/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FFmpegSupport
|
||||
import UIKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate")
|
||||
}
|
||||
|
||||
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)"])
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,14 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
var watchNextButtonControlsConstraints: [NSLayoutConstraint] = []
|
||||
var isControlsVisible = false
|
||||
|
||||
var subtitleBottomConstraint: NSLayoutConstraint?
|
||||
|
||||
var subtitleBottomPadding: CGFloat = 10.0 {
|
||||
didSet {
|
||||
updateSubtitleLabelConstraints()
|
||||
}
|
||||
}
|
||||
|
||||
init(module: ScrapingModule,
|
||||
urlString: String,
|
||||
fullUrl: String,
|
||||
|
|
@ -85,9 +93,9 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
fatalError("Invalid URL string")
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
if urlString.contains("ascdn") {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
self.player = AVPlayer(playerItem: playerItem)
|
||||
|
|
@ -106,6 +114,10 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
|
||||
// Load persistent subtitle settings on launch
|
||||
loadSubtitleSettings()
|
||||
|
||||
setupPlayerViewController()
|
||||
setupControls()
|
||||
setupSubtitleLabel()
|
||||
|
|
@ -256,12 +268,12 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
playPauseButton.heightAnchor.constraint(equalToConstant: 50),
|
||||
|
||||
backwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
|
||||
backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -30),
|
||||
backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -50),
|
||||
backwardButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
backwardButton.heightAnchor.constraint(equalToConstant: 40),
|
||||
|
||||
forwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
|
||||
forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 30),
|
||||
forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 50),
|
||||
forwardButton.widthAnchor.constraint(equalToConstant: 40),
|
||||
forwardButton.heightAnchor.constraint(equalToConstant: 40)
|
||||
])
|
||||
|
|
@ -275,14 +287,25 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
updateSubtitleLabelAppearance()
|
||||
view.addSubview(subtitleLabel)
|
||||
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
subtitleBottomConstraint = subtitleLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -subtitleBottomPadding)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
subtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
subtitleLabel.bottomAnchor.constraint(equalTo: sliderHostingController?.view.bottomAnchor ?? view.safeAreaLayoutGuide.bottomAnchor),
|
||||
subtitleBottomConstraint!,
|
||||
subtitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36),
|
||||
subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36)
|
||||
])
|
||||
}
|
||||
|
||||
func updateSubtitleLabelConstraints() {
|
||||
subtitleBottomConstraint?.constant = -subtitleBottomPadding
|
||||
view.setNeedsLayout()
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func setupDismissButton() {
|
||||
dismissButton = UIButton(type: .system)
|
||||
dismissButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||
|
|
@ -374,11 +397,7 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
func updateSubtitleLabelAppearance() {
|
||||
subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize))
|
||||
subtitleLabel.textColor = subtitleUIColor()
|
||||
if subtitleBackgroundEnabled {
|
||||
subtitleLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6)
|
||||
} else {
|
||||
subtitleLabel.backgroundColor = .clear
|
||||
}
|
||||
subtitleLabel.backgroundColor = subtitleBackgroundEnabled ? UIColor.black.withAlphaComponent(0.6) : .clear
|
||||
subtitleLabel.layer.cornerRadius = 5
|
||||
subtitleLabel.clipsToBounds = true
|
||||
subtitleLabel.layer.shadowColor = UIColor.black.cgColor
|
||||
|
|
@ -538,42 +557,120 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
if let subURL = subtitlesURL, !subURL.isEmpty {
|
||||
let foregroundActions = [
|
||||
UIAction(title: "White") { _ in self.subtitleForegroundColor = "white"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Yellow") { _ in self.subtitleForegroundColor = "yellow"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Green") { _ in self.subtitleForegroundColor = "green"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Blue") { _ in self.subtitleForegroundColor = "blue"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Red") { _ in self.subtitleForegroundColor = "red"; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Purple") { _ in self.subtitleForegroundColor = "purple"; self.updateSubtitleLabelAppearance() }
|
||||
UIAction(title: "White") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "white" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Yellow") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "yellow" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Green") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "green" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Blue") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "blue" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Red") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "red" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Purple") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = "purple" }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let colorMenu = UIMenu(title: "Subtitle Color", children: foregroundActions)
|
||||
|
||||
let fontSizeActions = [
|
||||
UIAction(title: "16") { _ in self.subtitleFontSize = 16; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "18") { _ in self.subtitleFontSize = 18; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "20") { _ in self.subtitleFontSize = 20; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "22") { _ in self.subtitleFontSize = 22; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "24") { _ in self.subtitleFontSize = 24; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "16") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 16 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "18") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 18 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "20") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 20 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "22") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 22 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "24") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = 24 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Custom") { _ in self.presentCustomFontAlert() }
|
||||
]
|
||||
let fontSizeMenu = UIMenu(title: "Font Size", children: fontSizeActions)
|
||||
|
||||
let shadowActions = [
|
||||
UIAction(title: "None") { _ in self.subtitleShadowRadius = 0; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Low") { _ in self.subtitleShadowRadius = 1; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "Medium") { _ in self.subtitleShadowRadius = 3; self.updateSubtitleLabelAppearance() },
|
||||
UIAction(title: "High") { _ in self.subtitleShadowRadius = 6; self.updateSubtitleLabelAppearance() }
|
||||
UIAction(title: "None") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 0 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Low") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 1 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "Medium") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 3 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
},
|
||||
UIAction(title: "High") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 6 }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let shadowMenu = UIMenu(title: "Shadow Intensity", children: shadowActions)
|
||||
|
||||
let backgroundActions = [
|
||||
UIAction(title: "Toggle") { _ in
|
||||
self.subtitleBackgroundEnabled.toggle()
|
||||
SubtitleSettingsManager.shared.update { settings in settings.backgroundEnabled.toggle() }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
]
|
||||
let backgroundMenu = UIMenu(title: "Background", children: backgroundActions)
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu])
|
||||
let paddingActions = [
|
||||
UIAction(title: "10p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 10 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "20p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 20 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "30p") { _ in
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 30 }
|
||||
self.loadSubtitleSettings()
|
||||
},
|
||||
UIAction(title: "Custom") { _ in self.presentCustomPaddingAlert() }
|
||||
]
|
||||
let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions)
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu])
|
||||
|
||||
menuElements = [subtitleOptionsMenu]
|
||||
}
|
||||
|
|
@ -581,6 +678,26 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
return UIMenu(title: "", children: menuElements)
|
||||
}
|
||||
|
||||
func presentCustomPaddingAlert() {
|
||||
let alert = UIAlertController(title: "Enter Custom Padding", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
textField.placeholder = "Padding Value"
|
||||
textField.keyboardType = .numberPad
|
||||
textField.text = String(Int(self.subtitleBottomPadding))
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
|
||||
if let text = alert.textFields?.first?.text, let intValue = Int(text) {
|
||||
let newSize = CGFloat(intValue)
|
||||
SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = newSize }
|
||||
self.loadSubtitleSettings()
|
||||
}
|
||||
}))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func presentCustomFontAlert() {
|
||||
let alert = UIAlertController(title: "Enter Custom Font Size", message: nil, preferredStyle: .alert)
|
||||
alert.addTextField { textField in
|
||||
|
|
@ -591,7 +708,8 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
|
||||
if let text = alert.textFields?.first?.text, let newSize = Double(text) {
|
||||
self.subtitleFontSize = newSize
|
||||
SubtitleSettingsManager.shared.update { settings in settings.fontSize = newSize }
|
||||
self.loadSubtitleSettings()
|
||||
self.updateSubtitleLabelAppearance()
|
||||
}
|
||||
}))
|
||||
|
|
@ -600,6 +718,15 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func loadSubtitleSettings() {
|
||||
let settings = SubtitleSettingsManager.shared.settings
|
||||
self.subtitleForegroundColor = settings.foregroundColor
|
||||
self.subtitleFontSize = settings.fontSize
|
||||
self.subtitleShadowRadius = settings.shadowRadius
|
||||
self.subtitleBackgroundEnabled = settings.backgroundEnabled
|
||||
self.subtitleBottomPadding = settings.bottomPadding
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
|
||||
return .landscape
|
||||
|
|
@ -631,3 +758,4 @@ class CustomMediaPlayerViewController: UIViewController {
|
|||
|
||||
// yes? Like the plural of the famous american rapper ye? -IBHRAD
|
||||
// low taper fade the meme is massive -cranci
|
||||
// cranci still doesnt have a job -seiike
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// SubtitleSettingsManager.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 09/03/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
struct SubtitleSettings: Codable {
|
||||
var foregroundColor: String = "white"
|
||||
var fontSize: Double = 20.0
|
||||
var shadowRadius: Double = 1.0
|
||||
var backgroundEnabled: Bool = true
|
||||
var bottomPadding: CGFloat = 20.0
|
||||
}
|
||||
|
||||
class SubtitleSettingsManager {
|
||||
static let shared = SubtitleSettingsManager()
|
||||
|
||||
private let userDefaultsKey = "SubtitleSettings"
|
||||
|
||||
var settings: SubtitleSettings {
|
||||
get {
|
||||
if let data = UserDefaults.standard.data(forKey: userDefaultsKey),
|
||||
let savedSettings = try? JSONDecoder().decode(SubtitleSettings.self, from: data) {
|
||||
return savedSettings
|
||||
}
|
||||
return SubtitleSettings()
|
||||
}
|
||||
set {
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
UserDefaults.standard.set(data, forKey: userDefaultsKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(_ updateBlock: (inout SubtitleSettings) -> Void) {
|
||||
var currentSettings = settings
|
||||
updateBlock(¤tSettings)
|
||||
settings = currentSettings
|
||||
}
|
||||
}
|
||||
|
|
@ -39,9 +39,8 @@ class VideoPlayerViewController: UIViewController {
|
|||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
if streamUrl.contains("ascdn") {
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
}
|
||||
request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
|
||||
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", forHTTPHeaderField: "User-Agent")
|
||||
|
||||
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
|
||||
let playerItem = AVPlayerItem(asset: asset)
|
||||
|
|
|
|||
83
Sora/Views/DownloadView.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// DownloadView.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 12/03/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadItem: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let episode: Int
|
||||
let type: String
|
||||
var progress: Double
|
||||
var status: String
|
||||
}
|
||||
|
||||
class DownloadViewModel: ObservableObject {
|
||||
@Published var downloads: [DownloadItem] = []
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateStatus(_:)), name: .DownloadManagerStatusUpdate, object: nil)
|
||||
}
|
||||
|
||||
@objc func updateStatus(_ notification: Notification) {
|
||||
guard let info = notification.userInfo,
|
||||
let title = info["title"] as? String,
|
||||
let episode = info["episode"] as? Int,
|
||||
let type = info["type"] as? String,
|
||||
let status = info["status"] as? String,
|
||||
let progress = info["progress"] as? Double else { return }
|
||||
|
||||
if let index = downloads.firstIndex(where: { $0.title == title && $0.episode == episode }) {
|
||||
downloads[index] = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status)
|
||||
} else {
|
||||
let newDownload = DownloadItem(title: title, episode: episode, type: type, progress: progress, status: status)
|
||||
downloads.append(newDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadView: View {
|
||||
@StateObject var viewModel = DownloadViewModel()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List(viewModel.downloads) { download in
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: iconName(for: download))
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("\(download.title) - Episode \(download.episode)")
|
||||
.font(.headline)
|
||||
|
||||
ProgressView(value: download.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .accentColor))
|
||||
.frame(height: 8)
|
||||
|
||||
Text(download.status)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.navigationTitle("Downloads")
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
func iconName(for download: DownloadItem) -> String {
|
||||
if download.type == "hls" {
|
||||
return download.status.lowercased().contains("converting") ? "arrow.triangle.2.circlepath.circle.fill" : "checkmark.circle.fill"
|
||||
} else {
|
||||
return download.progress >= 1.0 ? "checkmark.circle.fill" : "arrow.down.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,309 +0,0 @@
|
|||
//
|
||||
// HomeView.swift
|
||||
// Sora
|
||||
//
|
||||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HomeView: View {
|
||||
@AppStorage("trackingService") private var tracingService: String = "AniList"
|
||||
@State private var aniListItems: [AniListItem] = []
|
||||
@State private var trendingItems: [AniListItem] = []
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
|
||||
private var currentDeviceSeasonAndYear: (season: String, year: Int) {
|
||||
let currentDate = Date()
|
||||
let calendar = Calendar.current
|
||||
let year = calendar.component(.year, from: currentDate)
|
||||
let month = calendar.component(.month, from: currentDate)
|
||||
|
||||
let season: String
|
||||
switch month {
|
||||
case 1...3:
|
||||
season = "Winter"
|
||||
case 4...6:
|
||||
season = "Spring"
|
||||
case 7...9:
|
||||
season = "Summer"
|
||||
default:
|
||||
season = "Fall"
|
||||
}
|
||||
return (season, year)
|
||||
}
|
||||
|
||||
private var trendingDateString: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "EEEE, dd MMMM yyyy"
|
||||
return formatter.string(from: Date())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
ScrollView {
|
||||
if !continueWatchingItems.isEmpty {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Text("Continue Watching")
|
||||
.font(.headline)
|
||||
.padding(.horizontal, 8)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(continueWatchingItems.reversed())) { item in
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Sora" {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
episodeImageUrl: item.imageUrl
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 240, height: 135)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 240, height: 135)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
KFImage(URL(string: item.module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(4)
|
||||
.padding(4),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.blur(radius: 3)
|
||||
.frame(height: 30)
|
||||
|
||||
ProgressView(value: item.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.padding(.horizontal, 8)
|
||||
.scaleEffect(x: 1, y: 1.5, anchor: .center)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.frame(width: 250, height: 190)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markContinueWatchingItemAsWatched(item: item) }) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeContinueWatchingItem(item: item) }) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
.frame(height: 190)
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(alignment: .bottom, spacing: 5) {
|
||||
Text("Seasonal")
|
||||
.font(.headline)
|
||||
Text("of \(currentDeviceSeasonAndYear.season) \(String(format: "%d", currentDeviceSeasonAndYear.year))")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if aniListItems.isEmpty {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(aniListItems, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
VStack {
|
||||
KFImage(URL(string: item.coverImage.large))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 130, height: 195)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
Text(item.title.romaji)
|
||||
.font(.caption)
|
||||
.frame(width: 130)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
|
||||
HStack(alignment: .bottom, spacing: 5) {
|
||||
Text("Trending")
|
||||
.font(.headline)
|
||||
Text("on \(trendingDateString)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
if trendingItems.isEmpty {
|
||||
ForEach(0..<5, id: \.self) { _ in
|
||||
HomeSkeletonCell()
|
||||
}
|
||||
} else {
|
||||
ForEach(trendingItems, id: \.id) { item in
|
||||
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
|
||||
VStack {
|
||||
KFImage(URL(string: item.coverImage.large))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 130, height: 195)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 130, height: 195)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
||||
Text(item.title.romaji)
|
||||
.font(.caption)
|
||||
.frame(width: 130)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
}
|
||||
.onAppear {
|
||||
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
||||
if tracingService == "TMDB" {
|
||||
TMDBSeasonal.fetchTMDBSeasonal { items in
|
||||
if let items = items {
|
||||
aniListItems = items
|
||||
}
|
||||
}
|
||||
|
||||
TMBDTrending.fetchTMDBTrending { items in
|
||||
if let items = items {
|
||||
trendingItems = items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AnilistServiceSeasonalAnime().fetchSeasonalAnime { items in
|
||||
if let items = items {
|
||||
aniListItems = items
|
||||
}
|
||||
}
|
||||
AnilistServiceTrendingAnime().fetchTrendingAnime { items in
|
||||
if let items = items {
|
||||
trendingItems = items
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) {
|
||||
let key = "lastPlayedTime_\(item.fullUrl)"
|
||||
let totalKey = "totalTime_\(item.fullUrl)"
|
||||
UserDefaults.standard.set(99999999.0, forKey: key)
|
||||
UserDefaults.standard.set(99999999.0, forKey: totalKey)
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
|
||||
if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) {
|
||||
continueWatchingItems.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeContinueWatchingItem(item: ContinueWatchingItem) {
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
|
||||
if let index = continueWatchingItems.firstIndex(where: { $0.id == item.id }) {
|
||||
continueWatchingItems.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,14 +13,16 @@ struct LibraryItem: Codable, Identifiable {
|
|||
let imageUrl: String
|
||||
let href: String
|
||||
let moduleId: String
|
||||
let moduleName: String
|
||||
let dateAdded: Date
|
||||
|
||||
init(title: String, imageUrl: String, href: String, moduleId: String) {
|
||||
init(title: String, imageUrl: String, href: String, moduleId: String, moduleName: String) {
|
||||
self.id = UUID()
|
||||
self.title = title
|
||||
self.imageUrl = imageUrl
|
||||
self.href = href
|
||||
self.moduleId = moduleId
|
||||
self.moduleName = moduleName
|
||||
self.dateAdded = Date()
|
||||
}
|
||||
}
|
||||
|
|
@ -55,15 +57,15 @@ class LibraryManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func isBookmarked(href: String) -> Bool {
|
||||
func isBookmarked(href: String, moduleName: String) -> Bool {
|
||||
bookmarks.contains { $0.href == href }
|
||||
}
|
||||
|
||||
func toggleBookmark(title: String, imageUrl: String, href: String, moduleId: String) {
|
||||
func toggleBookmark(title: String, imageUrl: String, href: String, moduleId: String, moduleName: String) {
|
||||
if let index = bookmarks.firstIndex(where: { $0.href == href }) {
|
||||
bookmarks.remove(at: index)
|
||||
} else {
|
||||
let bookmark = LibraryItem(title: title, imageUrl: imageUrl, href: href, moduleId: moduleId)
|
||||
let bookmark = LibraryItem(title: title, imageUrl: imageUrl, href: href, moduleId: moduleId, moduleName: moduleName)
|
||||
bookmarks.insert(bookmark, at: 0)
|
||||
}
|
||||
saveBookmarks()
|
||||
|
|
|
|||
|
|
@ -12,75 +12,256 @@ struct LibraryView: View {
|
|||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
|
||||
private let columns = [
|
||||
GridItem(.adaptive(minimum: 150), spacing: 16)
|
||||
GridItem(.adaptive(minimum: 150), spacing: 12)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
if libraryManager.bookmarks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "magazine")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No Items saved")
|
||||
.font(.headline)
|
||||
Text("You can bookmark items to find them easily here")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Continue Watching")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if continueWatchingItems.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "play.circle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("No items to continue watching.")
|
||||
.font(.headline)
|
||||
Text("Recently watched content will appear here.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
ContinueWatchingSection(items: $continueWatchingItems, markAsWatched: { item in
|
||||
markContinueWatchingItemAsWatched(item: item)
|
||||
}, removeItem: { item in
|
||||
removeContinueWatchingItem(item: item)
|
||||
})
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
LazyVGrid(columns: columns, spacing: 16) {
|
||||
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)) {
|
||||
VStack {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 150, height: 225)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(2/3, contentMode: .fill)
|
||||
.cornerRadius(10)
|
||||
.frame(width: 150, height: 225)
|
||||
|
||||
Text("Bookmarks")
|
||||
.font(.title2)
|
||||
.bold()
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if libraryManager.bookmarks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "magazine")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
Text("You have no items saved.")
|
||||
.font(.headline)
|
||||
Text("Bookmark items for an easier access later.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
LazyVGrid(columns: columns, 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)) {
|
||||
VStack {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 150, height: 225)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(2/3, contentMode: .fill)
|
||||
.frame(width: 150, height: 225)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(4)
|
||||
.padding(4),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
|
||||
KFImage(URL(string: module.metadata.iconUrl))
|
||||
.placeholder {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 35, height: 35)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(Circle())
|
||||
.padding(5)
|
||||
Text(item.title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
|
||||
Text(item.title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
.onAppear {
|
||||
fetchContinueWatching()
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
private func fetchContinueWatching() {
|
||||
continueWatchingItems = ContinueWatchingManager.shared.fetchItems()
|
||||
}
|
||||
|
||||
private func markContinueWatchingItemAsWatched(item: ContinueWatchingItem) {
|
||||
let key = "lastPlayedTime_\(item.fullUrl)"
|
||||
let totalKey = "totalTime_\(item.fullUrl)"
|
||||
UserDefaults.standard.set(99999999.0, forKey: key)
|
||||
UserDefaults.standard.set(99999999.0, forKey: totalKey)
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
continueWatchingItems.removeAll { $0.id == item.id }
|
||||
}
|
||||
|
||||
private func removeContinueWatchingItem(item: ContinueWatchingItem) {
|
||||
ContinueWatchingManager.shared.remove(item: item)
|
||||
continueWatchingItems.removeAll { $0.id == item.id }
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueWatchingSection: View {
|
||||
@Binding var items: [ContinueWatchingItem]
|
||||
var markAsWatched: (ContinueWatchingItem) -> Void
|
||||
var removeItem: (ContinueWatchingItem) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(items.reversed())) { item in
|
||||
ContinueWatchingCell(item: item,markAsWatched: {
|
||||
markAsWatched(item)
|
||||
}, removeItem: {
|
||||
removeItem(item)
|
||||
})
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.frame(height: 190)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContinueWatchingCell: View {
|
||||
let item: ContinueWatchingItem
|
||||
var markAsWatched: () -> Void
|
||||
var removeItem: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
if UserDefaults.standard.string(forKey: "externalPlayer") == "Default" {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: item.module)
|
||||
videoPlayerViewController.streamUrl = item.streamUrl
|
||||
videoPlayerViewController.fullUrl = item.fullUrl
|
||||
videoPlayerViewController.episodeImageUrl = item.imageUrl
|
||||
videoPlayerViewController.episodeNumber = item.episodeNumber
|
||||
videoPlayerViewController.mediaTitle = item.mediaTitle
|
||||
videoPlayerViewController.subtitles = item.subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: item.module,
|
||||
urlString: item.streamUrl,
|
||||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
subtitlesURL: item.subtitles,
|
||||
episodeImageUrl: item.imageUrl
|
||||
)
|
||||
customMediaPlayer.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.imageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : item.imageUrl))
|
||||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 240, height: 135)
|
||||
.shimmering()
|
||||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.frame(width: 240, height: 135)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
.overlay(
|
||||
KFImage(URL(string: item.module.metadata.iconUrl))
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.cornerRadius(4)
|
||||
.padding(4),
|
||||
alignment: .topLeading
|
||||
)
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.black.opacity(0.3))
|
||||
.blur(radius: 3)
|
||||
.frame(height: 30)
|
||||
|
||||
ProgressView(value: item.progress)
|
||||
.progressViewStyle(LinearProgressViewStyle(tint: .white))
|
||||
.padding(.horizontal, 8)
|
||||
.scaleEffect(x: 1, y: 1.5, anchor: .center)
|
||||
},
|
||||
alignment: .bottom
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text("Episode \(item.episodeNumber)")
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(item.mediaTitle)
|
||||
.font(.caption)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
.frame(width: 240, height: 170)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: { markAsWatched() }) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button(role: .destructive, action: { removeItem() }) {
|
||||
Label("Remove Item", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,34 +15,19 @@ struct EpisodeLink: Identifiable {
|
|||
}
|
||||
|
||||
struct EpisodeCell: View {
|
||||
let episodeIndex: Int
|
||||
let episode: String
|
||||
let episodeID: Int
|
||||
let progress: Double
|
||||
let itemID: Int
|
||||
|
||||
let onTap: (String) -> Void
|
||||
let onMarkAllPrevious: () -> Void
|
||||
|
||||
@State private var episodeTitle: String = ""
|
||||
@State private var episodeImageUrl: String = ""
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var currentProgress: Double = 0.0
|
||||
let onTap: (String) -> Void
|
||||
|
||||
private func markAsWatched() {
|
||||
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(episode)")
|
||||
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(episode)")
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
private func resetProgress() {
|
||||
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(episode)")
|
||||
UserDefaults.standard.set(0.0, forKey: "totalTime_\(episode)")
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episode)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episode)")
|
||||
currentProgress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
|
|
@ -76,31 +61,55 @@ struct EpisodeCell: View {
|
|||
}
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
if currentProgress <= 0.9 {
|
||||
if progress <= 0.9 {
|
||||
Button(action: markAsWatched) {
|
||||
Label("Mark as Watched", systemImage: "checkmark.circle")
|
||||
}
|
||||
}
|
||||
|
||||
if currentProgress != 0 {
|
||||
if progress != 0 {
|
||||
Button(action: resetProgress) {
|
||||
Label("Reset Progress", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
}
|
||||
|
||||
if episodeIndex > 0 {
|
||||
Button(action: onMarkAllPrevious) {
|
||||
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil ||
|
||||
UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") {
|
||||
if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil
|
||||
|| UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") {
|
||||
fetchEpisodeDetails()
|
||||
}
|
||||
updateProgress()
|
||||
currentProgress = progress
|
||||
}
|
||||
.onTapGesture {
|
||||
onTap(episodeImageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchEpisodeDetails() {
|
||||
private func markAsWatched() {
|
||||
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(episode)")
|
||||
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(episode)")
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
private func resetProgress() {
|
||||
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(episode)")
|
||||
UserDefaults.standard.set(0.0, forKey: "totalTime_\(episode)")
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
private func updateProgress() {
|
||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episode)")
|
||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episode)")
|
||||
currentProgress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
}
|
||||
|
||||
private func fetchEpisodeDetails() {
|
||||
guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else {
|
||||
isLoading = false
|
||||
return
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ struct MediaInfoView: View {
|
|||
@State var isRefetching: Bool = true
|
||||
@State var isFetchingEpisode: Bool = false
|
||||
|
||||
@State private var refreshTrigger: Bool = false
|
||||
|
||||
@State private var selectedEpisodeNumber: Int = 0
|
||||
@State private var selectedEpisodeImage: String = ""
|
||||
|
||||
|
|
@ -167,10 +169,11 @@ struct MediaInfoView: View {
|
|||
title: title,
|
||||
imageUrl: imageUrl,
|
||||
href: href,
|
||||
moduleId: module.id.uuidString
|
||||
moduleId: module.id.uuidString,
|
||||
moduleName: module.metadata.sourceName
|
||||
)
|
||||
}) {
|
||||
Image(systemName: libraryManager.isBookmarked(href: href) ? "bookmark.fill" : "bookmark")
|
||||
Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 27)
|
||||
.foregroundColor(Color.accentColor)
|
||||
|
|
@ -209,15 +212,34 @@ struct MediaInfoView: View {
|
|||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||
|
||||
EpisodeCell(episode: ep.href, episodeID: ep.number - 1, progress: progress, itemID: itemID ?? 0, onTap: { imageUrl in
|
||||
EpisodeCell(
|
||||
episodeIndex: i,
|
||||
episode: ep.href,
|
||||
episodeID: ep.number - 1,
|
||||
progress: progress,
|
||||
itemID: itemID ?? 0,
|
||||
onTap: { imageUrl in
|
||||
if !isFetchingEpisode {
|
||||
selectedEpisodeNumber = ep.number
|
||||
selectedEpisodeImage = imageUrl
|
||||
fetchStream(href: ep.href)
|
||||
AnalyticsManager.shared.sendEvent(event: "watch", additionalData: ["title": title, "episode": ep.number])
|
||||
AnalyticsManager.shared.sendEvent(
|
||||
event: "watch",
|
||||
additionalData: ["title": title, "episode": ep.number]
|
||||
)
|
||||
}
|
||||
},
|
||||
onMarkAllPrevious: {
|
||||
for idx in 0..<i {
|
||||
let href = episodeLinks[idx].href
|
||||
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(href)")
|
||||
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(href)")
|
||||
}
|
||||
refreshTrigger.toggle()
|
||||
Logger.shared.log("Marked \(ep.number) episodes watched within anime \"\(title)\".", type: "General")
|
||||
}
|
||||
)
|
||||
.id(refreshTrigger)
|
||||
.disabled(isFetchingEpisode)
|
||||
}
|
||||
}
|
||||
|
|
@ -261,7 +283,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
.onAppear {
|
||||
if !hasFetched {
|
||||
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching", duration: 1.0, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 1.0, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||
fetchDetails()
|
||||
fetchItemID(byTitle: title) { result in
|
||||
switch result {
|
||||
|
|
@ -489,7 +511,14 @@ struct MediaInfoView: View {
|
|||
|
||||
func playStream(url: String, fullURL: String, subtitles: String? = nil) {
|
||||
DispatchQueue.main.async {
|
||||
let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Default"
|
||||
guard let streamURL = URL(string: url) else {
|
||||
Logger.shared.log("Invalid stream URL: \(url)", type: "Error")
|
||||
handleStreamFailure()
|
||||
return
|
||||
}
|
||||
let subtitleFileURL = subtitles != nil ? URL(string: subtitles!) : nil
|
||||
|
||||
let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora"
|
||||
var scheme: String?
|
||||
|
||||
switch externalPlayer {
|
||||
|
|
@ -501,7 +530,29 @@ struct MediaInfoView: View {
|
|||
scheme = "outplayer://\(url)"
|
||||
case "nPlayer":
|
||||
scheme = "nplayer-\(url)"
|
||||
case "Sora":
|
||||
case "Default":
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: module)
|
||||
videoPlayerViewController.streamUrl = url
|
||||
videoPlayerViewController.fullUrl = fullURL
|
||||
videoPlayerViewController.episodeNumber = selectedEpisodeNumber
|
||||
videoPlayerViewController.episodeImageUrl = selectedEpisodeImage
|
||||
videoPlayerViewController.mediaTitle = title
|
||||
videoPlayerViewController.subtitles = subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let scheme = scheme, let url = URL(string: scheme), UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
Logger.shared.log("Opening external app with scheme: \(url)", type: "General")
|
||||
} else {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
module: module,
|
||||
urlString: url,
|
||||
|
|
@ -521,28 +572,6 @@ struct MediaInfoView: View {
|
|||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let scheme = scheme, let url = URL(string: scheme), UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
Logger.shared.log("Opening external app with scheme: \(url)", type: "General")
|
||||
} else {
|
||||
let videoPlayerViewController = VideoPlayerViewController(module: module)
|
||||
videoPlayerViewController.streamUrl = url
|
||||
videoPlayerViewController.fullUrl = fullURL
|
||||
videoPlayerViewController.episodeNumber = selectedEpisodeNumber
|
||||
videoPlayerViewController.episodeImageUrl = selectedEpisodeImage
|
||||
videoPlayerViewController.mediaTitle = title
|
||||
videoPlayerViewController.subtitles = subtitles ?? ""
|
||||
videoPlayerViewController.modalPresentationStyle = .fullScreen
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ struct SearchView: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
if let selectedModule = selectedModule {
|
||||
Text(selectedModule.metadata.sourceName)
|
||||
.font(.headline)
|
||||
|
|
@ -161,6 +161,8 @@ struct SearchView: View {
|
|||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.id("moduleMenuHStack")
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ struct SettingsViewData: View {
|
|||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("App storage")) {
|
||||
Section(header: Text("App storage"), footer: Text("The caches used by Sora are stored images that help load content faster\n\nThe App Data should never be erased if you dont know what that will cause.\n\nClearing the documents folder will remove all the modules and downloads")) {
|
||||
Button(action: clearCache) {
|
||||
Text("Clear Cache")
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ struct SettingsViewData: View {
|
|||
.alert(isPresented: $showEraseAppDataAlert) {
|
||||
Alert(
|
||||
title: Text("Confirm Erase App Data"),
|
||||
message: Text("Are you sure you want to erase all app data? This action cannot be undone. (The app will then restart)"),
|
||||
message: Text("Are you sure you want to erase all app data? This action cannot be undone. (The app will then close)"),
|
||||
primaryButton: .destructive(Text("Erase")) {
|
||||
eraseAppData()
|
||||
},
|
||||
|
|
@ -43,7 +43,7 @@ struct SettingsViewData: View {
|
|||
.alert(isPresented: $showRemoveDocumentsAlert) {
|
||||
Alert(
|
||||
title: Text("Confirm Remove All Files"),
|
||||
message: Text("Are you sure you want to remove all files in the documents folder? This will also remove all modules and you will lose the favorite items. This action cannot be undone. (The app will then restart)"),
|
||||
message: Text("Are you sure you want to remove all files in the documents folder? This will also remove all modules and you will lose the favorite items. This action cannot be undone. (The app will then close)"),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
removeAllFilesInDocuments()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ struct SettingsViewGeneral: View {
|
|||
@AppStorage("refreshModulesOnLaunch") private var refreshModulesOnLaunch: Bool = false
|
||||
@AppStorage("fetchEpisodeMetadata") private var fetchEpisodeMetadata: Bool = true
|
||||
@AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false
|
||||
@AppStorage("multiThreads") private var multiThreadsEnabled: Bool = false
|
||||
@AppStorage("metadataProviders") private var metadataProviders: String = "AniList"
|
||||
private let metadataProvidersList = ["AniList"]
|
||||
@EnvironmentObject var settings: Settings
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -52,12 +55,32 @@ struct SettingsViewGeneral: View {
|
|||
}
|
||||
Toggle("Fetch Episode metadata", isOn: $fetchEpisodeMetadata)
|
||||
.tint(.accentColor)
|
||||
HStack {
|
||||
Text("Metadata Provider")
|
||||
Spacer()
|
||||
Menu(metadataProviders) {
|
||||
ForEach(metadataProvidersList, id: \.self) { provider in
|
||||
Button(action: {
|
||||
metadataProviders = provider
|
||||
}) {
|
||||
Text(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//Section(header: Text("Downloads"), footer: Text("Note that the modules will be replaced only if there is a different version string inside the JSON file.")) {
|
||||
// Toggle("Multi Threads conversion", isOn: $multiThreadsEnabled)
|
||||
// .tint(.accentColor)
|
||||
//}
|
||||
|
||||
Section(header: Text("Modules"), footer: Text("Note that the modules will be replaced only if there is a different version string inside the JSON file.")) {
|
||||
Toggle("Refresh Modules on Launch", isOn: $refreshModulesOnLaunch)
|
||||
.tint(.accentColor)
|
||||
}
|
||||
|
||||
Section(header: Text("Analytics"), footer: Text("Allow Sora to collect anonymous data to improve the app. No personal information is collected. This can be disabled at any time.\n\n Information collected: \n- App version\n- Device model\n- Module Name/Version\n- Error Messages\n- Title of Watched Content")) {
|
||||
Toggle("Enable Analytics", isOn: $analyticsEnabled)
|
||||
.tint(.accentColor)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsViewPlayer: View {
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Default"
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Sora"
|
||||
@AppStorage("alwaysLandscape") private var isAlwaysLandscape = false
|
||||
@AppStorage("hideNextButton") private var isHideNextButton = false
|
||||
@AppStorage("rememberPlaySpeed") private var isRememberPlaySpeed = false
|
||||
|
|
@ -56,7 +56,88 @@ struct SettingsViewPlayer: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubtitleSettingsSection()
|
||||
}
|
||||
.navigationTitle("Player")
|
||||
}
|
||||
}
|
||||
|
||||
struct SubtitleSettingsSection: View {
|
||||
@State private var foregroundColor: String = SubtitleSettingsManager.shared.settings.foregroundColor
|
||||
@State private var fontSize: Double = SubtitleSettingsManager.shared.settings.fontSize
|
||||
@State private var shadowRadius: Double = SubtitleSettingsManager.shared.settings.shadowRadius
|
||||
@State private var backgroundEnabled: Bool = SubtitleSettingsManager.shared.settings.backgroundEnabled
|
||||
@State private var bottomPadding: CGFloat = SubtitleSettingsManager.shared.settings.bottomPadding
|
||||
|
||||
private let colors = ["white", "yellow", "green", "blue", "red", "purple"]
|
||||
private let shadowOptions = [0, 1, 3, 6]
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Subtitle Settings")) {
|
||||
HStack {
|
||||
Text("Subtitle Color")
|
||||
Spacer()
|
||||
Menu(foregroundColor) {
|
||||
ForEach(colors, id: \.self) { color in
|
||||
Button(action: {
|
||||
foregroundColor = color
|
||||
SubtitleSettingsManager.shared.update { settings in
|
||||
settings.foregroundColor = color
|
||||
}
|
||||
}) {
|
||||
Text(color.capitalized)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Shadow")
|
||||
Spacer()
|
||||
Menu("\(Int(shadowRadius))") {
|
||||
ForEach(shadowOptions, id: \.self) { option in
|
||||
Button(action: {
|
||||
shadowRadius = Double(option)
|
||||
SubtitleSettingsManager.shared.update { settings in
|
||||
settings.shadowRadius = Double(option)
|
||||
}
|
||||
}) {
|
||||
Text("\(option)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Background Enabled", isOn: $backgroundEnabled)
|
||||
.tint(.accentColor)
|
||||
.onChange(of: backgroundEnabled) { newValue in
|
||||
SubtitleSettingsManager.shared.update { settings in
|
||||
settings.backgroundEnabled = newValue
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Font Size:")
|
||||
Spacer()
|
||||
Stepper("\(Int(fontSize))", value: $fontSize, in: 12...36, step: 1)
|
||||
.onChange(of: fontSize) { newValue in
|
||||
SubtitleSettingsManager.shared.update { settings in
|
||||
settings.fontSize = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Bottom Padding:")
|
||||
Spacer()
|
||||
Stepper("\(Int(bottomPadding))", value: $bottomPadding, in: 0...50, step: 1)
|
||||
.onChange(of: bottomPadding) { newValue in
|
||||
SubtitleSettingsManager.shared.update { settings in
|
||||
settings.bottomPadding = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
//
|
||||
// SettingsViewTrackingServices.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 05/03/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct SettingsViewTrackingServices: View {
|
||||
@AppStorage("trackingService") private var trackingService: String = "AniList"
|
||||
@EnvironmentObject var settings: Settings
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Tracking Service")) {
|
||||
HStack {
|
||||
Text("Service")
|
||||
Spacer()
|
||||
Menu {
|
||||
Button(action: { trackingService = "AniList" }) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://avatars.githubusercontent.com/u/18018524?s=280&v=4"))
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
Text("AniList")
|
||||
}
|
||||
}
|
||||
Button(action: { trackingService = "TMDB" }) {
|
||||
HStack {
|
||||
KFImage(URL(string: "https://pbs.twimg.com/profile_images/1243623122089041920/gVZIvphd_400x400.jpg"))
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
Text("TMDB")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
KFImage(URL(string: trackingService == "TMDB" ? "https://pbs.twimg.com/profile_images/1243623122089041920/gVZIvphd_400x400.jpg" : "https://avatars.githubusercontent.com/u/18018524?s=280&v=4"))
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
Text(trackingService)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Tracking Service")
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,6 @@ struct SettingsView: View {
|
|||
NavigationLink(destination: SettingsViewModule()) {
|
||||
Text("Modules")
|
||||
}
|
||||
NavigationLink(destination: SettingsViewTrackingServices()) {
|
||||
Text("Tracking Services")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Info")) {
|
||||
|
|
@ -49,6 +46,19 @@ struct SettingsView: View {
|
|||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://discord.gg/x7hppDWFDZ") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Join the Discord")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/cranci1/Sora/issues") {
|
||||
UIApplication.shared.open(url)
|
||||
|
|
@ -63,12 +73,12 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://discord.gg/x7hppDWFDZ") {
|
||||
if let url = URL(string: "https://github.com/cranci1/Sora/blob/dev/LICENSE") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text("Join the Discord")
|
||||
Text("License (GPLv3.0)")
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
|
|
@ -76,7 +86,7 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
Section(footer: Text("Running Sora 0.2.1")) {}
|
||||
Section(footer: Text("Running Sora 0.2.0 - cranci1")) {}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; };
|
||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; };
|
||||
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */; };
|
||||
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E852D58A328000F0673 /* AniList-Trending.swift */; };
|
||||
|
|
@ -20,13 +21,11 @@
|
|||
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF4E2D786C9E007E289F /* TMDB-Trending.swift */; };
|
||||
1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF512D7871B7007E289F /* TMDBItem.swift */; };
|
||||
1334FF542D787217007E289F /* TMDBRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF532D787217007E289F /* TMDBRequest.swift */; };
|
||||
1334FF562D7872E9007E289F /* SettingsViewTrackingServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF552D7872E9007E289F /* SettingsViewTrackingServices.swift */; };
|
||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; };
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; };
|
||||
133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; };
|
||||
133D7C752D2BE2520075467E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C742D2BE2520075467E /* Preview Assets.xcassets */; };
|
||||
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7C2D2BE2630075467E /* SearchView.swift */; };
|
||||
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7D2D2BE2630075467E /* HomeView.swift */; };
|
||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C7E2D2BE2630075467E /* LibraryView.swift */; };
|
||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C802D2BE2630075467E /* MediaInfoView.swift */; };
|
||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C822D2BE2630075467E /* SettingsView.swift */; };
|
||||
|
|
@ -53,6 +52,9 @@
|
|||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; };
|
||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; };
|
||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; };
|
||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
|
||||
13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; };
|
||||
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; };
|
||||
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; };
|
||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */; };
|
||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD32D32D97400C1EBD7 /* Double+Extension.swift */; };
|
||||
|
|
@ -62,6 +64,7 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = "<group>"; };
|
||||
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
|
||||
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
|
||||
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -76,14 +79,12 @@
|
|||
1334FF4E2D786C9E007E289F /* TMDB-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TMDB-Trending.swift"; sourceTree = "<group>"; };
|
||||
1334FF512D7871B7007E289F /* TMDBItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TMDBItem.swift; sourceTree = "<group>"; };
|
||||
1334FF532D787217007E289F /* TMDBRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TMDBRequest.swift; sourceTree = "<group>"; };
|
||||
1334FF552D7872E9007E289F /* SettingsViewTrackingServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackingServices.swift; sourceTree = "<group>"; };
|
||||
133D7C6A2D2BE2500075467E /* Sulfur.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sulfur.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
133D7C6D2D2BE2500075467E /* SoraApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraApp.swift; sourceTree = "<group>"; };
|
||||
133D7C6F2D2BE2500075467E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
133D7C712D2BE2520075467E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
133D7C742D2BE2520075467E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
133D7C7C2D2BE2630075467E /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
133D7C7D2D2BE2630075467E /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
133D7C7E2D2BE2630075467E /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
133D7C802D2BE2630075467E /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = "<group>"; };
|
||||
133D7C822D2BE2630075467E /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -108,6 +109,8 @@
|
|||
13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = "<group>"; };
|
||||
13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.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>"; };
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -122,6 +125,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */,
|
||||
1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */,
|
||||
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */,
|
||||
);
|
||||
|
|
@ -258,7 +262,7 @@
|
|||
1399FAD22D3AB34F00E97C31 /* SettingsView */,
|
||||
133F55B92D33B53E00E08EEA /* LibraryView */,
|
||||
133D7C7C2D2BE2630075467E /* SearchView.swift */,
|
||||
133D7C7D2D2BE2630075467E /* HomeView.swift */,
|
||||
130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -281,7 +285,6 @@
|
|||
131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */,
|
||||
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */,
|
||||
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */,
|
||||
1334FF552D7872E9007E289F /* SettingsViewTrackingServices.swift */,
|
||||
);
|
||||
path = SettingsSubViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -289,6 +292,7 @@
|
|||
133D7C852D2BE2640075467E /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13DB7CEA2D7DED50004371D3 /* DownloadManager */,
|
||||
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */,
|
||||
13103E8C2D58E037000F0673 /* SkeletonCells */,
|
||||
13DC0C442D302C6A00D0F966 /* MediaPlayer */,
|
||||
|
|
@ -400,6 +404,14 @@
|
|||
path = Drops;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13DB7CEA2D7DED50004371D3 /* DownloadManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */,
|
||||
);
|
||||
path = DownloadManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13DC0C442D302C6A00D0F966 /* MediaPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -416,6 +428,7 @@
|
|||
13EA2BD22D32D97400C1EBD7 /* Components */,
|
||||
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */,
|
||||
13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */,
|
||||
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */,
|
||||
);
|
||||
path = CustomPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -448,6 +461,7 @@
|
|||
packageProductDependencies = (
|
||||
133D7C962D2BE2AF0075467E /* Kingfisher */,
|
||||
1359ED192D76FA7D00C13034 /* Drops */,
|
||||
13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */,
|
||||
);
|
||||
productName = Sora;
|
||||
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
|
||||
|
|
@ -479,6 +493,7 @@
|
|||
packageReferences = (
|
||||
133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */,
|
||||
13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
|
||||
);
|
||||
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -522,9 +537,10 @@
|
|||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||
1334FF562D7872E9007E289F /* SettingsViewTrackingServices.swift in Sources */,
|
||||
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
|
||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */,
|
||||
1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */,
|
||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
||||
|
|
@ -535,7 +551,6 @@
|
|||
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */,
|
||||
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */,
|
||||
13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */,
|
||||
133D7C8D2D2BE2640075467E /* HomeView.swift in Sources */,
|
||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */,
|
||||
13103E8B2D58E028000F0673 /* View.swift in Sources */,
|
||||
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */,
|
||||
|
|
@ -545,6 +560,7 @@
|
|||
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
||||
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
||||
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */,
|
||||
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
|
||||
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
|
||||
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
|
||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||
|
|
@ -702,6 +718,7 @@
|
|||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -743,6 +760,7 @@
|
|||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -800,6 +818,14 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kewlbear/FFmpeg-iOS-Lame";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
|
@ -813,6 +839,11 @@
|
|||
package = 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */;
|
||||
productName = Drops;
|
||||
};
|
||||
13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 13DB7CC42D7DC7D2004371D3 /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */;
|
||||
productName = "FFmpeg-iOS-Lame";
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 133D7C622D2BE2500075467E /* Project object */;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,24 @@
|
|||
"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/screenshots/ios-1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/screenshots/ios-2.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
assets/screenshots/ios-3.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/screenshots/ios-4.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
assets/screenshots/ipad-1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/screenshots/ipad-2.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/screenshots/ipad-3.png
Normal file
|
After Width: | Height: | Size: 932 KiB |
BIN
assets/screenshots/ipad-4.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/screenshots/ipad-5.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |