added auth exchange

This commit is contained in:
cranci1 2025-03-23 10:25:53 +01:00
parent 63d093b47c
commit 748cc4e999
9 changed files with 183 additions and 702 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = "<group>"; };
130C6BF82D53A4C200DC1432 /* Sora.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Sora.entitlements; sourceTree = "<group>"; };
130C6BF92D53AB1F00DC1432 /* SettingsViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewData.swift; sourceTree = "<group>"; };
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Seasonal.swift"; sourceTree = "<group>"; };
13103E852D58A328000F0673 /* AniList-Trending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-Trending.swift"; sourceTree = "<group>"; };
13103E882D58A39A000F0673 /* AniListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AniListItem.swift; sourceTree = "<group>"; };
13103E8A2D58E028000F0673 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
13103E8D2D58E04A000F0673 /* SkeletonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCell.swift; sourceTree = "<group>"; };
@ -96,8 +93,6 @@
133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = "<group>"; };
1359ED132D76F49900C13034 /* finTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = finTopView.swift; sourceTree = "<group>"; };
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = "<group>"; };
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-MediaInfo.swift"; sourceTree = "<group>"; };
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-DetailsView.swift"; sourceTree = "<group>"; };
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = "<group>"; };
@ -110,6 +105,9 @@
13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = "<group>"; };
13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = "<group>"; };
13DB468B2D900939008CBC03 /* Anilist-Login.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Login.swift"; sourceTree = "<group>"; };
13DB468C2D90093A008CBC03 /* Anilist-Token.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Anilist-Token.swift"; sourceTree = "<group>"; };
13DB468F2D900A38008CBC03 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = "<group>"; };
13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = "<group>"; };
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@ -148,23 +146,12 @@
13103E812D589D77000F0673 /* AniList */ = {
isa = PBXGroup;
children = (
136F21B72D5B8DAC006409AC /* MediaInfo */,
13DB468A2D900919008CBC03 /* Auth */,
13103E872D58A392000F0673 /* Struct */,
13103E822D589D7D000F0673 /* HomePage */,
);
path = AniList;
sourceTree = "<group>";
};
13103E822D589D7D000F0673 /* HomePage */ = {
isa = PBXGroup;
children = (
136F21BA2D5B8F17006409AC /* DetailsView */,
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
13103E852D58A328000F0673 /* AniList-Trending.swift */,
);
path = HomePage;
sourceTree = "<group>";
};
13103E872D58A392000F0673 /* Struct */ = {
isa = PBXGroup;
children = (
@ -317,6 +304,7 @@
1359ED132D76F49900C13034 /* finTopView.swift */,
13CBEFD92D5F7D1200D011EE /* String.swift */,
13103E8A2D58E028000F0673 /* View.swift */,
13DB468F2D900A38008CBC03 /* URL.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -348,22 +336,6 @@
path = LibraryView;
sourceTree = "<group>";
};
136F21B72D5B8DAC006409AC /* MediaInfo */ = {
isa = PBXGroup;
children = (
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */,
);
path = MediaInfo;
sourceTree = "<group>";
};
136F21BA2D5B8F17006409AC /* DetailsView */ = {
isa = PBXGroup;
children = (
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */,
);
path = DetailsView;
sourceTree = "<group>";
};
1384DCDF2D89BE870094797A /* Helpers */ = {
isa = PBXGroup;
children = (
@ -416,6 +388,15 @@
path = Drops;
sourceTree = "<group>";
};
13DB468A2D900919008CBC03 /* Auth */ = {
isa = PBXGroup;
children = (
13DB468B2D900939008CBC03 /* Anilist-Login.swift */,
13DB468C2D90093A008CBC03 /* Anilist-Token.swift */,
);
path = Auth;
sourceTree = "<group>";
};
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 */,