mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 00:22:12 +00:00
added auth exchange
This commit is contained in:
parent
63d093b47c
commit
748cc4e999
9 changed files with 183 additions and 702 deletions
|
|
@ -29,7 +29,11 @@ struct SoraApp: App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.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")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
Sora/Tracking Services/AniList/Auth/Anilist-Login.swift
Normal file
34
Sora/Tracking Services/AniList/Auth/Anilist-Login.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Sora/Tracking Services/AniList/Auth/Anilist-Token.swift
Normal file
88
Sora/Tracking Services/AniList/Auth/Anilist-Token.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
Sora/Utils/Extensions/URL.swift
Normal file
20
Sora/Utils/Extensions/URL.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,6 @@
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; };
|
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130217CB2D81C55E0011EFF5 /* DownloadView.swift */; };
|
||||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130C6BF92D53AB1F00DC1432 /* SettingsViewData.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 */; };
|
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E882D58A39A000F0673 /* AniListItem.swift */; };
|
||||||
13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; };
|
13103E8B2D58E028000F0673 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8A2D58E028000F0673 /* View.swift */; };
|
||||||
13103E8E2D58E04A000F0673 /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13103E8D2D58E04A000F0673 /* SkeletonCell.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 */; };
|
1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; };
|
||||||
1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 1359ED192D76FA7D00C13034 /* Drops */; };
|
1359ED1A2D76FA7D00C13034 /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 1359ED192D76FA7D00C13034 /* Drops */; };
|
||||||
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; };
|
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 */; };
|
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
|
||||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
|
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
|
||||||
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.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 */; };
|
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; };
|
||||||
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; };
|
13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; };
|
||||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.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 */; };
|
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; };
|
||||||
13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; };
|
13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; };
|
||||||
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
13DC0C412D2EC9BA00D0F966 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
|
@ -148,23 +146,12 @@
|
||||||
13103E812D589D77000F0673 /* AniList */ = {
|
13103E812D589D77000F0673 /* AniList */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
136F21B72D5B8DAC006409AC /* MediaInfo */,
|
13DB468A2D900919008CBC03 /* Auth */,
|
||||||
13103E872D58A392000F0673 /* Struct */,
|
13103E872D58A392000F0673 /* Struct */,
|
||||||
13103E822D589D7D000F0673 /* HomePage */,
|
|
||||||
);
|
);
|
||||||
path = AniList;
|
path = AniList;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
13103E822D589D7D000F0673 /* HomePage */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
136F21BA2D5B8F17006409AC /* DetailsView */,
|
|
||||||
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
|
|
||||||
13103E852D58A328000F0673 /* AniList-Trending.swift */,
|
|
||||||
);
|
|
||||||
path = HomePage;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
13103E872D58A392000F0673 /* Struct */ = {
|
13103E872D58A392000F0673 /* Struct */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -317,6 +304,7 @@
|
||||||
1359ED132D76F49900C13034 /* finTopView.swift */,
|
1359ED132D76F49900C13034 /* finTopView.swift */,
|
||||||
13CBEFD92D5F7D1200D011EE /* String.swift */,
|
13CBEFD92D5F7D1200D011EE /* String.swift */,
|
||||||
13103E8A2D58E028000F0673 /* View.swift */,
|
13103E8A2D58E028000F0673 /* View.swift */,
|
||||||
|
13DB468F2D900A38008CBC03 /* URL.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -348,22 +336,6 @@
|
||||||
path = LibraryView;
|
path = LibraryView;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
1384DCDF2D89BE870094797A /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -416,6 +388,15 @@
|
||||||
path = Drops;
|
path = Drops;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
13DB468A2D900919008CBC03 /* Auth */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
13DB468B2D900939008CBC03 /* Anilist-Login.swift */,
|
||||||
|
13DB468C2D90093A008CBC03 /* Anilist-Token.swift */,
|
||||||
|
);
|
||||||
|
path = Auth;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
13DB7CEA2D7DED50004371D3 /* DownloadManager */ = {
|
13DB7CEA2D7DED50004371D3 /* DownloadManager */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -542,12 +523,12 @@
|
||||||
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
|
||||||
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */,
|
1334FF4F2D786C9E007E289F /* TMDB-Trending.swift in Sources */,
|
||||||
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */,
|
13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */,
|
||||||
|
13DB46902D900A38008CBC03 /* URL.swift in Sources */,
|
||||||
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
130C6BFA2D53AB1F00DC1432 /* SettingsViewData.swift in Sources */,
|
||||||
1334FF542D787217007E289F /* TMDBRequest.swift in Sources */,
|
1334FF542D787217007E289F /* TMDBRequest.swift in Sources */,
|
||||||
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
|
||||||
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
|
||||||
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
|
||||||
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
|
|
||||||
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */,
|
||||||
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
|
||||||
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */,
|
13DB7CEC2D7DED5D004371D3 /* DownloadManager.swift in Sources */,
|
||||||
|
|
@ -555,7 +536,6 @@
|
||||||
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
|
||||||
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
13C0E5EC2D5F85F800E7F619 /* ContinueWatchingItem.swift in Sources */,
|
||||||
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
|
13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */,
|
||||||
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
|
|
||||||
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
13EA2BD62D32D97400C1EBD7 /* Double+Extension.swift in Sources */,
|
||||||
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
133D7C8F2D2BE2640075467E /* MediaInfoView.swift in Sources */,
|
||||||
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */,
|
13103E892D58A39A000F0673 /* AniListItem.swift in Sources */,
|
||||||
|
|
@ -566,18 +546,18 @@
|
||||||
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */,
|
1327FBA92D758DEA00FC6689 /* UIDevice+Model.swift in Sources */,
|
||||||
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
|
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */,
|
||||||
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
133D7C8C2D2BE2640075467E /* SearchView.swift in Sources */,
|
||||||
|
13DB468E2D90093A008CBC03 /* Anilist-Token.swift in Sources */,
|
||||||
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */,
|
1EAC7A322D888BC50083984D /* MusicProgressSlider.swift in Sources */,
|
||||||
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
|
||||||
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
|
||||||
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
|
||||||
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */,
|
|
||||||
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
|
130217CC2D81C55E0011EFF5 /* DownloadView.swift in Sources */,
|
||||||
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
|
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
|
||||||
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
|
|
||||||
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
|
||||||
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */,
|
||||||
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
|
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */,
|
||||||
1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */,
|
1334FF4D2D786C93007E289F /* TMDB-Seasonal.swift in Sources */,
|
||||||
|
13DB468D2D90093A008CBC03 /* Anilist-Login.swift in Sources */,
|
||||||
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */,
|
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */,
|
||||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
||||||
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue