diff --git a/.swiftlint.yml b/.swiftlint.yml index 9a8a006..97d506d 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -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: diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 27c219e..5cbd1c2 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -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" : { diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 722b897..cf17674 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -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") diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift index 8019520..a3e9d47 100644 --- a/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift @@ -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) diff --git a/Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift b/Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift index 6ee21b6..563b8c7 100644 --- a/Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift +++ b/Sora/Tracking Services/AniList/Mutations/AniListPushUpdates.swift @@ -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 } } diff --git a/Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift b/Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift index 6ab08e2..dc24d6e 100644 --- a/Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift +++ b/Sora/Tracking Services/Trakt/Auth/Trakt-Token.swift @@ -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) diff --git a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift index 48eed3f..dbf96e7 100644 --- a/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift +++ b/Sora/Tracking Services/Trakt/Mutations/TraktPushUpdates.swift @@ -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 } diff --git a/Sora/Utils/Analytics/Analytics.swift b/Sora/Utils/Analytics/Analytics.swift index 6b5d4a2..32b3ad3 100644 --- a/Sora/Utils/Analytics/Analytics.swift +++ b/Sora/Utils/Analytics/Analytics.swift @@ -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 diff --git a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift index 9d143d9..32b5a37 100644 --- a/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift +++ b/Sora/Utils/ContinueWatching/ContinueWatchingManager.swift @@ -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) } diff --git a/Sora/Utils/ContinueWatching/DownloadManager.swift b/Sora/Utils/ContinueWatching/DownloadManager.swift index 9c3d90d..611c511 100644 --- a/Sora/Utils/ContinueWatching/DownloadManager.swift +++ b/Sora/Utils/ContinueWatching/DownloadManager.swift @@ -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 { diff --git a/Sora/Utils/Drops/DropManager.swift b/Sora/Utils/Drops/DropManager.swift index 11b38c8..9754c01 100644 --- a/Sora/Utils/Drops/DropManager.swift +++ b/Sora/Utils/Drops/DropManager.swift @@ -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 diff --git a/Sora/Utils/Extensions/Color.swift b/Sora/Utils/Extensions/Color.swift index 92c63ae..cb9d9e6 100644 --- a/Sora/Utils/Extensions/Color.swift +++ b/Sora/Utils/Extensions/Color.swift @@ -8,7 +8,6 @@ import SwiftUI extension Color { - /// Intitialize SwiftUI Color via HEX String /// /// - Parameters: diff --git a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift index a1c7446..b6a5c5d 100644 --- a/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift +++ b/Sora/Utils/Extensions/JavaScriptCore+Extensions.swift @@ -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"]) diff --git a/Sora/Utils/Extensions/String.swift b/Sora/Utils/Extensions/String.swift index 0937e50..e688656 100644 --- a/Sora/Utils/Extensions/String.swift +++ b/Sora/Utils/Extensions/String.swift @@ -19,6 +19,6 @@ extension String { } var trimmed: String { - return self.trimmingCharacters(in: .whitespacesAndNewlines) + self.trimmingCharacters(in: .whitespacesAndNewlines) } } diff --git a/Sora/Utils/Extensions/UIDevice+Model.swift b/Sora/Utils/Extensions/UIDevice+Model.swift index 05ae506..c28471d 100644 --- a/Sora/Utils/Extensions/UIDevice+Model.swift +++ b/Sora/Utils/Extensions/UIDevice+Model.swift @@ -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 diff --git a/Sora/Utils/Extensions/UIKit.swift b/Sora/Utils/Extensions/UIKit.swift index 2bc5d76..0ad142d 100644 --- a/Sora/Utils/Extensions/UIKit.swift +++ b/Sora/Utils/Extensions/UIKit.swift @@ -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 { } diff --git a/Sora/Utils/Extensions/URLSession.swift b/Sora/Utils/Extensions/URLSession.swift index 58e9100..ec70165 100644 --- a/Sora/Utils/Extensions/URLSession.swift +++ b/Sora/Utils/Extensions/URLSession.swift @@ -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", diff --git a/Sora/Utils/Extensions/View.swift b/Sora/Utils/Extensions/View.swift index fa83775..64e40c8 100644 --- a/Sora/Utils/Extensions/View.swift +++ b/Sora/Utils/Extensions/View.swift @@ -37,4 +37,3 @@ struct HideToolbarModifier: ViewModifier { } } } - diff --git a/Sora/Utils/Extensions/finTopView.swift b/Sora/Utils/Extensions/finTopView.swift index 9ee5c0c..b656208 100644 --- a/Sora/Utils/Extensions/finTopView.swift +++ b/Sora/Utils/Extensions/finTopView.swift @@ -7,7 +7,7 @@ import UIKit -class findTopViewController { +class FindTopViewController { static func findViewController(_ viewController: UIViewController) -> UIViewController { if let presented = viewController.presentedViewController { return findViewController(presented) diff --git a/Sora/Utils/JSLoader/JSController-Details.swift b/Sora/Utils/JSLoader/JSController-Details.swift index 6240209..831f9e6 100644 --- a/Sora/Utils/JSLoader/JSController-Details.swift +++ b/Sora/Utils/JSLoader/JSController-Details.swift @@ -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 diff --git a/Sora/Utils/JSLoader/JSController-Explore.swift b/Sora/Utils/JSLoader/JSController-Explore.swift index d4eeab5..b96d956 100644 --- a/Sora/Utils/JSLoader/JSController-Explore.swift +++ b/Sora/Utils/JSLoader/JSController-Explore.swift @@ -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 diff --git a/Sora/Utils/JSLoader/JSController-Search.swift b/Sora/Utils/JSLoader/JSController-Search.swift index 2538c73..c3192bc 100644 --- a/Sora/Utils/JSLoader/JSController-Search.swift +++ b/Sora/Utils/JSLoader/JSController-Search.swift @@ -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 { diff --git a/Sora/Utils/JSLoader/JSController-Streams.swift b/Sora/Utils/JSLoader/JSController-Streams.swift index 837b561..85182a0 100644 --- a/Sora/Utils/JSLoader/JSController-Streams.swift +++ b/Sora/Utils/JSLoader/JSController-Streams.swift @@ -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 diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift index d05841e..77d8fd9 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/Double+Extension.swift @@ -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] = [] @Published var outroSegments: [ClosedRange] = [] } @@ -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 diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift index 3cee987..eb40ad9 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/MusicProgressSlider.swift @@ -25,7 +25,7 @@ struct MusicProgressSlider: 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: 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 } } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift index 7ad4c61..a2aaf83 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Components/VolumeSlider.swift @@ -15,12 +15,12 @@ struct VolumeSlider: 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: 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: View { if !newValue { value = sliderValueInRange() } - onEditingChanged(newValue) + onEditingChanged?(newValue) } .onAppear { localRealProgress = progress(for: value) diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 9cef1ae..69e629b 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -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) } diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift index 0f7dd71..7bd9a90 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/SubtitleSettingsManager.swift @@ -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 { diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift index efbf183..3f67cb8 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/Helpers/VTTSubtitlesLoader.swift @@ -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 } diff --git a/Sora/Utils/MediaPlayer/NormalPlayer.swift b/Sora/Utils/MediaPlayer/NormalPlayer.swift index cb937bf..3258aff 100644 --- a/Sora/Utils/MediaPlayer/NormalPlayer.swift +++ b/Sora/Utils/MediaPlayer/NormalPlayer.swift @@ -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 diff --git a/Sora/Utils/Modules/CommunityLib.swift b/Sora/Utils/Modules/CommunityLib.swift index 818f088..8a3c483 100644 --- a/Sora/Utils/Modules/CommunityLib.swift +++ b/Sora/Utils/Modules/CommunityLib.swift @@ -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, diff --git a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift index 0984e58..d726d9f 100644 --- a/Sora/Utils/Modules/ModuleAdditionSettingsView.swift +++ b/Sora/Utils/Modules/ModuleAdditionSettingsView.swift @@ -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 { diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index b76cd70..65b97f2 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -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 = [] } } } diff --git a/Sora/Utils/Modules/Modules.swift b/Sora/Utils/Modules/Modules.swift index 6f59dc5..28fe10a 100644 --- a/Sora/Utils/Modules/Modules.swift +++ b/Sora/Utils/Modules/Modules.swift @@ -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 } } diff --git a/Sora/Utils/ProfileStore/Profile.swift b/Sora/Utils/ProfileStore/Profile.swift index 4043b48..aa7f60b 100644 --- a/Sora/Utils/ProfileStore/Profile.swift +++ b/Sora/Utils/ProfileStore/Profile.swift @@ -8,7 +8,7 @@ import Foundation struct Profile: Identifiable, Equatable, Codable { - var id: UUID = UUID() + var id = UUID() var name: String var emoji: String } diff --git a/Sora/Utils/ProfileStore/ProfileStore.swift b/Sora/Utils/ProfileStore/ProfileStore.swift index 29ec55f..35b0294 100644 --- a/Sora/Utils/ProfileStore/ProfileStore.swift +++ b/Sora/Utils/ProfileStore/ProfileStore.swift @@ -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 } diff --git a/Sora/Utils/SkeletonCells/Shimmer.swift b/Sora/Utils/SkeletonCells/Shimmer.swift index 7d506ab..4935391 100644 --- a/Sora/Utils/SkeletonCells/Shimmer.swift +++ b/Sora/Utils/SkeletonCells/Shimmer.swift @@ -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 } } } diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index da972f9..be76d08 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -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() } diff --git a/Sora/Views/DownloadView/DownloadView.swift b/Sora/Views/DownloadView/DownloadView.swift index d98a28a..84ec9d4 100644 --- a/Sora/Views/DownloadView/DownloadView.swift +++ b/Sora/Views/DownloadView/DownloadView.swift @@ -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)) diff --git a/Sora/Views/ExploreView/ExploreView.swift b/Sora/Views/ExploreView/ExploreView.swift index 8da5d61..311c458 100644 --- a/Sora/Views/ExploreView/ExploreView.swift +++ b/Sora/Views/ExploreView/ExploreView.swift @@ -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.. 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] ?? [] } } diff --git a/Sora/Views/LibraryView/LibraryManager.swift b/Sora/Views/LibraryView/LibraryManager.swift index 21697ca..35c61f7 100644 --- a/Sora/Views/LibraryView/LibraryManager.swift +++ b/Sora/Views/LibraryView/LibraryManager.swift @@ -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() } diff --git a/Sora/Views/LibraryView/LibraryView.swift b/Sora/Views/LibraryView/LibraryView.swift index eac74ae..c257d88 100644 --- a/Sora/Views/LibraryView/LibraryView.swift +++ b/Sora/Views/LibraryView/LibraryView.swift @@ -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() diff --git a/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift index 4d00f0e..ec4ce4e 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/CircularProgressBar.swift @@ -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)) diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index 7ba449b..08e355c 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -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() } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index f522999..d7df916 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -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) } } } diff --git a/Sora/Views/SearchView/SearchView.swift b/Sora/Views/SearchView/SearchView.swift index 49e545e..0b02f1a 100644 --- a/Sora/Views/SearchView/SearchView.swift +++ b/Sora/Views/SearchView/SearchView.swift @@ -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.. 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") } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift index a735f1e..06c98af 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewAlternateAppIconPicker.swift @@ -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)") } }) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift index 86cea84..68dbeb5 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewData.swift @@ -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) diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift index ac6baee..64f11a7 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewGeneral.swift @@ -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) } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift index 4738249..5a01367 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLogger.swift @@ -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") } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift index 32424aa..5fb0e6e 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewLoggerFilter.swift @@ -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() { diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift index dee6693..95d8d1a 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewModule.swift @@ -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? @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, diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift index 1d47e19..6d25f0a 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewPlayer.swift @@ -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)") } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift index 8a3d188..d9fdb81 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewProfile.swift @@ -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") } } } diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift index 0490792..260e6ab 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewTrackers.swift @@ -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)" diff --git a/Sora/Views/SettingsView/SettingsView.swift b/Sora/Views/SettingsView/SettingsView.swift index f54a7e6..73ef05c 100644 --- a/Sora/Views/SettingsView/SettingsView.swift +++ b/Sora/Views/SettingsView/SettingsView.swift @@ -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) } diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index e9f334d..5c168ca 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -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" */; diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d147912..55034eb 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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