not to bad you know
Some checks are pending
Build and Release IPA / Build IPA (push) Waiting to run

This commit is contained in:
cranci1 2025-02-11 15:27:04 +01:00
parent 842df29bfe
commit ae75fd80ce
4 changed files with 454 additions and 40 deletions

View file

@ -30,6 +30,8 @@
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.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 */; };
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; };
@ -73,6 +75,8 @@
133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.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>"; };
@ -115,6 +119,7 @@
13103E812D589D77000F0673 /* AniList */ = {
isa = PBXGroup;
children = (
136F21B72D5B8DAC006409AC /* MediaInfo */,
13103E872D58A392000F0673 /* Struct */,
13103E822D589D7D000F0673 /* HomePage */,
);
@ -124,6 +129,7 @@
13103E822D589D7D000F0673 /* HomePage */ = {
isa = PBXGroup;
children = (
136F21BA2D5B8F17006409AC /* DetailsView */,
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
13103E852D58A328000F0673 /* AniList-Trending.swift */,
);
@ -272,6 +278,22 @@
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>";
};
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
isa = PBXGroup;
children = (
@ -430,6 +452,7 @@
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
@ -447,6 +470,7 @@
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */,
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,

View file

@ -0,0 +1,249 @@
//
// AniList-DetailsView.swift
// Sora
//
// Created by Francesco on 11/02/25.
//
import SwiftUI
import Kingfisher
struct MediaDetailItem: View {
var title: String
var value: String
var body: some View {
VStack {
Text(value)
.font(.headline)
Text(title)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.horizontal)
}
}
struct AniListDetailsView: View {
let animeID: Int
@State private var mediaInfo: [String: Any]?
@State private var isLoading: Bool = true
var body: some View {
ScrollView {
VStack(spacing: 16) {
if isLoading {
ProgressView()
.padding()
} else if let media = mediaInfo {
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: 200, height: 300)
.shimmering()
}
.resizable()
.scaledToFit()
.frame(width: 200, height: 300)
.cornerRadius(10)
.shadow(radius: 5)
}
if let titleDict = media["title"] as? [String: Any],
let userPreferred = titleDict["userPreferred"] as? String {
Text(userPreferred)
.font(.title2)
.fontWeight(.bold)
.padding(.top, 8)
}
if let trailer = media["trailer"] as? [String: Any],
let trailerID = trailer["id"] as? String,
let site = trailer["site"] as? String {
if site.lowercased() == "youtube",
let url = URL(string: "https://www.youtube.com/watch?v=\(trailerID)") {
Link("Watch Trailer on YouTube", destination: url)
.padding(.top, 4)
} else {
Text("Trailer available on \(site)")
.padding(.top, 4)
}
}
if let synopsis = media["description"] as? String {
Text(synopsis)
.padding(.horizontal)
.foregroundColor(.secondary)
.font(.system(size: 14))
}
VStack(alignment: .leading, spacing: 4) {
if let format = media["format"] as? String {
Text("Format: \(format)")
}
if let status = media["status"] as? String {
Text("Status: \(status)")
}
if let season = media["season"] as? String {
Text("Season: \(season)")
}
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 {
Text("Start Date: \(year)-\(month)-\(day)")
}
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 {
Text("End Date: \(year)-\(month)-\(day)")
}
if let country = media["countryOfOrigin"] as? String {
Text("Country: \(country)")
}
if let source = media["source"] as? String {
Text("Source: \(source)")
}
}
.font(.caption)
.foregroundColor(.secondary)
.padding(.horizontal)
.padding(.top, 4)
HStack(spacing: 24) {
if let type = media["type"] as? String {
MediaDetailItem(title: "Type", value: type)
}
if let episodes = media["episodes"] as? Int {
MediaDetailItem(title: "Episodes", value: "\(episodes)")
}
if let duration = media["duration"] as? Int {
MediaDetailItem(title: "Length", value: "\(duration) mins")
}
}
.frame(maxWidth: .infinity)
.padding(.vertical)
if let charactersDict = media["characters"] as? [String: Any],
let edges = charactersDict["edges"] as? [[String: Any]] {
VStack(alignment: .leading, spacing: 8) {
Text("Characters")
.font(.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(Array(edges.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) {
VStack {
KFImage(imageUrl)
.resizable()
.scaledToFill()
.frame(width: 120, height: 120)
.clipShape(Circle())
Text(fullName)
.font(.caption)
}
.frame(width: 140, height: 140)
}
}
}
.padding(.horizontal)
}
}
}
if let stats = media["stats"] as? [String: Any],
let scoreDistribution = stats["scoreDistribution"] as? [[String: Any]] {
VStack(alignment: .center) {
Text("Score Distribution")
.font(.headline)
HStack(alignment: .bottom, spacing: 8) {
let maxValue = scoreDistribution.compactMap { $0["amount"] as? Int }.max() ?? 1
ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { _, dataPoint in
if let score = dataPoint["score"] as? Int,
let amount = dataPoint["amount"] as? Int {
VStack {
Rectangle()
.fill(Color.accentColor)
.frame(width: 20, height: CGFloat(amount) / CGFloat(maxValue) * 100)
Text("\(score)")
.font(.caption)
}
}
}
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
}
if let relations = media["relations"] as? [String: Any],
let nodes = relations["nodes"] as? [[String: Any]] {
VStack(alignment: .leading, spacing: 8) {
Text("Correlation")
.font(.headline)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(Array(nodes.enumerated()), id: \.offset) { _, node in
if let titleDict = node["title"] as? [String: Any],
let title = titleDict["userPreferred"] as? String,
let coverImageDict = node["coverImage"] as? [String: Any],
let imageUrlStr = coverImageDict["extraLarge"] as? String,
let imageUrl = URL(string: imageUrlStr) {
VStack {
KFImage(imageUrl)
.resizable()
.scaledToFill()
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(title)
.font(.caption)
}
.frame(width: 130, height: 195)
}
}
}
.padding(.horizontal)
}
}
}
} else {
Text("Failed to load media details.")
.padding()
}
}
}
.navigationBarTitle("")
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
.onAppear {
fetchDetails()
}
}
private func fetchDetails() {
AnilistServiceMediaInfo.fetchAnimeDetails(animeID: animeID) { result in
DispatchQueue.main.async {
switch result {
case .success(let media):
self.mediaInfo = media
case .failure(let error):
print("Error: \(error)")
}
self.isLoading = false
}
}
}
}

View file

@ -0,0 +1,135 @@
//
// 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

@ -60,26 +60,29 @@ struct HomeView: View {
}
} else {
ForEach(aniListItems, id: \.id) { item in
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
.foregroundColor(.primary)
}
}
}
}
@ -104,26 +107,29 @@ struct HomeView: View {
}
} else {
ForEach(trendingItems, id: \.id) { item in
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
NavigationLink(destination: AniListDetailsView(animeID: item.id)) {
VStack {
KFImage(URL(string: item.coverImage.large))
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 130, height: 195)
.shimmering()
}
.setProcessor(RoundCornerImageProcessor(cornerRadius: 10))
.resizable()
.scaledToFill()
.frame(width: 130, height: 195)
.cornerRadius(10)
.clipped()
Text(item.title.romaji)
.font(.caption)
.frame(width: 130)
.lineLimit(1)
.multilineTextAlignment(.center)
.foregroundColor(.primary)
}
}
}
}