mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-18 15:12:09 +00:00
more linting
This commit is contained in:
parent
a4fbcbc112
commit
ee11dc81c1
58 changed files with 563 additions and 422 deletions
|
|
@ -47,32 +47,35 @@ disabled_rules:
|
|||
# newly added:
|
||||
- multiple_closures_with_trailing_closure
|
||||
- closure_body_length
|
||||
- type_body_length
|
||||
- file_name
|
||||
- file_length
|
||||
- line_length
|
||||
- nesting
|
||||
- legacy_objc_type
|
||||
- function_body_length
|
||||
|
||||
# Configurations
|
||||
attributes:
|
||||
always_on_line_above:
|
||||
- "@ConfigurationElement"
|
||||
- "@OptionGroup"
|
||||
- "@RuleConfigurationDescriptionBuilder"
|
||||
|
||||
identifier_name:
|
||||
excluded:
|
||||
- id
|
||||
- trailing_comma
|
||||
- identifier_name
|
||||
- discarded_notification_center_observer
|
||||
- extension_access_modifier
|
||||
- explicit_init
|
||||
- superfluous_else
|
||||
- discouraged_optional_boolean
|
||||
- discouraged_none_name
|
||||
- attributes
|
||||
- prefer_key_path
|
||||
# should be fixed sometimes:
|
||||
- implicitly_unwrapped_optional
|
||||
- cyclomatic_complexity
|
||||
- unused_parameter
|
||||
- fatal_error
|
||||
- force_cast
|
||||
|
||||
large_tuple: 3
|
||||
number_separator:
|
||||
minimum_length: 5
|
||||
redundant_type_annotation:
|
||||
consider_default_literal_types_redundant: true
|
||||
single_test_class: *unit_test_configuration
|
||||
trailing_comma:
|
||||
mandatory_comma: true
|
||||
type_body_length: 400
|
||||
unneeded_override:
|
||||
affect_initializers: true
|
||||
unused_import:
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"+ Icon" : {
|
||||
|
||||
},
|
||||
"+10s" : {
|
||||
"localizations" : {
|
||||
|
|
@ -194,6 +197,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Alternative App Icon" : {
|
||||
|
||||
},
|
||||
"AniList" : {
|
||||
"localizations" : {
|
||||
|
|
@ -210,6 +216,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AniList Icon" : {
|
||||
|
||||
},
|
||||
"AniList.co" : {
|
||||
"localizations" : {
|
||||
|
|
@ -514,6 +523,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Checkmark Icon" : {
|
||||
|
||||
},
|
||||
"Clear Cache" : {
|
||||
"localizations" : {
|
||||
|
|
@ -530,6 +542,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Clear Icon" : {
|
||||
|
||||
},
|
||||
"Clear Logs" : {
|
||||
"localizations" : {
|
||||
|
|
@ -578,6 +593,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Configure Icon" : {
|
||||
|
||||
},
|
||||
"Confirm Erase App Data" : {
|
||||
"localizations" : {
|
||||
|
|
@ -914,6 +932,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Episode Icon" : {
|
||||
|
||||
},
|
||||
"Episodes" : {
|
||||
"localizations" : {
|
||||
|
|
@ -994,6 +1015,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Error Icon" : {
|
||||
|
||||
},
|
||||
"Error: %@" : {
|
||||
"localizations" : {
|
||||
|
|
@ -1027,6 +1051,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Expand Icon" : {
|
||||
|
||||
},
|
||||
"Explore" : {
|
||||
"localizations" : {
|
||||
|
|
@ -1059,6 +1086,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"External App Icon" : {
|
||||
|
||||
},
|
||||
"Failed to parse response" : {
|
||||
"localizations" : {
|
||||
|
|
@ -1663,6 +1693,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Magazine Icon" : {
|
||||
|
||||
},
|
||||
"Magnifying Glass Icon" : {
|
||||
|
||||
},
|
||||
"Main" : {
|
||||
"localizations" : {
|
||||
|
|
@ -1807,6 +1843,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"More Icon" : {
|
||||
|
||||
},
|
||||
"Name" : {
|
||||
"localizations" : {
|
||||
|
|
@ -2079,6 +2118,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Play Icon" : {
|
||||
|
||||
},
|
||||
"Play Offline Content" : {
|
||||
"localizations" : {
|
||||
|
|
@ -2175,6 +2217,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Questionmark Icon" : {
|
||||
|
||||
},
|
||||
"Recently watched content will appear here." : {
|
||||
"localizations" : {
|
||||
|
|
@ -2383,6 +2428,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Safari Icon" : {
|
||||
|
||||
},
|
||||
"Scan to Visit" : {
|
||||
"localizations" : {
|
||||
|
|
@ -2415,6 +2463,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Search Icon" : {
|
||||
|
||||
},
|
||||
"Search View" : {
|
||||
"localizations" : {
|
||||
|
|
@ -2655,6 +2706,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Speaker Icon" : {
|
||||
|
||||
},
|
||||
"Speed Settings" : {
|
||||
"localizations" : {
|
||||
|
|
@ -2671,6 +2725,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Star Icon" : {
|
||||
|
||||
},
|
||||
"Starting authentication..." : {
|
||||
"localizations" : {
|
||||
|
|
@ -2847,6 +2904,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Trakt Icon" : {
|
||||
|
||||
},
|
||||
"Trakt.tv" : {
|
||||
"localizations" : {
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ struct SoraApp: App {
|
|||
.accentColor(settings.accentColor)
|
||||
.onAppear {
|
||||
// pass initial profile value to other manager
|
||||
let suite = self.profileStore.getUserDefaultsSuite()
|
||||
self.libraryManager.userDefaultsSuite = suite
|
||||
self.continueWatchingManager.userDefaultsSuite = suite
|
||||
let suite = profileStore.getUserDefaultsSuite()
|
||||
libraryManager.userDefaultsSuite = suite
|
||||
continueWatchingManager.userDefaultsSuite = suite
|
||||
|
||||
_ = iCloudSyncManager.shared
|
||||
_ = ICloudSyncManager.shared
|
||||
|
||||
settings.updateAppearance()
|
||||
Task {
|
||||
|
|
@ -58,7 +58,7 @@ struct SoraApp: App {
|
|||
}
|
||||
.onChange(of: profileStore.currentProfile) { _ in
|
||||
// pass changed suite value to other manager
|
||||
let suite = self.profileStore.getUserDefaultsSuite()
|
||||
let suite = profileStore.getUserDefaultsSuite()
|
||||
libraryManager.updateProfileSuite(suite)
|
||||
continueWatchingManager.updateProfileSuite(suite)
|
||||
}
|
||||
|
|
@ -71,7 +71,6 @@ struct SoraApp: App {
|
|||
case "default_page":
|
||||
if let comps = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||
let libraryURL = comps.queryItems?.first(where: { $0.name == "url" })?.value {
|
||||
|
||||
UserDefaults.standard.set(libraryURL, forKey: "lastCommunityURL")
|
||||
UserDefaults.standard.set(true, forKey: "didReceiveDefaultPageLink")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 08/08/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
import UIKit
|
||||
|
||||
class AniListToken {
|
||||
static let clientID = "19551"
|
||||
|
|
@ -62,14 +62,14 @@ class AniListToken {
|
|||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": error.localizedDescription])
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
Logger.shared.log("No data received", type: "Error")
|
||||
NotificationCenter.default.post(name: authFailureNotification, object: nil, userInfo: ["error": "No data received"])
|
||||
completion(false)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 07/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
import UIKit
|
||||
|
||||
class AniListMutation {
|
||||
let apiURL = URL(string: "https://graphql.anilist.co")!
|
||||
|
|
@ -77,7 +77,7 @@ class AniListMutation {
|
|||
request.httpBody = jsonData
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ class AniListMutation {
|
|||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
if let data {
|
||||
do {
|
||||
_ = try JSONSerialization.jsonObject(with: data, options: [])
|
||||
Logger.shared.log("Successfully updated anime progress", type: "Debug")
|
||||
|
|
@ -118,8 +118,13 @@ class AniListMutation {
|
|||
"variables": variables
|
||||
]
|
||||
guard let jsonData = try? JSONSerialization.data(withJSONObject: requestBody, options: []) else {
|
||||
completion(.failure(NSError(domain: "", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to serialize GraphQL request"])))
|
||||
completion(
|
||||
.failure(
|
||||
NSError(domain: "", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to serialize GraphQL request"
|
||||
])
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -129,14 +134,19 @@ class AniListMutation {
|
|||
request.httpBody = jsonData
|
||||
|
||||
URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
if let e = error {
|
||||
return completion(.failure(e))
|
||||
if let error {
|
||||
return completion(.failure(error))
|
||||
}
|
||||
guard let data = data,
|
||||
guard let data,
|
||||
let json = try? JSONDecoder().decode(AniListMediaResponse.self, from: data),
|
||||
let mal = json.data.Media?.idMal else {
|
||||
return completion(.failure(NSError(domain: "", code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to decode AniList response or idMal missing"])))
|
||||
return completion(
|
||||
.failure(
|
||||
NSError(domain: "", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to decode AniList response or idMal missing"
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
||||
completion(.success(mal))
|
||||
}.resume()
|
||||
|
|
@ -145,8 +155,10 @@ class AniListMutation {
|
|||
private struct AniListMediaResponse: Decodable {
|
||||
struct DataField: Decodable {
|
||||
struct Media: Decodable { let idMal: Int? }
|
||||
|
||||
let Media: Media?
|
||||
}
|
||||
|
||||
let data: DataField
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
import UIKit
|
||||
|
||||
class TraktToken {
|
||||
static let clientID = "6ec81bf19deb80fdfa25652eef101576ca6aaa0dc016d36079b2de413d71c369"
|
||||
|
|
@ -101,12 +101,12 @@ class TraktToken {
|
|||
|
||||
let task = URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
DispatchQueue.main.async {
|
||||
if let error = error {
|
||||
if let error {
|
||||
handleFailure(error: error.localizedDescription, completion: completion)
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
handleFailure(error: "No data received", completion: completion)
|
||||
return
|
||||
}
|
||||
|
|
@ -115,7 +115,6 @@ class TraktToken {
|
|||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
if let accessToken = json["access_token"] as? String,
|
||||
let refreshToken = json["refresh_token"] as? String {
|
||||
|
||||
let accessSuccess = saveToKeychain(key: accessTokenKey, data: accessToken)
|
||||
let refreshSuccess = saveToKeychain(key: refreshTokenKey, data: refreshToken)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 13/04/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Security
|
||||
import UIKit
|
||||
|
||||
class TraktMutation {
|
||||
let apiURL = URL(string: "https://api.trakt.tv")!
|
||||
|
|
@ -110,7 +110,7 @@ class TraktMutation {
|
|||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,16 +18,16 @@ struct AnalyticsResponse: Codable {
|
|||
|
||||
// MARK: - Analytics Manager
|
||||
class AnalyticsManager {
|
||||
|
||||
static let shared = AnalyticsManager()
|
||||
private let analyticsURL = URL(string: "http://151.106.3.14:47474/analytics")!
|
||||
private let moduleManager = ModuleManager()
|
||||
|
||||
private init() {}
|
||||
private init() {
|
||||
print("[Info] Analytics initializer called")
|
||||
}
|
||||
|
||||
// MARK: - Send Analytics Data
|
||||
func sendEvent(event: String, additionalData: [String: Any] = [:]) {
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
|
||||
// Ensure the key is set with a default value if missing
|
||||
|
|
@ -80,13 +80,13 @@ class AnalyticsManager {
|
|||
return
|
||||
}
|
||||
|
||||
URLSession.shared.dataTask(with: request) { (data, _, error) in
|
||||
if let error = error {
|
||||
URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
if let error {
|
||||
Logger.shared.log("Request failed: \(error.localizedDescription)", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
Logger.shared.log("No data received from server", type: "Debug")
|
||||
return
|
||||
}
|
||||
|
|
@ -106,12 +106,12 @@ class AnalyticsManager {
|
|||
|
||||
// MARK: - Get App Version
|
||||
private func getAppVersion() -> String {
|
||||
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown_version"
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown_version"
|
||||
}
|
||||
|
||||
// MARK: - Get Device Model
|
||||
private func getDeviceModel() -> String {
|
||||
return UIDevice.modelName
|
||||
UIDevice.modelName
|
||||
}
|
||||
|
||||
// MARK: - Get Selected Module
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ class ContinueWatchingManager: ObservableObject {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil)
|
||||
}
|
||||
|
||||
public func updateProfileSuite(_ newSuite: UserDefaults) {
|
||||
func updateProfileSuite(_ newSuite: UserDefaults) {
|
||||
userDefaultsSuite = newSuite
|
||||
loadItems()
|
||||
}
|
||||
|
||||
@objc private func handleiCloudSync() {
|
||||
@objc
|
||||
private func handleiCloudSync() {
|
||||
NotificationCenter.default.post(name: .ContinueWatchingDidUpdate, object: nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@
|
|||
// Created by Francesco on 29/04/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
|
||||
class DownloadManager: NSObject, ObservableObject {
|
||||
@Published var activeDownloads: [(URL, Double)] = []
|
||||
@Published var localPlaybackURL: URL?
|
||||
|
||||
|
||||
private var assetDownloadURLSession: AVAssetDownloadURLSession!
|
||||
private var activeDownloadTasks: [URLSessionTask: URL] = [:]
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
initializeDownloadSession()
|
||||
loadLocalContent()
|
||||
}
|
||||
|
||||
|
||||
private func initializeDownloadSession() {
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "hls-downloader")
|
||||
assetDownloadURLSession = AVAssetDownloadURLSession(
|
||||
|
|
@ -30,7 +30,7 @@ class DownloadManager: NSObject, ObservableObject {
|
|||
delegateQueue: .main
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func downloadAsset(from url: URL) {
|
||||
let asset = AVURLAsset(url: url)
|
||||
let task = assetDownloadURLSession.makeAssetDownloadTask(
|
||||
|
|
@ -39,21 +39,21 @@ class DownloadManager: NSObject, ObservableObject {
|
|||
assetArtworkData: nil,
|
||||
options: nil
|
||||
)
|
||||
|
||||
|
||||
task?.resume()
|
||||
activeDownloadTasks[task!] = url
|
||||
}
|
||||
|
||||
|
||||
private func loadLocalContent() {
|
||||
guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
|
||||
|
||||
|
||||
do {
|
||||
let contents = try FileManager.default.contentsOfDirectory(
|
||||
at: documents,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles
|
||||
)
|
||||
|
||||
|
||||
if let localURL = contents.first(where: { $0.pathExtension == "movpkg" }) {
|
||||
localPlaybackURL = localURL
|
||||
}
|
||||
|
|
@ -64,28 +64,33 @@ class DownloadManager: NSObject, ObservableObject {
|
|||
}
|
||||
|
||||
extension DownloadManager: AVAssetDownloadDelegate {
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
assetDownloadTask: AVAssetDownloadTask,
|
||||
didFinishDownloadingTo location: URL
|
||||
) {
|
||||
activeDownloadTasks.removeValue(forKey: assetDownloadTask)
|
||||
localPlaybackURL = location
|
||||
}
|
||||
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let error = error else { return }
|
||||
guard let error else { return }
|
||||
print("Download error: \(error.localizedDescription)")
|
||||
activeDownloadTasks.removeValue(forKey: task)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession,
|
||||
assetDownloadTask: AVAssetDownloadTask,
|
||||
didLoad timeRange: CMTimeRange,
|
||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
|
||||
timeRangeExpectedToLoad: CMTimeRange) {
|
||||
|
||||
|
||||
func urlSession(
|
||||
_ session: URLSession,
|
||||
assetDownloadTask: AVAssetDownloadTask,
|
||||
didLoad timeRange: CMTimeRange,
|
||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
|
||||
timeRangeExpectedToLoad: CMTimeRange
|
||||
) {
|
||||
guard let url = activeDownloadTasks[assetDownloadTask] else { return }
|
||||
let progress = loadedTimeRanges
|
||||
.map { $0.timeRangeValue.duration.seconds / timeRangeExpectedToLoad.duration.seconds }
|
||||
.reduce(0, +)
|
||||
|
||||
|
||||
if let index = activeDownloads.firstIndex(where: { $0.0 == url }) {
|
||||
activeDownloads[index].1 = progress
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import UIKit
|
|||
class DropManager {
|
||||
static let shared = DropManager()
|
||||
|
||||
private init() {}
|
||||
private init() {
|
||||
print("[Info] Drops initialized")
|
||||
}
|
||||
|
||||
func showDrop(title: String, subtitle: String, duration: TimeInterval, icon: UIImage?) {
|
||||
let position: Drop.Position = .top
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import SwiftUI
|
||||
|
||||
extension Color {
|
||||
|
||||
/// Intitialize SwiftUI Color via HEX String
|
||||
///
|
||||
/// - Parameters:
|
||||
|
|
|
|||
|
|
@ -37,18 +37,18 @@ extension JSContext {
|
|||
return
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
if let headers = headers {
|
||||
if let headers {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
let task = URLSession.custom.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error in fetchNativeFunction: \(error.localizedDescription)", type: "Error")
|
||||
reject.call(withArguments: [error.localizedDescription])
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
Logger.shared.log("No data in response", type: "Error")
|
||||
reject.call(withArguments: ["No data"])
|
||||
return
|
||||
|
|
@ -88,30 +88,30 @@ extension JSContext {
|
|||
|
||||
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" {
|
||||
if httpMethod == "GET", let 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" {
|
||||
if httpMethod != "GET", let body, !body.isEmpty, body != "null", body != "undefined" {
|
||||
request.httpBody = body.data(using: .utf8)
|
||||
}
|
||||
|
||||
if let headers = headers {
|
||||
if let headers {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
Logger.shared.log("Redirect value is \(redirect.boolValue)", type: "Error")
|
||||
let task = URLSession.fetchData(allowRedirects: redirect.boolValue).downloadTask(with: request) { tempFileURL, response, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error in fetchV2NativeFunction: \(error.localizedDescription)", type: "Error")
|
||||
reject.call(withArguments: [error.localizedDescription])
|
||||
return
|
||||
}
|
||||
|
||||
guard let tempFileURL = tempFileURL else {
|
||||
guard let tempFileURL else {
|
||||
Logger.shared.log("No data in response", type: "Error")
|
||||
reject.call(withArguments: ["No data"])
|
||||
return
|
||||
|
|
@ -133,7 +133,6 @@ extension JSContext {
|
|||
}
|
||||
|
||||
if let text = String(data: data, encoding: .utf8) {
|
||||
|
||||
responseDict["body"] = text
|
||||
resolve.call(withArguments: [responseDict])
|
||||
} else {
|
||||
|
|
@ -141,7 +140,6 @@ extension JSContext {
|
|||
Logger.shared.log("Unable to decode data to text", type: "Error")
|
||||
resolve.call(withArguments: [responseDict])
|
||||
}
|
||||
|
||||
} catch {
|
||||
Logger.shared.log("Error reading downloaded file: \(error.localizedDescription)", type: "Error")
|
||||
reject.call(withArguments: ["Error reading downloaded file"])
|
||||
|
|
|
|||
|
|
@ -19,6 +19,6 @@ extension String {
|
|||
}
|
||||
|
||||
var trimmed: String {
|
||||
return self.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@
|
|||
import UIKit
|
||||
|
||||
public extension UIDevice {
|
||||
|
||||
static let modelName: String = {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
|
||||
let identifier = machineMirror.children.reduce(into: "") { identifier, element in
|
||||
guard let value = element.value as? Int8, value != 0 else { return }
|
||||
identifier += String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
|
||||
func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ extension UIApplication {
|
|||
}
|
||||
|
||||
extension Decodable where Self: UIColor {
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let components = try container.decode([CGFloat].self)
|
||||
|
|
@ -33,9 +32,8 @@ extension Encodable where Self: UIColor {
|
|||
(r, g, b, a) = (0, 0, 0, 0)
|
||||
var container = encoder.singleValueContainer()
|
||||
self.getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
try container.encode([r,g,b,a])
|
||||
try container.encode([r, g, b, a])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIColor: Codable { }
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import Foundation
|
|||
// URL DELEGATE CLASS FOR FETCH API
|
||||
class FetchDelegate: NSObject, URLSessionTaskDelegate {
|
||||
private let allowRedirects: Bool
|
||||
|
||||
init(allowRedirects: Bool) {
|
||||
self.allowRedirects = allowRedirects
|
||||
}
|
||||
|
||||
// This handles the redirection and prevents it.
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
|
||||
if allowRedirects {
|
||||
|
|
@ -19,10 +21,9 @@ class FetchDelegate: NSObject, URLSessionTaskDelegate {
|
|||
} else {
|
||||
completionHandler(nil) // Block Redirect
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension URLSession {
|
||||
static let userAgents = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
|
|
|
|||
|
|
@ -37,4 +37,3 @@ struct HideToolbarModifier: ViewModifier {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class findTopViewController {
|
||||
class FindTopViewController {
|
||||
static func findViewController(_ viewController: UIViewController) -> UIViewController {
|
||||
if let presented = viewController.presentedViewController {
|
||||
return findViewController(presented)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
|
||||
guard let url = URL(string: url) else {
|
||||
completion([], [])
|
||||
|
|
@ -16,15 +15,15 @@ extension JSController {
|
|||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
guard let data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML", type: "Error")
|
||||
DispatchQueue.main.async { completion([], []) }
|
||||
return
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import JavaScriptCore
|
|||
|
||||
// TODO: implement and test
|
||||
extension JSController {
|
||||
|
||||
func fetchExploreResults(module: ScrapingModule, completion: @escaping ([ExploreItem]) -> Void) {
|
||||
/*let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
|
||||
|
|
@ -21,13 +20,13 @@ extension JSController {
|
|||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error: \(error)",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
guard let data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML",type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
|
||||
let searchUrl = module.metadata.searchBaseUrl.replacingOccurrences(of: "%s", with: keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||
|
||||
|
|
@ -18,15 +17,15 @@ extension JSController {
|
|||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
guard let data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML", type: "Error")
|
||||
DispatchQueue.main.async { completion([]) }
|
||||
return
|
||||
|
|
@ -73,7 +72,6 @@ extension JSController {
|
|||
}
|
||||
|
||||
let thenBlock: @convention(block) (JSValue) -> Void = { result in
|
||||
|
||||
Logger.shared.log(result.toString(), type: "HTMLStrings")
|
||||
if let jsonString = result.toString(),
|
||||
let data = jsonString.data(using: .utf8) {
|
||||
|
|
@ -92,7 +90,6 @@ extension JSController {
|
|||
DispatchQueue.main.async {
|
||||
completion(resultItems)
|
||||
}
|
||||
|
||||
} else {
|
||||
Logger.shared.log("Failed to parse JSON", type: "Error")
|
||||
DispatchQueue.main.async {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import JavaScriptCore
|
||||
|
||||
extension JSController {
|
||||
|
||||
func fetchStreamUrl(episodeUrl: String, softsub: Bool = false, module: ScrapingModule, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
|
||||
guard let url = URL(string: episodeUrl) else {
|
||||
completion((nil, nil))
|
||||
|
|
@ -16,15 +15,15 @@ extension JSController {
|
|||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Network error: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let html = String(data: data, encoding: .utf8) else {
|
||||
guard let data, let html = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to decode HTML", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
|
|
@ -164,15 +163,15 @@ extension JSController {
|
|||
func fetchStreamUrlJSSecond(episodeUrl: String, softsub: Bool = false, module: ScrapingModule, completion: @escaping ((streams: [String]?, subtitles: [String]?)) -> Void) {
|
||||
let url = URL(string: episodeUrl)!
|
||||
let task = URLSession.custom.dataTask(with: url) { [weak self] data, _, error in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("URLSession error: \(error.localizedDescription)", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let htmlString = String(data: data, encoding: .utf8) else {
|
||||
guard let data, let htmlString = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to fetch HTML data", type: "Error")
|
||||
DispatchQueue.main.async { completion((nil, nil)) }
|
||||
return
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
// Thanks to pratikg29 for this code inside his open source project "https://github.com/pratikg29/Custom-Slider-Control?ref=iosexample.com"
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
extension Double {
|
||||
func asTimeString(style: DateComponentsFormatter.UnitsStyle) -> String {
|
||||
|
|
@ -41,11 +41,11 @@ enum TimeStringStyle {
|
|||
}
|
||||
|
||||
class VolumeViewModel: ObservableObject {
|
||||
@Published var value: Double = 0.0
|
||||
@Published var value = 0.0
|
||||
}
|
||||
|
||||
class SliderViewModel: ObservableObject {
|
||||
@Published var sliderValue: Double = 0.0
|
||||
@Published var sliderValue = 0.0
|
||||
@Published var introSegments: [ClosedRange<Double>] = []
|
||||
@Published var outroSegments: [ClosedRange<Double>] = []
|
||||
}
|
||||
|
|
@ -53,8 +53,10 @@ class SliderViewModel: ObservableObject {
|
|||
struct AniListMediaResponse: Decodable {
|
||||
struct DataField: Decodable {
|
||||
struct Media: Decodable { let idMal: Int? }
|
||||
|
||||
let Media: Media?
|
||||
}
|
||||
|
||||
let data: DataField
|
||||
}
|
||||
|
||||
|
|
@ -64,9 +66,11 @@ struct AniSkipResponse: Decodable {
|
|||
let startTime: Double
|
||||
let endTime: Double
|
||||
}
|
||||
|
||||
let interval: Interval
|
||||
let skipType: String
|
||||
}
|
||||
|
||||
let found: Bool
|
||||
let results: [Result]
|
||||
let statusCode: Int
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
@GestureState private var isActive = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
|
|
@ -135,11 +135,10 @@ struct MusicProgressSlider<T: BinaryFloatingPoint>: View {
|
|||
private func getPrgPercentage(_ value: T) -> T {
|
||||
let range = inRange.upperBound - inRange.lowerBound
|
||||
let correctedStartValue = value - inRange.lowerBound
|
||||
let percentage = correctedStartValue / range
|
||||
return percentage
|
||||
return correctedStartValue / range
|
||||
}
|
||||
|
||||
private func getPrgValue() -> T {
|
||||
return ((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
((localRealProgress + localTempProgress) * (inRange.upperBound - inRange.lowerBound)) + inRange.lowerBound
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
|
|||
let fillColor: Color
|
||||
let emptyColor: Color
|
||||
let height: CGFloat
|
||||
let onEditingChanged: (Bool) -> Void
|
||||
let onEditingChanged: ((Bool) -> Void)? = nil
|
||||
|
||||
@State private var localRealProgress: T = 0
|
||||
@State private var localTempProgress: T = 0
|
||||
@State private var lastVolumeValue: T = 0
|
||||
@GestureState private var isActive: Bool = false
|
||||
@GestureState private var isActive = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { bounds in
|
||||
|
|
@ -50,6 +50,8 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
|
|||
.onTapGesture {
|
||||
handleIconTap()
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityLabel("Speaker Icon")
|
||||
}
|
||||
.frame(width: isActive ? bounds.size.width * 1.02 : bounds.size.width, alignment: .center)
|
||||
.animation(animation, value: isActive)
|
||||
|
|
@ -72,7 +74,7 @@ struct VolumeSlider<T: BinaryFloatingPoint>: View {
|
|||
if !newValue {
|
||||
value = sliderValueInRange()
|
||||
}
|
||||
onEditingChanged(newValue)
|
||||
onEditingChanged?(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
localRealProgress = progress(for: value)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
// Created by Francesco on 23/02/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
import MediaPlayer
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import MarqueeLabel
|
||||
import MediaPlayer
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate {
|
||||
let module: ScrapingModule
|
||||
|
|
@ -22,11 +22,11 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
let episodeNumber: Int
|
||||
let episodeImageUrl: String
|
||||
let subtitlesURL: String?
|
||||
let onWatchNext: () -> Void
|
||||
let onWatchNext: (() -> Void)?
|
||||
let aniListID: Int
|
||||
|
||||
private var aniListUpdatedSuccessfully = false
|
||||
private var aniListUpdateImpossible: Bool = false
|
||||
private var aniListUpdateImpossible = false
|
||||
private var aniListRetryCount = 0
|
||||
private let aniListMaxRetries = 6
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
fullUrl: String,
|
||||
title: String,
|
||||
episodeNumber: Int,
|
||||
onWatchNext: @escaping () -> Void,
|
||||
onWatchNext: (() -> Void)?,
|
||||
subtitlesURL: String?,
|
||||
aniListID: Int,
|
||||
episodeImageUrl: String) {
|
||||
|
|
@ -383,7 +383,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func playerItemDidChange() {
|
||||
@objc
|
||||
private func playerItemDidChange() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if self.qualityButton.isHidden && self.isHLSStream {
|
||||
|
|
@ -1373,10 +1374,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
UserDefaults.standard.set(self.currentTimeVal, forKey: "lastPlayedTime_\(self.fullUrl)")
|
||||
UserDefaults.standard.set(self.duration, forKey: "totalTime_\(self.fullUrl)")
|
||||
|
||||
if self.subtitlesEnabled {
|
||||
if subtitlesEnabled {
|
||||
let adjustedTime = self.currentTimeVal - self.subtitleDelay
|
||||
let cues = self.subtitlesLoader.cues.filter { adjustedTime >= $0.startTime && adjustedTime <= $0.endTime }
|
||||
if cues.count > 0 {
|
||||
if cues.isEmpty {
|
||||
self.subtitleLabels[0].text = cues[0].text.strippedHTML
|
||||
self.subtitleLabels[0].isHidden = false
|
||||
} else {
|
||||
|
|
@ -1478,14 +1479,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func skipIntro() {
|
||||
@objc
|
||||
private func skipIntro() {
|
||||
if let range = skipIntervals.op {
|
||||
player.seek(to: range.end)
|
||||
skipIntroButton.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func skipOutro() {
|
||||
@objc
|
||||
private func skipOutro() {
|
||||
if let range = skipIntervals.ed {
|
||||
player.seek(to: range.end)
|
||||
skipOutroButton.isHidden = true
|
||||
|
|
@ -1515,7 +1518,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
currentMenuButtonTrailing.isActive = true
|
||||
}
|
||||
|
||||
@objc func toggleControls() {
|
||||
@objc
|
||||
func toggleControls() {
|
||||
if isDimmed {
|
||||
dimButton.isHidden = false
|
||||
dimButton.alpha = 1.0
|
||||
|
|
@ -1542,7 +1546,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
@objc
|
||||
func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
if gesture.state == .began {
|
||||
let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
|
||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
|
|
@ -1554,7 +1559,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
@objc
|
||||
func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
|
||||
if gesture.state == .began {
|
||||
let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
|
||||
let finalSkip = holdValue > 0 ? holdValue : 30
|
||||
|
|
@ -1566,7 +1572,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc func seekBackward() {
|
||||
@objc
|
||||
func seekBackward() {
|
||||
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||
currentTimeVal = max(currentTimeVal - finalSkip, 0)
|
||||
|
|
@ -1576,7 +1583,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
animateButtonRotation(backwardButton, clockwise: false)
|
||||
}
|
||||
|
||||
@objc func seekForward() {
|
||||
@objc
|
||||
func seekForward() {
|
||||
let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
|
||||
let finalSkip = skipValue > 0 ? skipValue : 10
|
||||
currentTimeVal = min(currentTimeVal + finalSkip, duration)
|
||||
|
|
@ -1585,7 +1593,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
animateButtonRotation(forwardButton)
|
||||
}
|
||||
|
||||
@objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||
@objc
|
||||
func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
|
||||
let tapLocation = gesture.location(in: view)
|
||||
if tapLocation.x < view.bounds.width / 2 {
|
||||
seekBackward()
|
||||
|
|
@ -1596,11 +1605,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) {
|
||||
@objc
|
||||
func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func togglePlayPause() {
|
||||
@objc
|
||||
func togglePlayPause() {
|
||||
if isPlaying {
|
||||
player.pause()
|
||||
isPlaying = false
|
||||
|
|
@ -1623,23 +1634,27 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc func dismissTapped() {
|
||||
@objc
|
||||
func dismissTapped() {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func watchNextTapped() {
|
||||
@objc
|
||||
func watchNextTapped() {
|
||||
player.pause()
|
||||
dismiss(animated: true) { [weak self] in
|
||||
self?.onWatchNext()
|
||||
self?.onWatchNext?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func skip85Tapped() {
|
||||
@objc
|
||||
func skip85Tapped() {
|
||||
currentTimeVal = min(currentTimeVal + 85, duration)
|
||||
player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
|
||||
}
|
||||
|
||||
@objc private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
||||
@objc
|
||||
private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
|
||||
guard isHoldPauseEnabled else { return }
|
||||
|
||||
if gesture.state == .began {
|
||||
|
|
@ -1647,7 +1662,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func dimTapped() {
|
||||
@objc
|
||||
private func dimTapped() {
|
||||
isDimmed.toggle()
|
||||
dimButtonTimer?.invalidate()
|
||||
|
||||
|
|
@ -1748,7 +1764,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in
|
||||
guard let self = self,
|
||||
let data = data,
|
||||
let data,
|
||||
let content = String(data: data, encoding: .utf8) else {
|
||||
Logger.shared.log("Failed to load m3u8 file")
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -2066,16 +2082,16 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
self.presentCustomDelayAlert()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
let resetDelayAction = UIAction(title: "Reset Delay") { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = 0.0 }
|
||||
self.subtitleDelay = 0.0
|
||||
self.loadSubtitleSettings()
|
||||
}
|
||||
|
||||
|
||||
let delayMenu = UIMenu(title: "Subtitle Delay", children: delayActions + [resetDelayAction])
|
||||
|
||||
|
||||
let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [
|
||||
subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu, delayMenu
|
||||
])
|
||||
|
|
@ -2208,7 +2224,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
|
||||
@objc
|
||||
private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
beginHoldSpeed()
|
||||
|
|
@ -2219,7 +2236,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||
@objc
|
||||
private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: view)
|
||||
|
||||
switch gesture.state {
|
||||
|
|
@ -2289,8 +2307,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
activeFillColor: .white,
|
||||
fillColor: .white.opacity(0.6),
|
||||
emptyColor: .white.opacity(0.3),
|
||||
height: 10,
|
||||
onEditingChanged: { _ in }
|
||||
height: 10
|
||||
)
|
||||
.shadow(color: Color.black.opacity(0.6), radius: 4, x: 0, y: 2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import UIKit
|
|||
|
||||
struct SubtitleSettings: Codable {
|
||||
var foregroundColor: UIColor = .white
|
||||
var fontSize: Double = 20.0
|
||||
var shadowRadius: Double = 1.0
|
||||
var backgroundEnabled: Bool = true
|
||||
var fontSize = 20.0
|
||||
var shadowRadius = 1.0
|
||||
var backgroundEnabled = true
|
||||
var bottomPadding: CGFloat = 20.0
|
||||
var subtitleDelay: Double = 0.0
|
||||
var subtitleDelay = 0.0
|
||||
}
|
||||
|
||||
class SubtitleSettingsManager {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class VTTSubtitlesLoader: ObservableObject {
|
|||
let format = determineSubtitleFormat(from: url)
|
||||
|
||||
URLSession.shared.dataTask(with: url) { data, _, error in
|
||||
guard let data = data,
|
||||
guard let data,
|
||||
let content = String(data: data, encoding: .utf8),
|
||||
error == nil else { return }
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,13 @@ class NormalPlayer: AVPlayerViewController {
|
|||
private func setupHoldGesture() {
|
||||
holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:)))
|
||||
holdGesture?.minimumPressDuration = 0.5
|
||||
if let holdGesture = holdGesture {
|
||||
if let holdGesture {
|
||||
view.addGestureRecognizer(holdGesture)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
|
||||
@objc
|
||||
private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
beginHoldSpeed()
|
||||
|
|
@ -37,7 +38,7 @@ class NormalPlayer: AVPlayerViewController {
|
|||
}
|
||||
|
||||
private func beginHoldSpeed() {
|
||||
guard let player = player else { return }
|
||||
guard let player else { return }
|
||||
originalRate = player.rate
|
||||
let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")
|
||||
player.rate = holdSpeed > 0 ? holdSpeed : 2.0
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ private struct ModuleLink: Identifiable {
|
|||
|
||||
struct CommunityLibraryView: View {
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
@AppStorage("lastCommunityURL") private var inputURL = ""
|
||||
|
||||
@AppStorage("lastCommunityURL") private var inputURL: String = ""
|
||||
@State private var webURL: URL?
|
||||
@State private var errorMessage: String?
|
||||
@State private var moduleLinkToAdd: ModuleLink?
|
||||
|
|
@ -30,7 +30,6 @@ struct CommunityLibraryView: View {
|
|||
}
|
||||
|
||||
WebView(url: webURL) { linkURL in
|
||||
|
||||
if let comps = URLComponents(url: linkURL, resolvingAgainstBaseURL: false),
|
||||
let m = comps.queryItems?.first(where: { $0.name == "url" })?.value {
|
||||
moduleLinkToAdd = ModuleLink(url: m)
|
||||
|
|
@ -92,6 +91,7 @@ struct WebView: UIViewRepresentable {
|
|||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
let onCustom: (URL) -> Void
|
||||
|
||||
init(onCustom: @escaping (URL) -> Void) { self.onCustom = onCustom }
|
||||
|
||||
func webView(_ webView: WKWebView,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 01/02/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct ModuleAdditionSettingsView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
|
@ -81,7 +81,6 @@ struct ModuleAdditionSettingsView: View {
|
|||
}
|
||||
|
||||
Divider()
|
||||
|
||||
} else if isLoading {
|
||||
VStack(spacing: 20) {
|
||||
ProgressView()
|
||||
|
|
@ -91,11 +90,12 @@ struct ModuleAdditionSettingsView: View {
|
|||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.top, 100)
|
||||
} else if let errorMessage = errorMessage {
|
||||
} else if let errorMessage {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 50))
|
||||
.foregroundColor(.red)
|
||||
.accessibilityLabel("Error Icon")
|
||||
Text(errorMessage)
|
||||
.foregroundColor(.red)
|
||||
.multilineTextAlignment(.center)
|
||||
|
|
@ -112,6 +112,7 @@ struct ModuleAdditionSettingsView: View {
|
|||
Button(action: addModule) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.accessibilityLabel("+ Icon")
|
||||
Text("Add Module")
|
||||
}
|
||||
.font(.headline)
|
||||
|
|
@ -128,7 +129,7 @@ struct ModuleAdditionSettingsView: View {
|
|||
.opacity(isLoading ? 0.6 : 1)
|
||||
|
||||
Button(action: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}) {
|
||||
Text("Cancel")
|
||||
.foregroundColor(.accentColor)
|
||||
|
|
@ -148,8 +149,8 @@ struct ModuleAdditionSettingsView: View {
|
|||
Task {
|
||||
guard let url = URL(string: moduleUrl) else {
|
||||
await MainActor.run {
|
||||
self.errorMessage = "Invalid URL"
|
||||
self.isLoading = false
|
||||
errorMessage = "Invalid URL"
|
||||
isLoading = false
|
||||
Logger.shared.log("Failed to open add module ui with url: \(moduleUrl)", type: "Error")
|
||||
}
|
||||
return
|
||||
|
|
@ -158,13 +159,13 @@ struct ModuleAdditionSettingsView: View {
|
|||
let (data, _) = try await URLSession.custom.data(from: url)
|
||||
let metadata = try JSONDecoder().decode(ModuleMetadata.self, from: data)
|
||||
await MainActor.run {
|
||||
self.moduleMetadata = metadata
|
||||
self.isLoading = false
|
||||
moduleMetadata = metadata
|
||||
isLoading = false
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.errorMessage = "Failed to fetch module: \(error.localizedDescription)"
|
||||
self.isLoading = false
|
||||
errorMessage = "Failed to fetch module: \(error.localizedDescription)"
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +179,7 @@ struct ModuleAdditionSettingsView: View {
|
|||
await MainActor.run {
|
||||
isLoading = false
|
||||
DropManager.shared.showDrop(title: "Module Added", subtitle: "Click it to select it.", duration: 2.0, icon: UIImage(systemName: "gear.badge.checkmark"))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
|
|
|
|||
|
|
@ -31,21 +31,22 @@ class ModuleManager: ObservableObject {
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func handleModulesSyncCompleted() {
|
||||
@objc
|
||||
private func handleModulesSyncCompleted() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let self else { return }
|
||||
|
||||
let url = self.getModulesFilePath()
|
||||
let url = getModulesFilePath()
|
||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||
Logger.shared.log("No modules file found after sync", type: "Error")
|
||||
self.modules = []
|
||||
modules = []
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let decodedModules = try JSONDecoder().decode([ScrapingModule].self, from: data)
|
||||
self.modules = decodedModules
|
||||
modules = decodedModules
|
||||
|
||||
Task {
|
||||
await self.checkJSModuleFiles()
|
||||
|
|
@ -53,7 +54,7 @@ class ModuleManager: ObservableObject {
|
|||
Logger.shared.log("Reloaded modules after iCloud sync")
|
||||
} catch {
|
||||
Logger.shared.log("Error handling modules sync: \(error.localizedDescription)", type: "Error")
|
||||
self.modules = []
|
||||
modules = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ struct ScrapingModule: Codable, Identifiable, Hashable {
|
|||
hasher.combine(id)
|
||||
}
|
||||
|
||||
static func == (lhs: ScrapingModule, rhs: ScrapingModule) -> Bool {
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import Foundation
|
||||
|
||||
struct Profile: Identifiable, Equatable, Codable {
|
||||
var id: UUID = UUID()
|
||||
var id = UUID()
|
||||
var name: String
|
||||
var emoji: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,16 @@
|
|||
import SwiftUI
|
||||
|
||||
class ProfileStore: ObservableObject {
|
||||
@AppStorage("profilesData") private var profilesData: Data = Data()
|
||||
@AppStorage("currentProfileID") private var currentProfileID: String = ""
|
||||
@AppStorage("profilesData") private var profilesData = Data()
|
||||
@AppStorage("currentProfileID") private var currentProfileID = ""
|
||||
|
||||
@Published public var profiles: [Profile] = []
|
||||
@Published public var currentProfile: Profile!
|
||||
@Published var profiles: [Profile] = []
|
||||
@Published var currentProfile: Profile!
|
||||
|
||||
public init() {
|
||||
init() {
|
||||
profiles = (try? JSONDecoder().decode([Profile].self, from: profilesData)) ?? []
|
||||
|
||||
if profiles.isEmpty {
|
||||
|
||||
// load default value
|
||||
let defaultProfile = Profile(name: String(localized: "Default User"), emoji: "👤")
|
||||
profiles = [defaultProfile]
|
||||
|
|
@ -26,7 +25,6 @@ class ProfileStore: ObservableObject {
|
|||
saveProfiles()
|
||||
setCurrentProfile(defaultProfile)
|
||||
} else {
|
||||
|
||||
// load current profile
|
||||
if let uuid = UUID(uuidString: currentProfileID),
|
||||
let match = profiles.first(where: { $0.id == uuid }) {
|
||||
|
|
@ -39,7 +37,7 @@ class ProfileStore: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func getUserDefaultsSuite() -> UserDefaults {
|
||||
func getUserDefaultsSuite() -> UserDefaults {
|
||||
guard let suite = UserDefaults(suiteName: currentProfile.id.uuidString) else {
|
||||
fatalError("This can only fail if suiteName == app bundle id ...")
|
||||
}
|
||||
|
|
@ -53,12 +51,12 @@ class ProfileStore: ObservableObject {
|
|||
profilesData = (try? JSONEncoder().encode(profiles)) ?? Data()
|
||||
}
|
||||
|
||||
public func setCurrentProfile(_ profile: Profile) {
|
||||
func setCurrentProfile(_ profile: Profile) {
|
||||
currentProfile = profile
|
||||
currentProfileID = profile.id.uuidString
|
||||
}
|
||||
|
||||
public func addProfile(name: String, emoji: String) {
|
||||
func addProfile(name: String, emoji: String) {
|
||||
let newProfile = Profile(name: name, emoji: emoji)
|
||||
profiles.append(newProfile)
|
||||
|
||||
|
|
@ -66,7 +64,7 @@ class ProfileStore: ObservableObject {
|
|||
setCurrentProfile(newProfile)
|
||||
}
|
||||
|
||||
public func editCurrentProfile(name: String, emoji: String) {
|
||||
func editCurrentProfile(name: String, emoji: String) {
|
||||
guard let index = profiles.firstIndex(where: { $0.id == currentProfile.id }) else { return }
|
||||
profiles[index].name = name
|
||||
profiles[index].emoji = emoji
|
||||
|
|
@ -75,7 +73,7 @@ class ProfileStore: ObservableObject {
|
|||
setCurrentProfile(profiles[index])
|
||||
}
|
||||
|
||||
public func deleteProfile(removalID: UUID?) {
|
||||
func deleteProfile(removalID: UUID?) {
|
||||
guard let removalID,
|
||||
profiles.count == 1
|
||||
else { return }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
|
||||
enum ShimmerType: String, CaseIterable, Identifiable {
|
||||
case shimmer, pulse, none
|
||||
|
||||
var id: String { self.rawValue }
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ struct ShimmerDefault: ViewModifier {
|
|||
.mask(content)
|
||||
.onAppear {
|
||||
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
|
||||
self.phase = 1
|
||||
phase = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +56,7 @@ struct ShimmerDefault: ViewModifier {
|
|||
|
||||
struct ShimmerPulse: ViewModifier {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@State private var opacity: Double = 0.3
|
||||
@State private var opacity = 0.3
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
|
|
@ -69,7 +70,7 @@ struct ShimmerPulse: ViewModifier {
|
|||
.mask(content)
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
|
||||
self.opacity = 0.8
|
||||
opacity = 0.8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import UIKit
|
|||
|
||||
// TODO: update "clear data" feature
|
||||
// TODO: tests
|
||||
class iCloudSyncManager {
|
||||
static let shared = iCloudSyncManager()
|
||||
class ICloudSyncManager {
|
||||
static let shared = ICloudSyncManager()
|
||||
|
||||
private let defaultsToSync: [String] = [
|
||||
"externalPlayer",
|
||||
|
|
@ -72,7 +72,8 @@ class iCloudSyncManager {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc private func willEnterBackground() {
|
||||
@objc
|
||||
private func willEnterBackground() {
|
||||
syncToiCloud()
|
||||
syncModulesToiCloud()
|
||||
}
|
||||
|
|
@ -125,7 +126,8 @@ class iCloudSyncManager {
|
|||
iCloud.synchronize()
|
||||
}
|
||||
|
||||
@objc private func iCloudDidChangeExternally(_ notification: Notification) {
|
||||
@objc
|
||||
private func iCloudDidChangeExternally(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let reason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int else {
|
||||
return
|
||||
|
|
@ -137,7 +139,8 @@ class iCloudSyncManager {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func userDefaultsDidChange(_ notification: Notification) {
|
||||
@objc
|
||||
private func userDefaultsDidChange(_ notification: Notification) {
|
||||
syncToiCloud()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,33 +5,33 @@
|
|||
// Created by Francesco on 29/04/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
|
||||
struct DownloadView: View {
|
||||
@StateObject private var viewModel = DownloadManager()
|
||||
@State private var hlsURL = "https://test-streams.mux.dev/x36xhzz/url_6/193039199_mp4_h264_aac_hq_7.m3u8"
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
TextField("Enter HLS URL", text: $hlsURL)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding()
|
||||
|
||||
|
||||
Button("Download Stream") {
|
||||
viewModel.downloadAsset(from: URL(string: hlsURL)!)
|
||||
}
|
||||
.padding()
|
||||
|
||||
List(viewModel.activeDownloads, id: \.0) { (url, progress) in
|
||||
|
||||
List(viewModel.activeDownloads, id: \.0) { url, progress in
|
||||
VStack(alignment: .leading) {
|
||||
Text(url.absoluteString)
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(LinearProgressViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NavigationLink("Play Offline Content") {
|
||||
if let url = viewModel.localPlaybackURL {
|
||||
VideoPlayer(player: AVPlayer(url: url))
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Dominic on 24.04.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct ExploreItem: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -18,8 +18,8 @@ struct ExploreItem: Identifiable {
|
|||
struct ExploreView: View {
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections: Bool?
|
||||
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape = 4
|
||||
|
||||
@StateObject private var jsController = JSController()
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
|
@ -73,12 +73,12 @@ struct ExploreView: View {
|
|||
ScrollView {
|
||||
let columnsCount = determineColumns()
|
||||
VStack(spacing: 0) {
|
||||
|
||||
if !(hideEmptySections ?? false) && selectedModule == nil {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "questionmark.app")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Questionmark Icon")
|
||||
Text("No Module Selected")
|
||||
.font(.headline)
|
||||
Text("Please select a module from settings")
|
||||
|
|
@ -92,7 +92,7 @@ struct ExploreView: View {
|
|||
|
||||
if isLoading {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
|
||||
ForEach(0..<columnsCount*4, id: \.self) { _ in
|
||||
ForEach(0 ..< columnsCount * 4, id: \.self) { _ in
|
||||
SkeletonCell(type: .explore, cellWidth: cellWidth)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,6 +103,7 @@ struct ExploreView: View {
|
|||
Image(systemName: "star")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Star Icon")
|
||||
Text("No Content Available")
|
||||
.font(.headline)
|
||||
Text("Try updating the Module")
|
||||
|
|
@ -182,7 +183,6 @@ struct ExploreView: View {
|
|||
} label: {
|
||||
Label("Edit Profiles", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
|
||||
} label: {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.3))
|
||||
|
|
@ -198,7 +198,9 @@ struct ExploreView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
if getModuleLanguageGroups().isEmpty {
|
||||
Button("No modules available") { }
|
||||
Button("No modules available") {
|
||||
print("[Error] No Modules Button clicked")
|
||||
}
|
||||
.disabled(true)
|
||||
|
||||
Divider()
|
||||
|
|
@ -225,6 +227,7 @@ struct ExploreView: View {
|
|||
if module.id.uuidString == selectedModuleId {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +237,7 @@ struct ExploreView: View {
|
|||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
if let selectedModule = selectedModule {
|
||||
if let selectedModule {
|
||||
Text(selectedModule.metadata.sourceName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -245,6 +248,7 @@ struct ExploreView: View {
|
|||
}
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Expand Icon")
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
|
|
@ -311,7 +315,7 @@ struct ExploreView: View {
|
|||
}
|
||||
|
||||
private func cleanLanguageName(_ language: String?) -> String {
|
||||
guard let language = language else { return "Unknown" }
|
||||
guard let language else { return "Unknown" }
|
||||
|
||||
let cleaned = language.replacingOccurrences(
|
||||
of: "\\s*\\([^\\)]*\\)",
|
||||
|
|
@ -338,10 +342,10 @@ struct ExploreView: View {
|
|||
}
|
||||
|
||||
private func getModuleLanguageGroups() -> [String] {
|
||||
return getModulesByLanguage().keys.sorted()
|
||||
getModulesByLanguage().keys.sorted()
|
||||
}
|
||||
|
||||
private func getModulesForLanguage(_ language: String) -> [ScrapingModule] {
|
||||
return getModulesByLanguage()[language] ?? []
|
||||
getModulesByLanguage()[language] ?? []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,13 @@ class LibraryManager: ObservableObject {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(handleiCloudSync), name: .iCloudSyncDidComplete, object: nil)
|
||||
}
|
||||
|
||||
public func updateProfileSuite(_ newSuite: UserDefaults) {
|
||||
func updateProfileSuite(_ newSuite: UserDefaults) {
|
||||
userDefaultsSuite = newSuite
|
||||
loadBookmarks()
|
||||
}
|
||||
|
||||
@objc private func handleiCloudSync() {
|
||||
@objc
|
||||
private func handleiCloudSync() {
|
||||
DispatchQueue.main.async {
|
||||
self.loadBookmarks()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryView: View {
|
||||
@EnvironmentObject private var libraryManager: LibraryManager
|
||||
|
|
@ -15,13 +15,13 @@ struct LibraryView: View {
|
|||
@EnvironmentObject private var profileStore: ProfileStore
|
||||
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections: Bool?
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape = 4
|
||||
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@State private var selectedBookmark: LibraryItem?
|
||||
@State private var isDetailActive: Bool = false
|
||||
@State private var isDetailActive = false
|
||||
|
||||
@State private var continueWatchingItems: [ContinueWatchingItem] = []
|
||||
@State private var isLandscape: Bool = UIDevice.current.orientation.isLandscape
|
||||
|
|
@ -57,7 +57,6 @@ struct LibraryView: View {
|
|||
let columnsCount = determineColumns()
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
|
||||
if hideEmptySections != true || !continueWatchingManager.items.isEmpty {
|
||||
Text("Continue Watching")
|
||||
.font(.title2)
|
||||
|
|
@ -70,6 +69,7 @@ struct LibraryView: View {
|
|||
Image(systemName: "play.circle")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Play Icon")
|
||||
Text("No items to continue watching.")
|
||||
.font(.headline)
|
||||
Text("Recently watched content will appear here.")
|
||||
|
|
@ -98,6 +98,7 @@ struct LibraryView: View {
|
|||
Image(systemName: "magazine")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Magazine Icon")
|
||||
Text("You have no items saved.")
|
||||
.font(.headline)
|
||||
Text("Bookmark items for an easier access later.")
|
||||
|
|
@ -120,7 +121,7 @@ struct LibraryView: View {
|
|||
.placeholder {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(2/3, contentMode: .fit)
|
||||
.aspectRatio(2 / 3, contentMode: .fit)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
|
|
@ -160,10 +161,12 @@ struct LibraryView: View {
|
|||
destination: Group {
|
||||
if let bookmark = selectedBookmark,
|
||||
let module = moduleManager.modules.first(where: { $0.id.uuidString == bookmark.moduleId }) {
|
||||
MediaInfoView(title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module)
|
||||
MediaInfoView(
|
||||
title: bookmark.title,
|
||||
imageUrl: bookmark.imageUrl,
|
||||
href: bookmark.href,
|
||||
module: module
|
||||
)
|
||||
} else {
|
||||
Text("No Data Available")
|
||||
}
|
||||
|
|
@ -212,7 +215,6 @@ struct LibraryView: View {
|
|||
} label: {
|
||||
Label("Edit Profiles", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
|
||||
} label: {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.3))
|
||||
|
|
@ -239,8 +241,8 @@ struct LibraryView: View {
|
|||
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)
|
||||
UserDefaults.standard.set(99_999_999.0, forKey: key)
|
||||
UserDefaults.standard.set(99_999_999.0, forKey: totalKey)
|
||||
continueWatchingManager.remove(item: item)
|
||||
}
|
||||
|
||||
|
|
@ -294,7 +296,7 @@ struct ContinueWatchingCell: View {
|
|||
var markAsWatched: () -> Void
|
||||
var removeItem: () -> Void
|
||||
|
||||
@State private var currentProgress: Double = 0.0
|
||||
@State private var currentProgress = 0.0
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
|
|
@ -311,7 +313,7 @@ struct ContinueWatchingCell: View {
|
|||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
FindTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
} else {
|
||||
let customMediaPlayer = CustomMediaPlayerViewController(
|
||||
|
|
@ -321,7 +323,7 @@ struct ContinueWatchingCell: View {
|
|||
fullUrl: item.fullUrl,
|
||||
title: item.mediaTitle,
|
||||
episodeNumber: item.episodeNumber,
|
||||
onWatchNext: { },
|
||||
onWatchNext: nil,
|
||||
subtitlesURL: item.subtitles,
|
||||
aniListID: item.aniListID ?? 0,
|
||||
episodeImageUrl: item.imageUrl
|
||||
|
|
@ -330,7 +332,7 @@ struct ContinueWatchingCell: 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)
|
||||
FindTopViewController.findViewController(rootVC).present(customMediaPlayer, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
|
|
@ -345,7 +347,7 @@ struct ContinueWatchingCell: View {
|
|||
}
|
||||
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.aspectRatio(16 / 9, contentMode: .fill)
|
||||
.frame(width: 240, height: 135)
|
||||
.cornerRadius(10)
|
||||
.clipped()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ struct CircularProgressBar: View {
|
|||
if progress >= 0.9 {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.system(size: 12))
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
} else {
|
||||
Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
|
||||
.font(.system(size: 12))
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 18/12/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct EpisodeLink: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -24,10 +24,10 @@ struct EpisodeCell: View {
|
|||
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
|
||||
@State private var episodeTitle = ""
|
||||
@State private var episodeImageUrl = ""
|
||||
@State private var isLoading = true
|
||||
@State private var currentProgress = 0.0
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system
|
||||
|
|
@ -39,8 +39,15 @@ struct EpisodeCell: View {
|
|||
: "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png"
|
||||
}
|
||||
|
||||
init(episodeIndex: Int, episode: String, episodeID: Int, progress: Double,
|
||||
itemID: Int, onTap: @escaping (String) -> Void, onMarkAllPrevious: @escaping () -> Void) {
|
||||
init(
|
||||
episodeIndex: Int,
|
||||
episode: String,
|
||||
episodeID: Int,
|
||||
progress: Double,
|
||||
itemID: Int,
|
||||
onTap: @escaping (String) -> Void,
|
||||
onMarkAllPrevious: @escaping () -> Void
|
||||
) {
|
||||
self.episodeIndex = episodeIndex
|
||||
self.episode = episode
|
||||
self.episodeID = episodeID
|
||||
|
|
@ -55,10 +62,10 @@ struct EpisodeCell: View {
|
|||
ZStack {
|
||||
KFImage(URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl))
|
||||
.resizable()
|
||||
.aspectRatio(16/9, contentMode: .fill)
|
||||
.aspectRatio(16 / 9, contentMode: .fill)
|
||||
.frame(width: 100, height: 56)
|
||||
.cornerRadius(8)
|
||||
|
||||
.accessibilityLabel("Episode Icon")
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
|
|
@ -107,6 +114,7 @@ struct EpisodeCell: View {
|
|||
.onChange(of: progress) { _ in
|
||||
updateProgress()
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.onTapGesture {
|
||||
let imageUrl = episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl
|
||||
onTap(imageUrl)
|
||||
|
|
@ -120,7 +128,7 @@ struct EpisodeCell: View {
|
|||
userDefaults.set(watchedTime, forKey: "lastPlayedTime_\(episode)")
|
||||
userDefaults.set(totalTime, forKey: "totalTime_\(episode)")
|
||||
DispatchQueue.main.async {
|
||||
self.updateProgress()
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +137,7 @@ struct EpisodeCell: View {
|
|||
userDefaults.set(0.0, forKey: "lastPlayedTime_\(episode)")
|
||||
userDefaults.set(0.0, forKey: "totalTime_\(episode)")
|
||||
DispatchQueue.main.async {
|
||||
self.updateProgress()
|
||||
updateProgress()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +159,14 @@ struct EpisodeCell: View {
|
|||
}
|
||||
|
||||
URLSession.custom.dataTask(with: url) { data, _, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Failed to fetch anime episode details: \(error)", type: "Error")
|
||||
DispatchQueue.main.async { self.isLoading = false }
|
||||
DispatchQueue.main.async { isLoading = false }
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
DispatchQueue.main.async { self.isLoading = false }
|
||||
guard let data else {
|
||||
DispatchQueue.main.async { isLoading = false }
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -170,20 +178,20 @@ struct EpisodeCell: View {
|
|||
let title = episodeDetails["title"] as? [String: String],
|
||||
let image = episodeDetails["image"] as? String else {
|
||||
Logger.shared.log("Invalid anime response format", type: "Error")
|
||||
DispatchQueue.main.async { self.isLoading = false }
|
||||
DispatchQueue.main.async { isLoading = false }
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
isLoading = false
|
||||
if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil
|
||||
|| UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") {
|
||||
self.episodeTitle = title["en"] ?? ""
|
||||
self.episodeImageUrl = image
|
||||
episodeTitle = title["en"] ?? ""
|
||||
episodeImageUrl = image
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async { self.isLoading = false }
|
||||
DispatchQueue.main.async { isLoading = false }
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SafariServices
|
||||
import SwiftUI
|
||||
|
||||
struct MediaItem: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -609,30 +609,30 @@ struct MediaInfoView: View {
|
|||
if module.metadata.asyncJS == true {
|
||||
jsController.fetchDetailsJS(url: href) { items, episodes in
|
||||
if let item = items.first {
|
||||
self.synopsis = item.description
|
||||
self.aliases = item.aliases
|
||||
self.airdate = item.airdate
|
||||
synopsis = item.description
|
||||
aliases = item.aliases
|
||||
airdate = item.airdate
|
||||
}
|
||||
self.episodeLinks = episodes
|
||||
self.isLoading = false
|
||||
self.isRefetching = false
|
||||
episodeLinks = episodes
|
||||
isLoading = false
|
||||
isRefetching = false
|
||||
}
|
||||
} else {
|
||||
jsController.fetchDetails(url: href) { items, episodes in
|
||||
if let item = items.first {
|
||||
self.synopsis = item.description
|
||||
self.aliases = item.aliases
|
||||
self.airdate = item.airdate
|
||||
synopsis = item.description
|
||||
aliases = item.aliases
|
||||
airdate = item.airdate
|
||||
}
|
||||
self.episodeLinks = episodes
|
||||
self.isLoading = false
|
||||
self.isRefetching = false
|
||||
episodeLinks = episodes
|
||||
isLoading = false
|
||||
isRefetching = false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||
self.isLoading = false
|
||||
self.isRefetching = false
|
||||
isLoading = false
|
||||
isRefetching = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -770,7 +770,7 @@ struct MediaInfoView: View {
|
|||
func handleStreamFailure(error: Error? = nil) {
|
||||
self.isFetchingEpisode = false
|
||||
self.showStreamLoadingView = false
|
||||
if let error = error {
|
||||
if let error {
|
||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||
AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"])
|
||||
}
|
||||
|
|
@ -835,7 +835,7 @@ struct MediaInfoView: View {
|
|||
}
|
||||
}
|
||||
|
||||
findTopViewController.findViewController(rootVC).present(alert, animated: true)
|
||||
FindTopViewController.findViewController(rootVC).present(alert, animated: true)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -878,7 +878,7 @@ struct MediaInfoView: View {
|
|||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let rootVC = windowScene.windows.first?.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
FindTopViewController.findViewController(rootVC).present(videoPlayerViewController, animated: true, completion: nil)
|
||||
}
|
||||
return
|
||||
default:
|
||||
|
|
@ -914,7 +914,7 @@ 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)
|
||||
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"))
|
||||
|
|
@ -982,12 +982,12 @@ struct MediaInfoView: View {
|
|||
request.httpBody = try? JSONSerialization.data(withJSONObject: parameters)
|
||||
|
||||
URLSession.custom.dataTask(with: request) { data, _, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"])))
|
||||
return
|
||||
}
|
||||
|
|
@ -1033,7 +1033,7 @@ struct MediaInfoView: View {
|
|||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first,
|
||||
let rootVC = window.rootViewController {
|
||||
findTopViewController.findViewController(rootVC).present(alert, animated: true)
|
||||
FindTopViewController.findViewController(rootVC).present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct SearchItem: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -18,8 +18,8 @@ struct SearchItem: Identifiable {
|
|||
struct SearchView: View {
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections: Bool?
|
||||
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape = 4
|
||||
|
||||
@StateObject private var jsController = JSController()
|
||||
@EnvironmentObject private var moduleManager: ModuleManager
|
||||
|
|
@ -96,6 +96,7 @@ struct SearchView: View {
|
|||
Image(systemName: "questionmark.app")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Questionmark Icon")
|
||||
Text("No Module Selected")
|
||||
.font(.headline)
|
||||
Text("Please select a module from settings")
|
||||
|
|
@ -110,7 +111,7 @@ struct SearchView: View {
|
|||
if !searchText.isEmpty {
|
||||
if isSearching {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: columnsCount), spacing: 16) {
|
||||
ForEach(0..<columnsCount*4, id: \.self) { _ in
|
||||
ForEach(0 ..< columnsCount * 4, id: \.self) { _ in
|
||||
SkeletonCell(type: .search, cellWidth: cellWidth)
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +122,7 @@ struct SearchView: View {
|
|||
Image(systemName: "magnifyingglass")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Magnifying Glass Icon")
|
||||
Text("No Results Found")
|
||||
.font(.headline)
|
||||
Text("Try different keywords")
|
||||
|
|
@ -201,7 +203,6 @@ struct SearchView: View {
|
|||
} label: {
|
||||
Label("Edit Profiles", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
|
||||
} label: {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.3))
|
||||
|
|
@ -216,7 +217,9 @@ struct SearchView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
if getModuleLanguageGroups().isEmpty {
|
||||
Button("No modules available") { }
|
||||
Button("No modules available") {
|
||||
print("[Error] No Modules Button clicked")
|
||||
}
|
||||
.disabled(true)
|
||||
|
||||
Divider()
|
||||
|
|
@ -243,6 +246,7 @@ struct SearchView: View {
|
|||
if module.id.uuidString == selectedModuleId {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +256,7 @@ struct SearchView: View {
|
|||
}
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
if let selectedModule = selectedModule {
|
||||
if let selectedModule {
|
||||
Text(selectedModule.metadata.sourceName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
@ -263,6 +267,7 @@ struct SearchView: View {
|
|||
}
|
||||
Image(systemName: "chevron.down")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("Expand Icon")
|
||||
}
|
||||
}
|
||||
.fixedSize()
|
||||
|
|
@ -338,7 +343,7 @@ struct SearchView: View {
|
|||
}
|
||||
|
||||
private func cleanLanguageName(_ language: String?) -> String {
|
||||
guard let language = language else { return "Unknown" }
|
||||
guard let language else { return "Unknown" }
|
||||
|
||||
let cleaned = language.replacingOccurrences(
|
||||
of: "\\s*\\([^\\)]*\\)",
|
||||
|
|
@ -365,11 +370,11 @@ struct SearchView: View {
|
|||
}
|
||||
|
||||
private func getModuleLanguageGroups() -> [String] {
|
||||
return getModulesByLanguage().keys.sorted()
|
||||
getModulesByLanguage().keys.sorted()
|
||||
}
|
||||
|
||||
private func getModulesForLanguage(_ language: String) -> [ScrapingModule] {
|
||||
return getModulesByLanguage()[language] ?? []
|
||||
getModulesByLanguage()[language] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,27 +391,28 @@ struct SearchBar: View {
|
|||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.onChange(of: text) {_ in
|
||||
debounceTimer?.invalidate()
|
||||
// Start a new timer to wait before performing the action
|
||||
debounceTimer?.invalidate()
|
||||
// Start a new timer to wait before performing the action
|
||||
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
|
||||
// Perform the action after the delay (debouncing)
|
||||
onSearchButtonClicked()
|
||||
}
|
||||
}
|
||||
// Perform the action after the delay (debouncing)
|
||||
onSearchButtonClicked()
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 8)
|
||||
|
||||
.accessibilityLabel("Search Icon")
|
||||
if !text.isEmpty {
|
||||
Button(action: {
|
||||
self.text = ""
|
||||
text = ""
|
||||
}) {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.trailing, 8)
|
||||
.accessibilityLabel("Clear Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||
|
||||
struct SettingsViewAlternateAppIconPicker: View {
|
||||
@Binding var isPresented: Bool
|
||||
@AppStorage("currentAppIcon") private var currentAppIcon: String = "Default"
|
||||
@AppStorage("currentAppIcon") private var currentAppIcon = "Default"
|
||||
|
||||
let icons: [(name: String, icon: String)] = [
|
||||
("Default", "Default"),
|
||||
|
|
@ -39,11 +39,12 @@ struct SettingsViewAlternateAppIconPicker: View {
|
|||
currentAppIcon == icon.name ? Color.accentColor.opacity(0.3) : Color.clear
|
||||
)
|
||||
.cornerRadius(10)
|
||||
|
||||
.accessibilityLabel("Alternative App Icon")
|
||||
Text(icon.name)
|
||||
.font(.caption)
|
||||
.foregroundColor(currentAppIcon == icon.name ? .accentColor : .primary)
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.onTapGesture {
|
||||
currentAppIcon = icon.name
|
||||
setAppIcon(named: icon.icon)
|
||||
|
|
@ -61,7 +62,7 @@ struct SettingsViewAlternateAppIconPicker: View {
|
|||
if UIApplication.shared.supportsAlternateIcons {
|
||||
UIApplication.shared.setAlternateIconName(iconName == "Default" ? nil : "AppIcon_\(iconName)", completionHandler: { error in
|
||||
isPresented = false
|
||||
if let error = error {
|
||||
if let error {
|
||||
print("Failed to set alternate icon: \(error.localizedDescription)")
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ struct SettingsViewData: View {
|
|||
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||
|
||||
do {
|
||||
if let cacheURL = cacheURL {
|
||||
if let cacheURL {
|
||||
let filePaths = try FileManager.default.contentsOfDirectory(at: cacheURL, includingPropertiesForKeys: nil, options: [])
|
||||
for filePath in filePaths {
|
||||
try FileManager.default.removeItem(at: filePath)
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsViewGeneral: View {
|
||||
@AppStorage("episodeChunkSize") private var episodeChunkSize: Int = 100
|
||||
@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"
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait: Int = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape: Int = 4
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections: Bool = false
|
||||
@AppStorage("currentAppIcon") private var currentAppIcon: String = "Default"
|
||||
@AppStorage("episodeSortOrder") private var episodeSortOrder: String = "Ascending"
|
||||
@AppStorage("episodeChunkSize") private var episodeChunkSize = 100
|
||||
@AppStorage("refreshModulesOnLaunch") private var refreshModulesOnLaunch = false
|
||||
@AppStorage("fetchEpisodeMetadata") private var fetchEpisodeMetadata = true
|
||||
@AppStorage("analyticsEnabled") private var analyticsEnabled = false
|
||||
@AppStorage("multiThreads") private var multiThreadsEnabled = false
|
||||
@AppStorage("metadataProviders") private var metadataProviders = "AniList"
|
||||
@AppStorage("mediaColumnsPortrait") private var mediaColumnsPortrait = 2
|
||||
@AppStorage("mediaColumnsLandscape") private var mediaColumnsLandscape = 4
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections = false
|
||||
@AppStorage("currentAppIcon") private var currentAppIcon = "Default"
|
||||
@AppStorage("episodeSortOrder") private var episodeSortOrder = "Ascending"
|
||||
|
||||
private let metadataProvidersList = ["AniList"]
|
||||
private let sortOrderOptions = ["Ascending", "Descending"]
|
||||
@EnvironmentObject var settings: Settings
|
||||
@State var showAppIconPicker: Bool = false
|
||||
@State private var showAppIconPicker = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
|
@ -75,7 +75,6 @@ struct SettingsViewGeneral: View {
|
|||
}
|
||||
|
||||
Section(header: Text("Media View"), footer: Text("The episode range controls how many episodes appear on each page. Episodes are grouped into sets (like 1-25, 26-50, and so on), allowing you to navigate through them more easily.\n\nFor episode metadata it is refering to the episode thumbnail and title, since sometimes it can contain spoilers.")) {
|
||||
|
||||
HStack {
|
||||
Text("Episodes Range")
|
||||
Spacer()
|
||||
|
|
@ -87,6 +86,7 @@ struct SettingsViewGeneral: View {
|
|||
if episodeChunkSize == chunkSize {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
Text("\(chunkSize)")
|
||||
}
|
||||
|
|
@ -108,6 +108,7 @@ struct SettingsViewGeneral: View {
|
|||
if provider == metadataProviders {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
Text(provider)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsViewLogger: View {
|
||||
@State private var logs: String = ""
|
||||
@State private var logs = ""
|
||||
@StateObject private var filterViewModel = LogFilterViewModel.shared
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -46,10 +46,12 @@ struct SettingsViewLogger: View {
|
|||
Image(systemName: "ellipsis.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.accessibilityLabel("More Icon")
|
||||
}
|
||||
|
||||
NavigationLink(destination: SettingsViewLoggerFilter(viewModel: filterViewModel)) {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.accessibilityLabel("Configure Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class LogFilterViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
func isFilterEnabled(for type: String) -> Bool {
|
||||
return filters.first(where: { $0.type == type })?.isEnabled ?? true
|
||||
filters.first(where: { $0.type == type })?.isEnabled ?? true
|
||||
}
|
||||
|
||||
private func saveFiltersToUserDefaults() {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// Created by Francesco on 05/01/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsViewModule: View {
|
||||
@EnvironmentObject var moduleManager: ModuleManager
|
||||
|
|
@ -14,12 +14,12 @@ struct SettingsViewModule: View {
|
|||
|
||||
@AppStorage("selectedModuleId") private var selectedModuleId: String?
|
||||
@AppStorage("hideEmptySections") private var hideEmptySections: Bool?
|
||||
@AppStorage("didReceiveDefaultPageLink") private var didReceiveDefaultPageLink: Bool = false
|
||||
@AppStorage("didReceiveDefaultPageLink") private var didReceiveDefaultPageLink = false
|
||||
|
||||
@State private var errorMessage: String?
|
||||
@State private var isLoading = false
|
||||
@State private var isRefreshing = false
|
||||
@State private var moduleUrl: String = ""
|
||||
@State private var moduleUrl = ""
|
||||
@State private var refreshTask: Task<Void, Never>?
|
||||
@State private var showLibrary = false
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ struct SettingsViewModule: View {
|
|||
Image(systemName: "plus.app")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("+ Icon")
|
||||
Text("No Modules")
|
||||
.font(.headline)
|
||||
|
||||
|
|
@ -84,9 +85,11 @@ struct SettingsViewModule: View {
|
|||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 25, height: 25)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.onTapGesture {
|
||||
selectedModuleId = module.id.uuidString
|
||||
}
|
||||
|
|
@ -194,11 +197,11 @@ struct SettingsViewModule: View {
|
|||
)
|
||||
|
||||
clipboardAlert.addAction(UIAlertAction(title: "Use Clipboard", style: .default, handler: { _ in
|
||||
self.displayModuleView(url: pasteboardString)
|
||||
displayModuleView(url: pasteboardString)
|
||||
}))
|
||||
|
||||
clipboardAlert.addAction(UIAlertAction(title: "Enter Manually", style: .cancel, handler: { _ in
|
||||
self.showManualUrlAlert()
|
||||
showManualUrlAlert()
|
||||
}))
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
|
|
@ -206,7 +209,6 @@ struct SettingsViewModule: View {
|
|||
windowScene.windows.first?.tintColor = UIColor(settings.accentColor)
|
||||
rootViewController.present(clipboardAlert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
showManualUrlAlert()
|
||||
}
|
||||
|
|
@ -226,7 +228,7 @@ struct SettingsViewModule: View {
|
|||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { _ in
|
||||
if let url = alert.textFields?.first?.text, !url.isEmpty {
|
||||
self.displayModuleView(url: url)
|
||||
displayModuleView(url: url)
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
@ -240,7 +242,7 @@ struct SettingsViewModule: View {
|
|||
func displayModuleView(url: String) {
|
||||
DispatchQueue.main.async {
|
||||
let addModuleView = ModuleAdditionSettingsView(moduleUrl: url)
|
||||
.environmentObject(self.moduleManager)
|
||||
.environmentObject(moduleManager)
|
||||
let hostingController = UIHostingController(rootView: addModuleView)
|
||||
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
|
|
|
|||
|
|
@ -8,19 +8,19 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SettingsViewPlayer: View {
|
||||
@AppStorage("externalPlayer") private var externalPlayer: String = "Sora"
|
||||
@AppStorage("externalPlayer") private var externalPlayer = "Sora"
|
||||
@AppStorage("alwaysLandscape") private var isAlwaysLandscape = false
|
||||
@AppStorage("rememberPlaySpeed") private var isRememberPlaySpeed = false
|
||||
@AppStorage("holdSpeedPlayer") private var holdSpeedPlayer: Double = 2.0
|
||||
@AppStorage("skipIncrement") private var skipIncrement: Double = 10.0
|
||||
@AppStorage("skipIncrementHold") private var skipIncrementHold: Double = 30.0
|
||||
@AppStorage("holdSpeedPlayer") private var holdSpeedPlayer = 2.0
|
||||
@AppStorage("skipIncrement") private var skipIncrement = 10.0
|
||||
@AppStorage("skipIncrementHold") private var skipIncrementHold = 30.0
|
||||
@AppStorage("holdForPauseEnabled") private var holdForPauseEnabled = false
|
||||
@AppStorage("skip85Visible") private var skip85Visible: Bool = true
|
||||
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled: Bool = false
|
||||
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
|
||||
|
||||
@AppStorage("skip85Visible") private var skip85Visible = true
|
||||
@AppStorage("doubleTapSeekEnabled") private var doubleTapSeekEnabled = false
|
||||
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible = true
|
||||
|
||||
private let mediaPlayers = ["Default", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "Sora"]
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(header: Text("Media Player"), footer: Text("Some features are limited to the Sora and Default player, such as ForceLandscape, holdSpeed and custom time skip increments.")) {
|
||||
|
|
@ -36,9 +36,11 @@ struct SettingsViewPlayer: View {
|
|||
if player == externalPlayer {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
} else if player != "Default" && player != "Sora" {
|
||||
Image(systemName: "arrow.up.forward.app")
|
||||
.foregroundColor(.secondary)
|
||||
.accessibilityLabel("External App Icon")
|
||||
} else {
|
||||
Color.clear.frame(width: 20)
|
||||
}
|
||||
|
|
@ -137,7 +139,7 @@ struct SubtitleSettingsSection: View {
|
|||
Section(header: Text("Subtitle Settings")) {
|
||||
ColorPicker("Subtitle Color", selection: Binding(
|
||||
get: {
|
||||
return Color(foregroundColor)
|
||||
Color(foregroundColor)
|
||||
},
|
||||
set: { newColor in
|
||||
let uiColor = UIColor(newColor)
|
||||
|
|
@ -162,6 +164,7 @@ struct SubtitleSettingsSection: View {
|
|||
if shadowRadius == Double(option) {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
Text("\(option)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||
|
||||
struct ProfileCell: View {
|
||||
let profile: Profile
|
||||
var isSelected: Bool = false
|
||||
var isSelected = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
|
|
@ -31,6 +31,7 @@ struct ProfileCell: View {
|
|||
if isSelected {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("Checkmark Icon")
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
|
@ -50,20 +51,21 @@ struct SettingsViewProfile: View {
|
|||
Button {
|
||||
profileStore.setCurrentProfile(profile)
|
||||
} label: {
|
||||
ProfileCell(profile: profile,
|
||||
ProfileCell(
|
||||
profile: profile,
|
||||
isSelected: profile.id == profileStore.currentProfile.id
|
||||
)
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if profileStore.profiles.count > 1 {
|
||||
Button(role: .destructive) {
|
||||
if profileStore.profiles.count > 1 {
|
||||
Button(role: .destructive) {
|
||||
profileIDToRemove = profile.id
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +75,6 @@ struct SettingsViewProfile: View {
|
|||
TextField("Avatar", text: Binding(
|
||||
get: { profileStore.currentProfile.emoji },
|
||||
set: { newValue in
|
||||
|
||||
// handle multi unicode emojis like "👨👩👧👦" or "🧙♂️"
|
||||
let emoji = String(newValue
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
|
@ -142,6 +143,7 @@ struct SettingsViewProfile: View {
|
|||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityLabel("+ Icon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@
|
|||
// Created by Francesco on 23/03/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Security
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsViewTrackers: View {
|
||||
@AppStorage("sendPushUpdates") private var isSendPushUpdates = true
|
||||
@AppStorage("sendTraktUpdates") private var isSendTraktUpdates = true
|
||||
|
||||
@State private var anilistStatus: LocalizedStringKey = "You are not logged in"
|
||||
@State private var isAnilistLoggedIn: Bool = false
|
||||
@State private var anilistUsername: String = ""
|
||||
@State private var isAnilistLoading: Bool = false
|
||||
@State private var isAnilistLoggedIn = false
|
||||
@State private var anilistUsername = ""
|
||||
@State private var isAnilistLoading = false
|
||||
@State private var profileColor: Color = .accentColor
|
||||
|
||||
@State private var traktStatus: LocalizedStringKey = "You are not logged in"
|
||||
@State private var isTraktLoggedIn: Bool = false
|
||||
@State private var traktUsername: String = ""
|
||||
@State private var isTraktLoading: Bool = false
|
||||
@State private var isTraktLoggedIn = false
|
||||
@State private var traktUsername = ""
|
||||
@State private var isTraktLoading = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
|
|
@ -32,6 +32,7 @@ struct SettingsViewTrackers: View {
|
|||
.frame(width: 80, height: 80)
|
||||
.clipShape(Rectangle())
|
||||
.cornerRadius(10)
|
||||
.accessibilityLabel("AniList Icon")
|
||||
Text("AniList.co")
|
||||
.font(.title2)
|
||||
}
|
||||
|
|
@ -76,6 +77,7 @@ struct SettingsViewTrackers: View {
|
|||
.frame(width: 80, height: 80)
|
||||
.clipShape(Rectangle())
|
||||
.cornerRadius(10)
|
||||
.accessibilityLabel("Trakt Icon")
|
||||
Text("Trakt.tv")
|
||||
.font(.title2)
|
||||
}
|
||||
|
|
@ -107,7 +109,9 @@ struct SettingsViewTrackers: View {
|
|||
}
|
||||
.modifier(SeparatorAlignmentModifier())
|
||||
|
||||
Section(footer: Text("Sora and cranci1 are not affiliated with AniList nor Trakt in any way.\n\nAlso note that progresses update may not be 100% accurate.")) {}
|
||||
Section(footer: Text("Sora and cranci1 are not affiliated with AniList nor Trakt in any way.\n\nAlso note that progresses update may not be 100% accurate.")) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Trackers")
|
||||
.onAppear {
|
||||
|
|
@ -131,33 +135,33 @@ struct SettingsViewTrackers: View {
|
|||
|
||||
func setupNotificationObservers() {
|
||||
NotificationCenter.default.addObserver(forName: AniListToken.authSuccessNotification, object: nil, queue: .main) { _ in
|
||||
self.anilistStatus = "Authentication successful!"
|
||||
self.updateAniListStatus()
|
||||
anilistStatus = "Authentication successful!"
|
||||
updateAniListStatus()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: AniListToken.authFailureNotification, object: nil, queue: .main) { notification in
|
||||
if let error = notification.userInfo?["error"] as? String {
|
||||
self.anilistStatus = "Login failed: \(error)"
|
||||
anilistStatus = "Login failed: \(error)"
|
||||
} else {
|
||||
self.anilistStatus = "Login failed with unknown error"
|
||||
anilistStatus = "Login failed with unknown error"
|
||||
}
|
||||
self.isAnilistLoggedIn = false
|
||||
self.isAnilistLoading = false
|
||||
isAnilistLoggedIn = false
|
||||
isAnilistLoading = false
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: TraktToken.authSuccessNotification, object: nil, queue: .main) { _ in
|
||||
self.traktStatus = "Authentication successful!"
|
||||
self.updateTraktStatus()
|
||||
traktStatus = "Authentication successful!"
|
||||
updateTraktStatus()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: TraktToken.authFailureNotification, object: nil, queue: .main) { notification in
|
||||
if let error = notification.userInfo?["error"] as? String {
|
||||
self.traktStatus = "Login failed: \(error)"
|
||||
traktStatus = "Login failed: \(error)"
|
||||
} else {
|
||||
self.traktStatus = "Login failed with unknown error"
|
||||
traktStatus = "Login failed with unknown error"
|
||||
}
|
||||
self.isTraktLoggedIn = false
|
||||
self.isTraktLoading = false
|
||||
isTraktLoggedIn = false
|
||||
isTraktLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,14 +200,14 @@ struct SettingsViewTrackers: View {
|
|||
|
||||
URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
DispatchQueue.main.async {
|
||||
self.isTraktLoading = false
|
||||
if let error = error {
|
||||
self.traktStatus = "Error: \(error.localizedDescription)"
|
||||
isTraktLoading = false
|
||||
if let error {
|
||||
traktStatus = "Error: \(error.localizedDescription)"
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
self.traktStatus = "No data received"
|
||||
guard let data else {
|
||||
traktStatus = "No data received"
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -211,11 +215,11 @@ struct SettingsViewTrackers: View {
|
|||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let user = json["user"] as? [String: Any],
|
||||
let username = user["username"] as? String {
|
||||
self.traktUsername = username
|
||||
self.traktStatus = "Logged in as \(username)"
|
||||
traktUsername = username
|
||||
traktStatus = "Logged in as \(username)"
|
||||
}
|
||||
} catch {
|
||||
self.traktStatus = "Failed to parse response"
|
||||
traktStatus = "Failed to parse response"
|
||||
}
|
||||
}
|
||||
}.resume()
|
||||
|
|
@ -313,12 +317,12 @@ struct SettingsViewTrackers: View {
|
|||
URLSession.shared.dataTask(with: request) { data, _, error in
|
||||
DispatchQueue.main.async {
|
||||
isAnilistLoading = false
|
||||
if let error = error {
|
||||
if let error {
|
||||
anilistStatus = "Error: \(error.localizedDescription)"
|
||||
Logger.shared.log("Error: \(error.localizedDescription)", type: "Error")
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
guard let data else {
|
||||
anilistStatus = "No data received"
|
||||
Logger.shared.log("No data received", type: "Error")
|
||||
return
|
||||
|
|
@ -330,7 +334,6 @@ struct SettingsViewTrackers: View {
|
|||
let name = viewer["name"] as? String,
|
||||
let options = viewer["options"] as? [String: Any],
|
||||
let colorName = options["profileColor"] as? String {
|
||||
|
||||
anilistUsername = name
|
||||
profileColor = colorFromName(colorName)
|
||||
anilistStatus = "Logged in as \(name)"
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ struct SettingsView: View {
|
|||
.foregroundColor(Color(hex: "7289DA"))
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.accessibilityLabel("Safari Icon")
|
||||
.foregroundColor(Color(hex: "7289DA"))
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +72,7 @@ struct SettingsView: View {
|
|||
.foregroundColor(.red)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.accessibilityLabel("Safari Icon")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +86,7 @@ struct SettingsView: View {
|
|||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.accessibilityLabel("Safari Icon")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +100,7 @@ struct SettingsView: View {
|
|||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.accessibilityLabel("Safari Icon")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +114,7 @@ struct SettingsView: View {
|
|||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Image(systemName: "safari")
|
||||
.accessibilityLabel("Safari Icon")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
|
@ -174,6 +179,7 @@ class Settings: ObservableObject {
|
|||
|
||||
private func applyColorToUIKit(_ color: Color) {
|
||||
let tempStepper = UIStepper()
|
||||
tempStepper.tintColor = UIColor(color)
|
||||
UIStepper.appearance().setDecrementImage(tempStepper.decrementImage(for: .normal), for: .normal)
|
||||
UIStepper.appearance().setIncrementImage(tempStepper.incrementImage(for: .normal), for: .normal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,13 +137,13 @@
|
|||
buildConfigurationList = 1207646F2DB6F6E1003621E9 /* Build configuration list for PBXNativeTarget "SulfurTV" */;
|
||||
buildPhases = (
|
||||
120764612DB6F6E0003621E9 /* Sources */,
|
||||
12DAC1832DBE3C1C00B31A65 /* ShellScript */,
|
||||
120764622DB6F6E0003621E9 /* Frameworks */,
|
||||
120764632DB6F6E0003621E9 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
123FEDD42DC41339001C4704 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
120764662DB6F6E0003621E9 /* SulfurTV */,
|
||||
|
|
@ -160,13 +160,13 @@
|
|||
buildConfigurationList = 133D7C782D2BE2520075467E /* Build configuration list for PBXNativeTarget "Sulfur" */;
|
||||
buildPhases = (
|
||||
133D7C662D2BE2500075467E /* Sources */,
|
||||
12DAC1822DBE3C0300B31A65 /* ShellScript */,
|
||||
133D7C672D2BE2500075467E /* Frameworks */,
|
||||
133D7C682D2BE2500075467E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
123FEDD62DC41341001C4704 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
126C42F62DB9AA97006BC27D /* Sora */,
|
||||
|
|
@ -214,6 +214,7 @@
|
|||
132E351E2D959E1D0007800E /* XCRemoteSwiftPackageReference "FFmpeg-iOS-Lame" */,
|
||||
132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */,
|
||||
13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */,
|
||||
123FEDD22DC412F0001C4704 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
|
||||
);
|
||||
productRefGroup = 133D7C6B2D2BE2500075467E /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -246,43 +247,6 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
12DAC1822DBE3C0300B31A65 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint lint --fix && swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
|
||||
};
|
||||
12DAC1832DBE3C1C00B31A65 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint lint --fix && swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
120764612DB6F6E0003621E9 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
|
@ -300,6 +264,17 @@
|
|||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
123FEDD42DC41339001C4704 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 123FEDD32DC41339001C4704 /* SwiftLintBuildToolPlugin */;
|
||||
};
|
||||
123FEDD62DC41341001C4704 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 123FEDD52DC41341001C4704 /* SwiftLintBuildToolPlugin */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
1207646D2DB6F6E1003621E9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
|
|
@ -634,6 +609,14 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
123FEDD22DC412F0001C4704 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/SimplyDanny/SwiftLintPlugins";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.59.1;
|
||||
};
|
||||
};
|
||||
132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/omaralbeik/Drops.git";
|
||||
|
|
@ -669,6 +652,16 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
123FEDD32DC41339001C4704 /* SwiftLintBuildToolPlugin */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 123FEDD22DC412F0001C4704 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
||||
productName = "plugin:SwiftLintBuildToolPlugin";
|
||||
};
|
||||
123FEDD52DC41341001C4704 /* SwiftLintBuildToolPlugin */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 123FEDD22DC412F0001C4704 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
||||
productName = "plugin:SwiftLintBuildToolPlugin";
|
||||
};
|
||||
132E351C2D959DDB0007800E /* Drops */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "e772caa8d6a8793d24bf04e3d77695cd5ac695f3605d2b657e40115caedf8863",
|
||||
"originHash" : "c4909124df3eb22bfcc539fb1f3936eb79c309605d2f34c5b02efa1fe3f48447",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "drops",
|
||||
|
|
@ -45,6 +45,15 @@
|
|||
"branch" : "master",
|
||||
"revision" : "18e4787f4dc1c26d2d581c4bc9aeae34686eeeae"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftlintplugins",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
|
||||
"state" : {
|
||||
"revision" : "8545ddf4de043e6f2051c5cf204f39ef778ebf6b",
|
||||
"version" : "0.59.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
|
|
|||
Loading…
Reference in a new issue