diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index a945639..4073e20 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -29,7 +29,11 @@ struct SoraApp: App { } } .onOpenURL { url in - handleURL(url) + if let params = url.queryParameters, params["code"] != nil { + Self.handleRedirect(url: url) + } else { + handleURL(url) + } } } } @@ -52,4 +56,20 @@ struct SoraApp: App { Logger.shared.log("Failed to present module addition view: No window scene found", type: "Error") } } + + static func handleRedirect(url: URL) { + guard let params = url.queryParameters, + let code = params["code"] else { + Logger.shared.log("Failed to extract authorization code") + return + } + + AniListToken.exchangeAuthorizationCodeForToken(code: code) { success in + if success { + Logger.shared.log("Token exchange successful") + } else { + Logger.shared.log("Token exchange failed", type: "Error") + } + } + } } diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift new file mode 100644 index 0000000..de958b1 --- /dev/null +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Login.swift @@ -0,0 +1,34 @@ +// +// Login.swift +// Ryu +// +// Created by Francesco on 08/08/24. +// + +import UIKit + +class AniListLogin { + static let clientID = "19551" + static let redirectURI = "sora://anilist" + + static let authorizationEndpoint = "https://anilist.co/api/v2/oauth/authorize" + + static func authenticate() { + let urlString = "\(authorizationEndpoint)?client_id=\(clientID)&redirect_uri=\(redirectURI)&response_type=code" + guard let url = URL(string: urlString) else { + return + } + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:]) { success in + if success { + Logger.shared.log("Safari opened successfully", type: "Debug") + } else { + Logger.shared.log("Failed to open Safari", type: "Error") + } + } + } else { + Logger.shared.log("Cannot open URL", type: "Error") + } + } +} diff --git a/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift new file mode 100644 index 0000000..68bc232 --- /dev/null +++ b/Sora/Tracking Services/AniList/Auth/Anilist-Token.swift @@ -0,0 +1,88 @@ +// +// Token.swift +// Ryu +// +// Created by Francesco on 08/08/24. +// + +import UIKit +import Security + +class AniListToken { + static let clientID = "19551" + static let clientSecret = "fk8EgkyFbXk95TbPwLYQLaiMaNIryMpDBwJsPXoX" + static let redirectURI = "sora://anilist" + + static let tokenEndpoint = "https://anilist.co/api/v2/oauth/token" + static let serviceName = "me.cranci.sora.AniListToken" + static let accountName = "AniListAccessToken" + + static func saveTokenToKeychain(token: String) -> Bool { + let tokenData = token.data(using: .utf8)! + + let deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrAccount as String: accountName + ] + SecItemDelete(deleteQuery as CFDictionary) + + let addQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: serviceName, + kSecAttrAccount as String: accountName, + kSecValueData as String: tokenData + ] + + let status = SecItemAdd(addQuery as CFDictionary, nil) + return status == errSecSuccess + } + + static func exchangeAuthorizationCodeForToken(code: String, completion: @escaping (Bool) -> Void) { + Logger.shared.log("Exchanging authorization code for access token...") + + guard let url = URL(string: tokenEndpoint) else { + Logger.shared.log("Invalid token endpoint URL", type: "Error") + completion(false) + return + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + let bodyString = "grant_type=authorization_code&client_id=\(clientID)&client_secret=\(clientSecret)&redirect_uri=\(redirectURI)&code=\(code)" + request.httpBody = bodyString.data(using: .utf8) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + Logger.shared.log("Error: \(error.localizedDescription)", type: "Error") + completion(false) + return + } + + guard let data = data else { + Logger.shared.log("No data received", type: "Error") + completion(false) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] { + if let accessToken = json["access_token"] as? String { + let success = saveTokenToKeychain(token: accessToken) + completion(success) + } else { + Logger.shared.log("Unexpected response: \(json)", type: "Error") + completion(false) + } + } + } catch { + Logger.shared.log("Failed to parse JSON: \(error.localizedDescription)", type: "Error") + completion(false) + } + } + + task.resume() + } +} diff --git a/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift b/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift deleted file mode 100644 index 341615f..0000000 --- a/Sora/Tracking Services/AniList/HomePage/AniList-Seasonal.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// AniList-Seasonal.swift -// Sora -// -// Created by Francesco on 09/02/25. -// - -import Foundation - -class AnilistServiceSeasonalAnime { - func fetchSeasonalAnime(completion: @escaping ([AniListItem]?) -> Void) { - let currentDate = Date() - let calendar = Calendar.current - let year = calendar.component(.year, from: currentDate) - let month = calendar.component(.month, from: currentDate) - - let season: String - switch month { - case 1...3: - season = "WINTER" - case 4...6: - season = "SPRING" - case 7...9: - season = "SUMMER" - default: - season = "FALL" - } - - let query = """ - query { - Page(page: 1, perPage: 100) { - media(season: \(season), seasonYear: \(year), type: ANIME, isAdult: false) { - id - title { - romaji - english - native - } - coverImage { - large - } - } - } - } - """ - - guard let url = URL(string: "https://graphql.anilist.co") else { - print("Invalid URL") - completion(nil) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - let parameters: [String: Any] = ["query": query] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch { - print("Error encoding JSON: \(error.localizedDescription)") - completion(nil) - return - } - - let task = URLSession.custom.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { - if let error = error { - print("Error fetching seasonal anime: \(error.localizedDescription)") - completion(nil) - return - } - - guard let data = data else { - print("No data returned") - completion(nil) - return - } - - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let dataObject = json["data"] as? [String: Any], - let page = dataObject["Page"] as? [String: Any], - let media = page["media"] as? [[String: Any]] { - - let seasonalAnime: [AniListItem] = media.compactMap { item -> AniListItem? in - guard let id = item["id"] as? Int, - let titleData = item["title"] as? [String: Any], - let romaji = titleData["romaji"] as? String, - let english = titleData["english"] as? String?, - let native = titleData["native"] as? String?, - let coverImageData = item["coverImage"] as? [String: Any], - let largeImageUrl = coverImageData["large"] as? String, - URL(string: largeImageUrl) != nil else { - return nil - } - - return AniListItem( - id: id, - title: AniListTitle(romaji: romaji, english: english, native: native), - coverImage: AniListCoverImage(large: largeImageUrl) - ) - } - completion(seasonalAnime) - } else { - print("Error parsing JSON or missing expected fields") - completion(nil) - } - } catch { - print("Error decoding JSON: \(error.localizedDescription)") - completion(nil) - } - } - } - task.resume() - } -} diff --git a/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift b/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift deleted file mode 100644 index b02e970..0000000 --- a/Sora/Tracking Services/AniList/HomePage/AniList-Trending.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// AniList-Trending.swift -// Sora -// -// Created by Francesco on 09/02/25. -// - -import Foundation - -class AnilistServiceTrendingAnime { - func fetchTrendingAnime(completion: @escaping ([AniListItem]?) -> Void) { - let query = """ - query { - Page(page: 1, perPage: 100) { - media(sort: TRENDING_DESC, type: ANIME, isAdult: false) { - id - title { - romaji - english - native - } - coverImage { - large - } - } - } - } - """ - guard let url = URL(string: "https://graphql.anilist.co") else { - print("Invalid URL") - completion(nil) - return - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - let parameters: [String: Any] = ["query": query] - do { - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) - } catch { - print("Error encoding JSON: \(error.localizedDescription)") - completion(nil) - return - } - - let task = URLSession.custom.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { - if let error = error { - print("Error fetching trending anime: \(error.localizedDescription)") - completion(nil) - return - } - guard let data = data else { - print("No data returned") - completion(nil) - return - } - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let dataObject = json["data"] as? [String: Any], - let page = dataObject["Page"] as? [String: Any], - let media = page["media"] as? [[String: Any]] { - - let trendingAnime: [AniListItem] = media.compactMap { item in - guard let id = item["id"] as? Int, - let titleData = item["title"] as? [String: Any], - let romaji = titleData["romaji"] as? String, - let coverImageData = item["coverImage"] as? [String: Any], - let largeImageUrl = coverImageData["large"] as? String else { - return nil - } - - return AniListItem( - id: id, - title: AniListTitle(romaji: romaji, english: titleData["english"] as? String, native: titleData["native"] as? String), - coverImage: AniListCoverImage(large: largeImageUrl) - ) - } - completion(trendingAnime) - } else { - print("Error parsing JSON or missing expected fields") - completion(nil) - } - } catch { - print("Error decoding JSON: \(error.localizedDescription)") - completion(nil) - } - } - } - task.resume() - } -} diff --git a/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift b/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift deleted file mode 100644 index 4f7a9d4..0000000 --- a/Sora/Tracking Services/AniList/HomePage/DetailsView/AniList-DetailsView.swift +++ /dev/null @@ -1,316 +0,0 @@ -// -// AniList-DetailsView.swift -// Sora -// -// Created by Francesco on 11/02/25. -// - -import SwiftUI -import Kingfisher - -struct AniListDetailsView: View { - let animeID: Int - @StateObject private var viewModel: AniListDetailsViewModel - - init(animeID: Int) { - self.animeID = animeID - _viewModel = StateObject(wrappedValue: AniListDetailsViewModel(animeID: animeID)) - } - - var body: some View { - ScrollView { - VStack(spacing: 16) { - if viewModel.isLoading { - ProgressView() - .padding() - } else if let media = viewModel.mediaInfo { - MediaHeaderView(media: media) - Divider() - MediaDetailsScrollView(media: media) - Divider() - SynopsisView(synopsis: media["description"] as? String) - Divider() - CharactersView(characters: media["characters"] as? [String: Any]) - Divider() - ScoreDistributionView(stats: media["stats"] as? [String: Any]) - } else { - Text("Failed to load media details.") - .padding() - } - } - } - .navigationBarTitle("", displayMode: .inline) - .navigationViewStyle(StackNavigationViewStyle()) - .onAppear { - viewModel.fetchDetails() - } - } -} - -class AniListDetailsViewModel: ObservableObject { - @Published var mediaInfo: [String: AnyHashable]? - @Published var isLoading: Bool = true - - let animeID: Int - - init(animeID: Int) { - self.animeID = animeID - } - - func fetchDetails() { - AnilistServiceMediaInfo.fetchAnimeDetails(animeID: animeID) { result in - DispatchQueue.main.async { - switch result { - case .success(let media): - var convertedMedia: [String: AnyHashable] = [:] - for (key, value) in media { - if let value = value as? AnyHashable { - convertedMedia[key] = value - } - } - self.mediaInfo = convertedMedia - case .failure(let error): - print("Error: \(error)") - } - self.isLoading = false - } - } - } -} - -struct MediaHeaderView: View { - let media: [String: Any] - - var body: some View { - HStack(alignment: .top, spacing: 16) { - if let coverDict = media["coverImage"] as? [String: Any], - let posterURLString = coverDict["extraLarge"] as? String, - let posterURL = URL(string: posterURLString) { - KFImage(posterURL) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) - .shimmering() - } - .resizable() - .aspectRatio(2/3, contentMode: .fill) - .cornerRadius(10) - .frame(width: 150, height: 225) - } - - VStack(alignment: .leading) { - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["english"] as? String { - Text(userPreferred) - .font(.system(size: 17)) - .fontWeight(.bold) - .onLongPressGesture { - UIPasteboard.general.string = userPreferred - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) - } - } - - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["romaji"] as? String { - Text(userPreferred) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - if let titleDict = media["title"] as? [String: Any], - let userPreferred = titleDict["native"] as? String { - Text(userPreferred) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - } - - Spacer() - } - .padding() - } -} - -struct MediaDetailsScrollView: View { - let media: [String: Any] - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 4) { - if let type = media["type"] as? String { - MediaDetailItem(title: "Type", value: type) - Divider() - } - if let episodes = media["episodes"] as? Int { - MediaDetailItem(title: "Episodes", value: "\(episodes)") - Divider() - } - if let duration = media["duration"] as? Int { - MediaDetailItem(title: "Length", value: "\(duration) mins") - Divider() - } - if let format = media["format"] as? String { - MediaDetailItem(title: "Format", value: format) - Divider() - } - if let status = media["status"] as? String { - MediaDetailItem(title: "Status", value: status) - Divider() - } - if let season = media["season"] as? String { - MediaDetailItem(title: "Season", value: season) - Divider() - } - if let startDate = media["startDate"] as? [String: Any], - let year = startDate["year"] as? Int, - let month = startDate["month"] as? Int, - let day = startDate["day"] as? Int { - MediaDetailItem(title: "Start Date", value: "\(year)-\(month)-\(day)") - Divider() - } - if let endDate = media["endDate"] as? [String: Any], - let year = endDate["year"] as? Int, - let month = endDate["month"] as? Int, - let day = endDate["day"] as? Int { - MediaDetailItem(title: "End Date", value: "\(year)-\(month)-\(day)") - } - } - } - } -} - -struct SynopsisView: View { - let synopsis: String? - - var body: some View { - if let synopsis = synopsis { - Text(synopsis.strippedHTML) - .padding(.horizontal) - .foregroundColor(.secondary) - .font(.system(size: 14)) - } else { - EmptyView() - } - } -} - -struct CharactersView: View { - let characters: [String: Any]? - - var body: some View { - if let charactersDict = characters, - let edges = charactersDict["edges"] as? [[String: Any]] { - VStack(alignment: .leading, spacing: 8) { - Text("Characters") - .font(.headline) - .padding(.horizontal) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - ForEach(Array(edges.prefix(15).enumerated()), id: \.offset) { _, edge in - if let node = edge["node"] as? [String: Any], - let nameDict = node["name"] as? [String: Any], - let fullName = nameDict["full"] as? String, - let imageDict = node["image"] as? [String: Any], - let imageUrlStr = imageDict["large"] as? String, - let imageUrl = URL(string: imageUrlStr) { - CharacterItemView(imageUrl: imageUrl, name: fullName) - } - } - } - .padding(.horizontal) - } - } - } else { - EmptyView() - } - } -} - -struct CharacterItemView: View { - let imageUrl: URL - let name: String - - var body: some View { - VStack { - KFImage(imageUrl) - .placeholder { - Circle() - .fill(Color.gray.opacity(0.3)) - .frame(width: 90, height: 90) - .shimmering() - } - .resizable() - .scaledToFill() - .frame(width: 90, height: 90) - .clipShape(Circle()) - Text(name) - .font(.caption) - .lineLimit(1) - } - .frame(width: 105, height: 110) - } -} - -struct ScoreDistributionView: View { - let stats: [String: Any]? - - @State private var barHeights: [CGFloat] = [] - - var body: some View { - if let stats = stats, - let scoreDistribution = stats["scoreDistribution"] as? [[String: AnyHashable]] { - - let maxValue: Int = scoreDistribution.compactMap { $0["amount"] as? Int }.max() ?? 1 - - let calculatedHeights = scoreDistribution.map { dataPoint -> CGFloat in - guard let amount = dataPoint["amount"] as? Int else { return 0 } - return CGFloat(amount) / CGFloat(maxValue) * 100 - } - - VStack { - Text("Score Distribution") - .font(.headline) - HStack(alignment: .bottom) { - ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { index, dataPoint in - if let score = dataPoint["score"] as? Int { - VStack { - Rectangle() - .fill(Color.accentColor) - .frame(width: 20, height: calculatedHeights[index]) - Text("\(score)") - .font(.caption) - } - } - } - } - } - .frame(maxWidth: .infinity) - .padding(.horizontal) - .onAppear { - barHeights = calculatedHeights - } - .onChange(of: scoreDistribution) { _ in - barHeights = calculatedHeights - } - } else { - EmptyView() - } - } -} - -struct MediaDetailItem: View { - var title: String - var value: String - - var body: some View { - VStack { - Text(value) - .font(.system(size: 17)) - Text(title) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - .padding(.horizontal) - } -} diff --git a/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift b/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift deleted file mode 100644 index 92b34b5..0000000 --- a/Sora/Tracking Services/AniList/MediaInfo/AniList-MediaInfo.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// AniList-MediaInfo.swift -// Sora -// -// Created by Francesco on 11/02/25. -// - -import Foundation - -class AnilistServiceMediaInfo { - static func fetchAnimeDetails(animeID: Int, completion: @escaping (Result<[String: Any], Error>) -> Void) { - let query = """ - query { - Media(id: \(animeID), type: ANIME) { - id - idMal - title { - romaji - english - native - userPreferred - } - type - format - status - description - startDate { - year - month - day - } - endDate { - year - month - day - } - season - episodes - duration - countryOfOrigin - isLicensed - source - hashtag - trailer { - id - site - } - updatedAt - coverImage { - extraLarge - } - bannerImage - genres - popularity - tags { - id - name - } - relations { - nodes { - id - coverImage { extraLarge } - title { userPreferred }, - mediaListEntry { status } - } - } - characters { - edges { - node { - name { - full - } - image { - large - } - } - role - voiceActors { - name { - first - last - native - } - } - } - } - siteUrl - stats { - scoreDistribution { - score - amount - } - } - airingSchedule(notYetAired: true) { - nodes { - airingAt - episode - } - } - } - } - """ - - let apiUrl = URL(string: "https://graphql.anilist.co")! - - var request = URLRequest(url: apiUrl) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try? JSONSerialization.data(withJSONObject: ["query": query], options: []) - - URLSession.custom.dataTask(with: request) { data, response, error in - if let error = error { - completion(.failure(error)) - return - } - - guard let data = data else { - completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) - return - } - - do { - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let data = json["data"] as? [String: Any], - let media = data["Media"] as? [String: Any] { - completion(.success(media)) - } else { - completion(.failure(NSError(domain: "AnimeService", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]))) - } - } catch { - completion(.failure(error)) - } - }.resume() - } -} diff --git a/Sora/Utils/Extensions/URL.swift b/Sora/Utils/Extensions/URL.swift new file mode 100644 index 0000000..3dc88f2 --- /dev/null +++ b/Sora/Utils/Extensions/URL.swift @@ -0,0 +1,20 @@ +// +// URL.swift +// Sulfur +// +// Created by Francesco on 23/03/25. +// + +import Foundation + +extension URL { + var queryParameters: [String: String]? { + guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true), + let queryItems = components.queryItems else { return nil } + var params = [String: String]() + for queryItem in queryItems { + params[queryItem.name] = queryItem.value + } + return params + } +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 27a0a15..fe85c2f 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -9,8 +9,6 @@ /* Begin PBXBuildFile section */ 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; }; 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */; }; - 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */; }; - 13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E852D58A328000F0673 /* AniList-Trending.swift */; }; 13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; }; 13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; }; 13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */; }; @@ -38,8 +36,6 @@ 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; 1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 1359ED192D76FA7D00C13034 /* Drops */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; - 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */; }; - 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */; }; 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; @@ -52,6 +48,9 @@ 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; }; 13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; }; 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; }; + 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468B2D900939008CBC03 /* Anilist-Login.swift */; }; + 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */; }; + 13DB46902D900A38008CBC03 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB468F2D900A38008CBC03 /* URL.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; }; @@ -68,8 +67,6 @@ 130217CB2D81C55E0011EFF5 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = ""; }; 130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = ""; }; - 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = ""; }; - 13103E852D58A328000F0673 /* AniList-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Trending.swift"; sourceTree = ""; }; 13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = ""; }; 13103E8A2D58E028000F0673 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 13103E8D2D58E04A000F0673 /* SkeletonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCell.swift; sourceTree = ""; }; @@ -96,8 +93,6 @@ 133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = ""; }; 1359ED132D76F49900C13034 /* finTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = finTopView.swift; sourceTree = ""; }; 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = ""; }; - 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-MediaInfo.swift"; sourceTree = ""; }; - 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-DetailsView.swift"; sourceTree = ""; }; 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; 139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = ""; }; @@ -110,6 +105,9 @@ 13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = ""; }; 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = ""; }; + 13DB468B2D900939008CBC03 /* Anilist-Login.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Login.swift"; sourceTree = ""; }; + 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Token.swift"; sourceTree = ""; }; + 13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; 13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -148,23 +146,12 @@ 13103E812D589D77000F0673 /* AniList */ = { isa = PBXGroup; children = ( - 136F21B72D5B8DAC006409AC /* MediaInfo */, + 13DB468A2D900919008CBC03 /* Auth */, 13103E872D58A392000F0673 /* Struct */, - 13103E822D589D7D000F0673 /* HomePage */, ); path = AniList; sourceTree = ""; }; - 13103E822D589D7D000F0673 /* HomePage */ = { - isa = PBXGroup; - children = ( - 136F21BA2D5B8F17006409AC /* DetailsView */, - 13103E832D589D8B000F0673 /* AniList-Seasonal.swift */, - 13103E852D58A328000F0673 /* AniList-Trending.swift */, - ); - path = HomePage; - sourceTree = ""; - }; 13103E872D58A392000F0673 /* Struct */ = { isa = PBXGroup; children = ( @@ -317,6 +304,7 @@ 1359ED132D76F49900C13034 /* finTopView.swift */, 13CBEFD92D5F7D1200D011EE /* String.swift */, 13103E8A2D58E028000F0673 /* View.swift */, + 13DB468F2D900A38008CBC03 /* URL.swift */, ); path = Extensions; sourceTree = ""; @@ -348,22 +336,6 @@ path = LibraryView; sourceTree = ""; }; - 136F21B72D5B8DAC006409AC /* MediaInfo */ = { - isa = PBXGroup; - children = ( - 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */, - ); - path = MediaInfo; - sourceTree = ""; - }; - 136F21BA2D5B8F17006409AC /* DetailsView */ = { - isa = PBXGroup; - children = ( - 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */, - ); - path = DetailsView; - sourceTree = ""; - }; 1384DCDF2D89BE870094797A /* Helpers */ = { isa = PBXGroup; children = ( @@ -416,6 +388,15 @@ path = Drops; sourceTree = ""; }; + 13DB468A2D900919008CBC03 /* Auth */ = { + isa = PBXGroup; + children = ( + 13DB468B2D900939008CBC03 /* Anilist-Login.swift */, + 13DB468C2D90093A008CBC03 /* Anilist-Token.swift */, + ); + path = Auth; + sourceTree = ""; + }; 13DB7CEA2D7DED50004371D3 /* DownloadManager */ = { isa = PBXGroup; children = ( @@ -542,12 +523,12 @@ 133D7C902D2BE2640075467E /* SettingsView.swift in Sources */, 1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */, 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */, + 13DB46902D900A38008CBC03 /* URL.swift in Sources */, 130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */, 1334FF542D787217007E289F /* TMDBRequest.swift in Sources */, 1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */, 13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */, 133D7C932D2BE2640075467E /* Modules.swift in Sources */, - 136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */, 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */, 133D7C702D2BE2500075467E /* ContentView.swift in Sources */, 13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */, @@ -555,7 +536,6 @@ 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */, 13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */, 13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */, - 13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */, 13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */, 133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */, 13103E892D58A39A000F0673 /* AniListItem.swift in Sources */, @@ -566,18 +546,18 @@ 1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */, 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */, 133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */, + 13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */, 1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */, 133D7C942D2BE2640075467E /* JSController.swift in Sources */, 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, - 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */, 130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, - 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */, 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */, 1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */, + 13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */, 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */, 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,