Merge branch 'dev' into macos

This commit is contained in:
Francesco 2025-03-28 14:29:24 +01:00
commit 16a52217e1
11 changed files with 261 additions and 182 deletions

View file

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Sora may requires access to your device's camera.</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

View file

@ -11,13 +11,11 @@ extension JSContext {
func setupConsoleLogging() {
let consoleObject = JSValue(newObjectIn: self)
// Set up console.log
let consoleLogFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log(message, type: "Debug")
}
consoleObject?.setObject(consoleLogFunction, forKeyedSubscript: "log" as NSString)
// Set up console.error
let consoleErrorFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log(message, type: "Error")
}
@ -25,7 +23,6 @@ extension JSContext {
self.setObject(consoleObject, forKeyedSubscript: "console" as NSString)
// Global log function
let logFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log("JavaScript log: \(message)", type: "Debug")
}
@ -45,7 +42,7 @@ extension JSContext {
request.setValue(value, forHTTPHeaderField: key)
}
}
let task = URLSession.cloudflareCustom.dataTask(with: request) { data, _, error in
let task = URLSession.custom.dataTask(with: request) { data, _, error in
if let error = error {
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error")
reject.call(withArguments: [error.localizedDescription])
@ -78,70 +75,127 @@ extension JSContext {
}
func setupFetchV2() {
let fetchV2NativeFunction: @convention(block) (String, [String: String]?, JSValue, JSValue) -> Void = { urlString, headers, resolve, reject in
let fetchV2NativeFunction: @convention(block) (String, [String: String]?, String?, String?, JSValue, JSValue) -> Void = { urlString, headers, method, body, resolve, reject in
guard let url = URL(string: urlString) else {
Logger.shared.log("Invalid URL", type: "Error")
reject.call(withArguments: ["Invalid URL"])
return
}
let httpMethod = method ?? "GET"
var request = URLRequest(url: url)
request.httpMethod = httpMethod
Logger.shared.log("FetchV2 Request: URL=\(url), Method=\(httpMethod), Body=\(body ?? "nil")", type: "Debug")
if httpMethod == "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" {
Logger.shared.log("GET request must not have a body", type: "Error")
reject.call(withArguments: ["GET request must not have a body"])
return
}
if httpMethod != "GET", let body = body, !body.isEmpty, body != "null", body != "undefined" {
request.httpBody = body.data(using: .utf8)
}
if let headers = headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
let task = URLSession.cloudflareCustom.dataTask(with: request) { data, response, error in
let task = URLSession.custom.downloadTask(with: request) { tempFileURL, response, error in
if let error = error {
Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error")
reject.call(withArguments: [error.localizedDescription])
return
}
guard let data = data else {
guard let tempFileURL = tempFileURL else {
Logger.shared.log("No data in response", type: "Error")
reject.call(withArguments: ["No data"])
return
}
// Just pass the raw data string and let JavaScript handle it
if let text = String(data: data, encoding: .utf8) {
resolve.call(withArguments: [text])
} else {
Logger.shared.log("Unable to decode data to text", type: "Error")
reject.call(withArguments: ["Unable to decode data"])
do {
let data = try Data(contentsOf: tempFileURL)
if data.count > 10_000_000 {
Logger.shared.log("Response exceeds maximum size", type: "Error")
reject.call(withArguments: ["Response exceeds maximum size"])
return
}
if let text = String(data: data, encoding: .utf8) {
resolve.call(withArguments: [text])
} else {
Logger.shared.log("Unable to decode data to text", type: "Error")
reject.call(withArguments: ["Unable to decode data"])
}
} catch {
Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: "Error")
reject.call(withArguments: ["Error reading downloaded file"])
}
}
task.resume()
}
self.setObject(fetchV2NativeFunction, forKeyedSubscript: "fetchV2Native" as NSString)
// Simpler fetchv2 implementation with text() and json() methods
let fetchv2Definition = """
function fetchv2(url, headers) {
return new Promise(function(resolve, reject) {
fetchV2Native(url, headers, function(rawText) {
const responseObj = {
_data: rawText,
text: function() {
return Promise.resolve(this._data);
},
json: function() {
try {
return Promise.resolve(JSON.parse(this._data));
} catch (e) {
return Promise.reject("JSON parse error: " + e.message);
}
}
};
resolve(responseObj);
}, reject);
});
}
"""
function fetchv2(url, headers = {}, method = "GET", body = null) {
if (method === "GET") {
return new Promise(function(resolve, reject) {
fetchV2Native(url, headers, method, null, function(rawText) { // Pass `null` explicitly
const responseObj = {
_data: rawText,
text: function() {
return Promise.resolve(this._data);
},
json: function() {
try {
return Promise.resolve(JSON.parse(this._data));
} catch (e) {
return Promise.reject("JSON parse error: " + e.message);
}
}
};
resolve(responseObj);
}, reject);
});
}
// Ensure body is properly serialized
const processedBody = body ? JSON.stringify(body) : null;
return new Promise(function(resolve, reject) {
fetchV2Native(url, headers, method, processedBody, function(rawText) {
const responseObj = {
_data: rawText,
text: function() {
return Promise.resolve(this._data);
},
json: function() {
try {
return Promise.resolve(JSON.parse(this._data));
} catch (e) {
return Promise.reject("JSON parse error: " + e.message);
}
}
};
resolve(responseObj);
}, reject);
});
}
"""
self.evaluateScript(fetchv2Definition)
}
func setupBase64Functions() {
// btoa function: converts binary string to base64-encoded ASCII string
let btoaFunction: @convention(block) (String) -> String? = { data in
guard let data = data.data(using: .utf8) else {
Logger.shared.log("btoa: Failed to encode input as UTF-8", type: "Error")
@ -150,7 +204,6 @@ extension JSContext {
return data.base64EncodedString()
}
// atob function: decodes base64-encoded ASCII string to binary string
let atobFunction: @convention(block) (String) -> String? = { base64String in
guard let data = Data(base64Encoded: base64String) else {
Logger.shared.log("atob: Invalid base64 input", type: "Error")
@ -160,12 +213,10 @@ extension JSContext {
return String(data: data, encoding: .utf8)
}
// Add the functions to the JavaScript context
self.setObject(btoaFunction, forKeyedSubscript: "btoa" as NSString)
self.setObject(atobFunction, forKeyedSubscript: "atob" as NSString)
}
// Helper method to set up all JavaScript functionality
func setupJavaScriptEnvironment() {
setupConsoleLogging()
setupNativeFetch()

View file

@ -6,60 +6,41 @@
//
import Foundation
import Network
extension URLSession {
static let userAgents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.2365.92",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.2277.128",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0",
"Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.105 Mobile Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Android 14; Mobile; rv:123.0) Gecko/123.0 Firefox/123.0",
"Mozilla/5.0 (Android 13; Mobile; rv:122.0) Gecko/122.0 Firefox/122.0"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.2569.45",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.2478.89",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15_1_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.0 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15.1; rv:128.0) Gecko/20100101 Firefox/128.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15.0; rv:127.0) Gecko/20100101 Firefox/127.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0",
"Mozilla/5.0 (Linux; Android 15; SM-S928B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6782.112 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6782.112 Mobile Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.3 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (iPad; CPU OS 18_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/19.3 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Android 15; Mobile; rv:128.0) Gecko/128.0 Firefox/128.0",
"Mozilla/5.0 (Android 15; Mobile; rv:127.0) Gecko/127.0 Firefox/127.0"
]
static let randomUserAgent: String = {
static var randomUserAgent: String = {
userAgents.randomElement() ?? userAgents[0]
}()
static let custom: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": randomUserAgent
]
return URLSession(configuration: configuration)
}()
static let cloudflareCustom: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": randomUserAgent
]
let dnsSettings: [AnyHashable: Any] = [
"DNSSettings": [
"ServerAddresses": ["1.1.1.1", "1.0.0.1"]
]
]
configuration.connectionProxyDictionary = dnsSettings
configuration.httpAdditionalHeaders = ["User-Agent": randomUserAgent]
return URLSession(configuration: configuration)
}()
}

View file

@ -36,7 +36,7 @@ class JSController: ObservableObject {
return
}
URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
@ -77,7 +77,7 @@ class JSController: ObservableObject {
return
}
URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
@ -130,7 +130,7 @@ class JSController: ObservableObject {
return
}
URLSession.cloudflareCustom.dataTask(with: url) { [weak self] data, _, error in
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
guard let self = self else { return }
if let error = error {
@ -430,7 +430,7 @@ class JSController: ObservableObject {
func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, completion: @escaping ((stream: String?, subtitles: String?)) -> Void) {
let url = URL(string: episodeUrl)!
let task = URLSession.cloudflareCustom.dataTask(with: url) { data, response, error in
let task = URLSession.custom.dataTask(with: url) { data, response, error in
if let error = error {
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error")
DispatchQueue.main.async { completion((nil, nil)) }

View file

@ -709,20 +709,16 @@ class CustomMediaPlayerViewController: UIViewController {
)
}
// Watch Next Button Logic:
let hideNext = UserDefaults.standard.bool(forKey: "hideNextButton")
let isNearEnd = (self.duration - self.currentTimeVal) <= (self.duration * 0.10)
&& self.currentTimeVal != self.duration
&& self.showWatchNextButton
&& self.duration != 0
if isNearEnd {
// First appearance: show the button in its normal position.
if !self.isWatchNextVisible {
self.isWatchNextVisible = true
self.watchNextButtonAppearedAt = self.currentTimeVal
// Choose constraints based on current controls visibility.
if self.isControlsVisible {
NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints)
@ -730,7 +726,6 @@ class CustomMediaPlayerViewController: UIViewController {
NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints)
NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints)
}
// Soft fade-in.
self.watchNextButton.isHidden = false
self.watchNextButton.alpha = 0.0
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: {
@ -738,23 +733,19 @@ class CustomMediaPlayerViewController: UIViewController {
}, completion: nil)
}
// When 5 seconds have elapsed from when the button first appeared:
if let appearedAt = self.watchNextButtonAppearedAt,
(self.currentTimeVal - appearedAt) >= 5,
!self.isWatchNextRepositioned {
// Fade out the button first (even if controls are visible).
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: {
self.watchNextButton.alpha = 0.0
}, completion: { _ in
self.watchNextButton.isHidden = true
// Then lock it to the controls-attached constraints.
NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints)
self.isWatchNextRepositioned = true
})
}
} else {
// Not near end: reset the watch-next button state.
self.watchNextButtonAppearedAt = nil
self.isWatchNextVisible = false
self.isWatchNextRepositioned = false
@ -771,7 +762,6 @@ class CustomMediaPlayerViewController: UIViewController {
func repositionWatchNextButton() {
self.isWatchNextRepositioned = true
// Update constraints so the button is now attached next to the playback controls.
UIView.animate(withDuration: 0.3, animations: {
NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints)
@ -799,7 +789,6 @@ class CustomMediaPlayerViewController: UIViewController {
self.skip85Button.alpha = self.isControlsVisible ? 0.8 : 0
if self.isControlsVisible {
// Always use the controls-attached constraints.
NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints)
if self.isWatchNextRepositioned || self.isWatchNextVisible {
@ -809,7 +798,6 @@ class CustomMediaPlayerViewController: UIViewController {
})
}
} else {
// When controls are hidden:
if !self.isWatchNextRepositioned && self.isWatchNextVisible {
NSLayoutConstraint.deactivate(self.watchNextButtonControlsConstraints)
NSLayoutConstraint.activate(self.watchNextButtonNormalConstraints)

View file

@ -29,7 +29,7 @@ class VTTSubtitlesLoader: ObservableObject {
let format = determineSubtitleFormat(from: url)
URLSession.cloudflareCustom.dataTask(with: url) { data, _, error in
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data,
let content = String(data: data, encoding: .utf8),
error == nil else { return }

View file

@ -296,7 +296,7 @@ struct MediaInfoView: View {
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(href)")
}
refreshTrigger.toggle()
Logger.shared.log("Marked \(ep.number - 1) episodes watched within anime \"\(title)\".", type: "General")
Logger.shared.log("Marked \(ep.number - 1) episodes watched within series \"\(title)\".", type: "General")
}
)
.id(refreshTrigger)
@ -626,9 +626,15 @@ struct MediaInfoView: View {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
Logger.shared.log("Opening external app with scheme: \(url)", type: "General")
} else {
guard let url = URL(string: url) else {
Logger.shared.log("Invalid stream URL: \(url)", type: "Error")
DropManager.shared.showDrop(title: "Error", subtitle: "Invalid stream URL", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
return
}
let customMediaPlayer = CustomMediaPlayerViewController(
module: module,
urlString: url,
urlString: url.absoluteString,
fullUrl: fullURL,
title: title,
episodeNumber: selectedEpisodeNumber,
@ -644,6 +650,9 @@ struct MediaInfoView: View {
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 {
Logger.shared.log("Failed to find root view controller", type: "Error")
DropManager.shared.showDrop(title: "Error", subtitle: "Failed to present player", duration: 2.0, icon: UIImage(systemName: "xmark.circle"))
}
}
}

View file

@ -165,20 +165,24 @@ struct SearchView: View {
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
ForEach(moduleManager.modules, id: \.id) { module in
Button {
selectedModuleId = module.id.uuidString
} label: {
HStack {
KFImage(URL(string: module.metadata.iconUrl))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.cornerRadius(4)
Text(module.metadata.sourceName)
if module.id.uuidString == selectedModuleId {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
ForEach(getModuleLanguageGroups(), id: \.self) { language in
Menu(language) {
ForEach(getModulesForLanguage(language), id: \.id) { module in
Button {
selectedModuleId = module.id.uuidString
} label: {
HStack {
KFImage(URL(string: module.metadata.iconUrl))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.cornerRadius(4)
Text(module.metadata.sourceName)
if module.id.uuidString == selectedModuleId {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
}
@ -269,6 +273,41 @@ struct SearchView: View {
return verticalSizeClass == .compact ? mediaColumnsLandscape : mediaColumnsPortrait
}
}
private func cleanLanguageName(_ language: String?) -> String {
guard let language = language else { return "Unknown" }
let cleaned = language.replacingOccurrences(
of: "\\s*\\([^\\)]*\\)",
with: "",
options: .regularExpression
).trimmingCharacters(in: .whitespaces)
return cleaned.isEmpty ? "Unknown" : cleaned
}
private func getModulesByLanguage() -> [String: [ScrapingModule]] {
var result = [String: [ScrapingModule]]()
for module in moduleManager.modules {
let language = cleanLanguageName(module.metadata.language)
if result[language] == nil {
result[language] = [module]
} else {
result[language]?.append(module)
}
}
return result
}
private func getModuleLanguageGroups() -> [String] {
return getModulesByLanguage().keys.sorted()
}
private func getModulesForLanguage(_ language: String) -> [ScrapingModule] {
return getModulesByLanguage()[language] ?? []
}
}
struct SearchBar: View {

View file

@ -14,9 +14,13 @@ struct SettingsViewGeneral: View {
@AppStorage("analyticsEnabled") private var analyticsEnabled: Bool = false
@AppStorage("multiThreads") private var multiThreadsEnabled: Bool = false
@AppStorage("metadataProviders") private var metadataProviders: String = "AniList"
@AppStorage("CustomDNSProvider") private var customDNSProvider: String = "Cloudflare"
@AppStorage("customPrimaryDNS") private var customPrimaryDNS: String = ""
@AppStorage("customSecondaryDNS") private var customSecondaryDNS: String = ""
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
private let customDNSProviderList = ["Cloudflare", "Google", "OpenDNS", "Quad9", "AdGuard", "CleanBrowsing", "ControlD", "Custom"]
private let metadataProvidersList = ["AniList"]
@EnvironmentObject var settings: Settings
@ -24,7 +28,7 @@ struct SettingsViewGeneral: View {
Form {
Section(header: Text("Interface")) {
ColorPicker("Accent Color", selection: $settings.accentColor)
HStack() {
HStack {
Text("Appearance")
Picker("Appearance", selection: $settings.selectedAppearance) {
Text("System").tag(Appearance.system)
@ -40,18 +44,10 @@ struct SettingsViewGeneral: View {
Text("Episodes Range")
Spacer()
Menu {
Button(action: { episodeChunkSize = 25 }) {
Text("25")
}
Button(action: { episodeChunkSize = 50 }) {
Text("50")
}
Button(action: { episodeChunkSize = 75 }) {
Text("75")
}
Button(action: { episodeChunkSize = 100 }) {
Text("100")
}
Button(action: { episodeChunkSize = 25 }) { Text("25") }
Button(action: { episodeChunkSize = 50 }) { Text("50") }
Button(action: { episodeChunkSize = 75 }) { Text("75") }
Button(action: { episodeChunkSize = 100 }) { Text("100") }
} label: {
Text("\(episodeChunkSize)")
}
@ -63,36 +59,24 @@ struct SettingsViewGeneral: View {
Spacer()
Menu(metadataProviders) {
ForEach(metadataProvidersList, id: \.self) { provider in
Button(action: {
metadataProviders = provider
}) {
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("Media Grid Layout"), footer: Text("Adjust the number of media items per row in portrait and landscape modes.")) {
HStack {
if UIDevice.current.userInterfaceIdiom == .pad {
Picker("Portrait Columns", selection: $mediaColumnsPortrait) {
ForEach(1..<6) { i in
Text("\(i)").tag(i)
}
ForEach(1..<6) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
} else {
Picker("Portrait Columns", selection: $mediaColumnsPortrait) {
ForEach(1..<5) { i in
Text("\(i)").tag(i)
}
ForEach(1..<5) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
}
@ -100,16 +84,12 @@ struct SettingsViewGeneral: View {
HStack {
if UIDevice.current.userInterfaceIdiom == .pad {
Picker("Landscape Columns", selection: $mediaColumnsLandscape) {
ForEach(2..<9) { i in
Text("\(i)").tag(i)
}
ForEach(2..<9) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
} else {
Picker("Landscape Columns", selection: $mediaColumnsLandscape) {
ForEach(2..<6) { i in
Text("\(i)").tag(i)
}
ForEach(2..<6) { i in Text("\(i)").tag(i) }
}
.pickerStyle(MenuPickerStyle())
}
@ -121,7 +101,7 @@ struct SettingsViewGeneral: View {
.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")) {
Section(header: Text("Advanced"), footer: Text("Anonymous data is collected to improve the app. No personal information is collected. This can be disabled at any time.")) {
Toggle("Enable Analytics", isOn: $analyticsEnabled)
.tint(.accentColor)
}

View file

@ -11,9 +11,9 @@ struct SettingsView: View {
var body: some View {
NavigationView {
Form {
Section(header: Text("Main Settings")) {
Section(header: Text("Main")) {
NavigationLink(destination: SettingsViewGeneral()) {
Text("General Settings")
Text("General Preferences")
}
NavigationLink(destination: SettingsViewPlayer()) {
Text("Media Player")
@ -21,9 +21,9 @@ struct SettingsView: View {
NavigationLink(destination: SettingsViewModule()) {
Text("Modules")
}
NavigationLink(destination: SettingsViewTrackers()) {
Text("Trackers")
}
//NavigationLink(destination: SettingsViewTrackers()) {
// Text("Trackers")
//}
}
Section(header: Text("Info")) {

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 */; };
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; };
13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; };
@ -14,6 +15,9 @@
131845F92D47C62D00CA7A54 /* SettingsViewGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131845F82D47C62D00CA7A54 /* SettingsViewGeneral.swift */; };
1327FBA72D758CEA00FC6689 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA62D758CEA00FC6689 /* Analytics.swift */; };
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */; };
132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; };
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */; };
132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; };
1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF4C2D786C93007E289F /* TMDB-Seasonal.swift */; };
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF4E2D786C9E007E289F /* TMDB-Trending.swift */; };
1334FF522D7871B7007E289F /* TMDBItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1334FF512D7871B7007E289F /* TMDBItem.swift */; };
@ -30,10 +34,8 @@
133D7C922D2BE2640075467E /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C872D2BE2640075467E /* URLSession.swift */; };
133D7C932D2BE2640075467E /* Modules.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C892D2BE2640075467E /* Modules.swift */; };
133D7C942D2BE2640075467E /* JSController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C8B2D2BE2640075467E /* JSController.swift */; };
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; };
1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; };
1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 1359ED192D76FA7D00C13034 /* Drops */; };
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; };
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
@ -52,6 +54,7 @@
13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; };
13DB46922D900BCE008CBC03 /* SettingsViewTrackers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */; };
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; };
13DC0C462D302C7500D0F966 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DC0C452D302C7500D0F966 /* VideoPlayer.swift */; };
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 +65,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>"; };
13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = "<group>"; };
@ -107,6 +111,7 @@
13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
13DB46912D900BCE008CBC03 /* SettingsViewTrackers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewTrackers.swift; sourceTree = "<group>"; };
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
13DC0C452D302C7500D0F966 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
13EA2BD12D32D97400C1EBD7 /* CustomPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPlayer.swift; sourceTree = "<group>"; };
@ -122,8 +127,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */,
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */,
132E35232D959E410007800E /* Kingfisher in Frameworks */,
132E35202D959E1D0007800E /* FFmpeg-iOS-Lame in Frameworks */,
132E351D2D959DDB0007800E /* Drops in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -247,6 +253,7 @@
1399FAD22D3AB34F00E97C31 /* SettingsView */,
133F55B92D33B53E00E08EEA /* LibraryView */,
133D7C7C2D2BE2630075467E /* SearchView.swift */,
130217CB2D81C55E0011EFF5 /* DownloadView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -277,6 +284,7 @@
133D7C852D2BE2640075467E /* Utils */ = {
isa = PBXGroup;
children = (
13DB7CEA2D7DED50004371D3 /* DownloadManager */,
13C0E5E82D5F85DD00E7F619 /* ContinueWatching */,
13103E8C2D58E037000F0673 /* SkeletonCells */,
13DC0C442D302C6A00D0F966 /* MediaPlayer */,
@ -392,6 +400,14 @@
path = Auth;
sourceTree = "<group>";
};
13DB7CEA2D7DED50004371D3 /* DownloadManager */ = {
isa = PBXGroup;
children = (
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */,
);
path = DownloadManager;
sourceTree = "<group>";
};
13DC0C442D302C6A00D0F966 /* MediaPlayer */ = {
isa = PBXGroup;
children = (
@ -438,8 +454,9 @@
);
name = Sulfur;
packageProductDependencies = (
133D7C962D2BE2AF0075467E /* Kingfisher */,
1359ED192D76FA7D00C13034 /* Drops */,
132E351C2D959DDB0007800E /* Drops */,
132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */,
132E35222D959E410007800E /* Kingfisher */,
);
productName = Sora;
productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */;
@ -469,8 +486,9 @@
);
mainGroup = 133D7C612D2BE2500075467E;
packageReferences = (
133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */,
1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */,
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */,
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
);
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
projectDirPath = "";
@ -516,6 +534,7 @@
133D7C932D2BE2640075467E /* Modules.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,6 +554,7 @@
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
@ -778,15 +798,7 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = exactVersion;
version = 7.9.1;
};
};
1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */ = {
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/omaralbeik/Drops.git";
requirement = {
@ -794,19 +806,40 @@
kind = branch;
};
};
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kewlbear/FFmpeg-iOS-Lame";
requirement = {
branch = main;
kind = branch;
};
};
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher.git";
requirement = {
kind = exactVersion;
version = 7.9.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
133D7C962D2BE2AF0075467E /* Kingfisher */ = {
132E351C2D959DDB0007800E /* Drops */ = {
isa = XCSwiftPackageProductDependency;
package = 133D7C952D2BE2AF0075467E /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
1359ED192D76FA7D00C13034 /* Drops */ = {
isa = XCSwiftPackageProductDependency;
package = 1359ED182D76FA7D00C13034 /* XCRemoteSwiftPackageReference "Drops" */;
package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */;
productName = Drops;
};
132E351F2D959E1D0007800E /* FFmpeg-iOS-Lame */ = {
isa = XCSwiftPackageProductDependency;
package = 132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */;
productName = "FFmpeg-iOS-Lame";
};
132E35222D959E410007800E /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */;
productName = Kingfisher;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 133D7C622D2BE2500075467E /* Project object */;