TestFlight update (#36)

This commit is contained in:
cranci 2025-03-14 17:05:37 +01:00 committed by GitHub
commit 4db8539739
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1015 additions and 525 deletions

View file

@ -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")

View file

@ -30,6 +30,7 @@
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>
</dict>
</plist>

View file

@ -19,7 +19,6 @@ class TMDBSeasonal {
var request = URLRequest(url: components.url!)
let token = TMBDRequest.getToken()
print(token)
request.allHTTPHeaderFields = [
"accept": "application/json",

View 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)
}
}
}

View file

@ -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

View file

@ -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(&currentSettings)
settings = currentSettings
}
}

View file

@ -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)

View 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"
}
}
}

View file

@ -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)
}
}
}

View file

@ -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()

View file

@ -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")
}
}
}
}

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
},

View file

@ -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)

View file

@ -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
}
}
}
}
}
}

View file

@ -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")
}
}

View file

@ -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")
}

View file

@ -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 */;

View file

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB