Skip to content

Commit ae75fd8

Browse files
committed
not to bad you know
1 parent 842df29 commit ae75fd8

4 files changed

Lines changed: 454 additions & 40 deletions

File tree

Sora.xcodeproj/project.pbxproj

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
133D7C972D2BE2AF0075467E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 133D7C962D2BE2AF0075467E /* Kingfisher */; };
3131
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; };
3232
135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; };
33+
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */; };
34+
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */; };
3335
138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; };
3436
138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; };
3537
139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; };
@@ -73,6 +75,8 @@
7375
133D7C8B2D2BE2640075467E /* JSController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSController.swift; sourceTree = "<group>"; };
7476
133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = "<group>"; };
7577
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = "<group>"; };
78+
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-MediaInfo.swift"; sourceTree = "<group>"; };
79+
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AniList-DetailsView.swift"; sourceTree = "<group>"; };
7680
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
7781
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
7882
139935652D468C450065CEFF /* ModuleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleManager.swift; sourceTree = "<group>"; };
@@ -115,6 +119,7 @@
115119
13103E812D589D77000F0673 /* AniList */ = {
116120
isa = PBXGroup;
117121
children = (
122+
136F21B72D5B8DAC006409AC /* MediaInfo */,
118123
13103E872D58A392000F0673 /* Struct */,
119124
13103E822D589D7D000F0673 /* HomePage */,
120125
);
@@ -124,6 +129,7 @@
124129
13103E822D589D7D000F0673 /* HomePage */ = {
125130
isa = PBXGroup;
126131
children = (
132+
136F21BA2D5B8F17006409AC /* DetailsView */,
127133
13103E832D589D8B000F0673 /* AniList-Seasonal.swift */,
128134
13103E852D58A328000F0673 /* AniList-Trending.swift */,
129135
);
@@ -272,6 +278,22 @@
272278
path = LibraryView;
273279
sourceTree = "<group>";
274280
};
281+
136F21B72D5B8DAC006409AC /* MediaInfo */ = {
282+
isa = PBXGroup;
283+
children = (
284+
136F21B82D5B8DD8006409AC /* AniList-MediaInfo.swift */,
285+
);
286+
path = MediaInfo;
287+
sourceTree = "<group>";
288+
};
289+
136F21BA2D5B8F17006409AC /* DetailsView */ = {
290+
isa = PBXGroup;
291+
children = (
292+
136F21BB2D5B8F29006409AC /* AniList-DetailsView.swift */,
293+
);
294+
path = DetailsView;
295+
sourceTree = "<group>";
296+
};
275297
138AA1B52D2D66EC0021F9DF /* EpisodeCell */ = {
276298
isa = PBXGroup;
277299
children = (
@@ -430,6 +452,7 @@
430452
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
431453
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
432454
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
455+
136F21B92D5B8DD8006409AC /* AniList-MediaInfo.swift in Sources */,
433456
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,
434457
13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */,
435458
13103E862D58A328000F0673 /* AniList-Trending.swift in Sources */,
@@ -447,6 +470,7 @@
447470
133D7C942D2BE2640075467E /* JSController.swift in Sources */,
448471
133D7C922D2BE2640075467E /* URLSession.swift in Sources */,
449472
133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */,
473+
136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */,
450474
133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */,
451475
13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */,
452476
133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */,
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//
2+
// AniList-DetailsView.swift
3+
// Sora
4+
//
5+
// Created by Francesco on 11/02/25.
6+
//
7+
8+
import SwiftUI
9+
import Kingfisher
10+
11+
struct MediaDetailItem: View {
12+
var title: String
13+
var value: String
14+
15+
var body: some View {
16+
VStack {
17+
Text(value)
18+
.font(.headline)
19+
Text(title)
20+
.font(.caption)
21+
.foregroundColor(.secondary)
22+
}
23+
.padding(.horizontal)
24+
}
25+
}
26+
27+
struct AniListDetailsView: View {
28+
let animeID: Int
29+
@State private var mediaInfo: [String: Any]?
30+
@State private var isLoading: Bool = true
31+
32+
var body: some View {
33+
ScrollView {
34+
VStack(spacing: 16) {
35+
if isLoading {
36+
ProgressView()
37+
.padding()
38+
} else if let media = mediaInfo {
39+
if let coverDict = media["coverImage"] as? [String: Any],
40+
let posterURLString = coverDict["extraLarge"] as? String,
41+
let posterURL = URL(string: posterURLString) {
42+
KFImage(posterURL)
43+
.placeholder {
44+
RoundedRectangle(cornerRadius: 10)
45+
.fill(Color.gray.opacity(0.3))
46+
.frame(width: 200, height: 300)
47+
.shimmering()
48+
}
49+
.resizable()
50+
.scaledToFit()
51+
.frame(width: 200, height: 300)
52+
.cornerRadius(10)
53+
.shadow(radius: 5)
54+
}
55+
56+
if let titleDict = media["title"] as? [String: Any],
57+
let userPreferred = titleDict["userPreferred"] as? String {
58+
Text(userPreferred)
59+
.font(.title2)
60+
.fontWeight(.bold)
61+
.padding(.top, 8)
62+
}
63+
64+
if let trailer = media["trailer"] as? [String: Any],
65+
let trailerID = trailer["id"] as? String,
66+
let site = trailer["site"] as? String {
67+
if site.lowercased() == "youtube",
68+
let url = URL(string: "https://www.youtube.com/watch?v=\(trailerID)") {
69+
Link("Watch Trailer on YouTube", destination: url)
70+
.padding(.top, 4)
71+
} else {
72+
Text("Trailer available on \(site)")
73+
.padding(.top, 4)
74+
}
75+
}
76+
77+
if let synopsis = media["description"] as? String {
78+
Text(synopsis)
79+
.padding(.horizontal)
80+
.foregroundColor(.secondary)
81+
.font(.system(size: 14))
82+
}
83+
84+
VStack(alignment: .leading, spacing: 4) {
85+
if let format = media["format"] as? String {
86+
Text("Format: \(format)")
87+
}
88+
if let status = media["status"] as? String {
89+
Text("Status: \(status)")
90+
}
91+
if let season = media["season"] as? String {
92+
Text("Season: \(season)")
93+
}
94+
if let startDate = media["startDate"] as? [String: Any],
95+
let year = startDate["year"] as? Int,
96+
let month = startDate["month"] as? Int,
97+
let day = startDate["day"] as? Int {
98+
Text("Start Date: \(year)-\(month)-\(day)")
99+
}
100+
if let endDate = media["endDate"] as? [String: Any],
101+
let year = endDate["year"] as? Int,
102+
let month = endDate["month"] as? Int,
103+
let day = endDate["day"] as? Int {
104+
Text("End Date: \(year)-\(month)-\(day)")
105+
}
106+
if let country = media["countryOfOrigin"] as? String {
107+
Text("Country: \(country)")
108+
}
109+
if let source = media["source"] as? String {
110+
Text("Source: \(source)")
111+
}
112+
}
113+
.font(.caption)
114+
.foregroundColor(.secondary)
115+
.padding(.horizontal)
116+
.padding(.top, 4)
117+
118+
HStack(spacing: 24) {
119+
if let type = media["type"] as? String {
120+
MediaDetailItem(title: "Type", value: type)
121+
}
122+
if let episodes = media["episodes"] as? Int {
123+
MediaDetailItem(title: "Episodes", value: "\(episodes)")
124+
}
125+
if let duration = media["duration"] as? Int {
126+
MediaDetailItem(title: "Length", value: "\(duration) mins")
127+
}
128+
}
129+
.frame(maxWidth: .infinity)
130+
.padding(.vertical)
131+
132+
if let charactersDict = media["characters"] as? [String: Any],
133+
let edges = charactersDict["edges"] as? [[String: Any]] {
134+
VStack(alignment: .leading, spacing: 8) {
135+
Text("Characters")
136+
.font(.headline)
137+
.padding(.horizontal)
138+
ScrollView(.horizontal, showsIndicators: false) {
139+
HStack(spacing: 16) {
140+
ForEach(Array(edges.enumerated()), id: \.offset) { _, edge in
141+
if let node = edge["node"] as? [String: Any],
142+
let nameDict = node["name"] as? [String: Any],
143+
let fullName = nameDict["full"] as? String,
144+
let imageDict = node["image"] as? [String: Any],
145+
let imageUrlStr = imageDict["large"] as? String,
146+
let imageUrl = URL(string: imageUrlStr) {
147+
VStack {
148+
KFImage(imageUrl)
149+
.resizable()
150+
.scaledToFill()
151+
.frame(width: 120, height: 120)
152+
.clipShape(Circle())
153+
Text(fullName)
154+
.font(.caption)
155+
}
156+
.frame(width: 140, height: 140)
157+
}
158+
}
159+
}
160+
.padding(.horizontal)
161+
}
162+
}
163+
}
164+
165+
if let stats = media["stats"] as? [String: Any],
166+
let scoreDistribution = stats["scoreDistribution"] as? [[String: Any]] {
167+
VStack(alignment: .center) {
168+
Text("Score Distribution")
169+
.font(.headline)
170+
HStack(alignment: .bottom, spacing: 8) {
171+
let maxValue = scoreDistribution.compactMap { $0["amount"] as? Int }.max() ?? 1
172+
ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { _, dataPoint in
173+
if let score = dataPoint["score"] as? Int,
174+
let amount = dataPoint["amount"] as? Int {
175+
VStack {
176+
Rectangle()
177+
.fill(Color.accentColor)
178+
.frame(width: 20, height: CGFloat(amount) / CGFloat(maxValue) * 100)
179+
Text("\(score)")
180+
.font(.caption)
181+
}
182+
}
183+
}
184+
}
185+
}
186+
.frame(maxWidth: .infinity)
187+
.padding(.horizontal)
188+
}
189+
190+
if let relations = media["relations"] as? [String: Any],
191+
let nodes = relations["nodes"] as? [[String: Any]] {
192+
VStack(alignment: .leading, spacing: 8) {
193+
Text("Correlation")
194+
.font(.headline)
195+
.padding(.horizontal)
196+
ScrollView(.horizontal, showsIndicators: false) {
197+
HStack(spacing: 10) {
198+
ForEach(Array(nodes.enumerated()), id: \.offset) { _, node in
199+
if let titleDict = node["title"] as? [String: Any],
200+
let title = titleDict["userPreferred"] as? String,
201+
let coverImageDict = node["coverImage"] as? [String: Any],
202+
let imageUrlStr = coverImageDict["extraLarge"] as? String,
203+
let imageUrl = URL(string: imageUrlStr) {
204+
VStack {
205+
KFImage(imageUrl)
206+
.resizable()
207+
.scaledToFill()
208+
.frame(width: 100, height: 150)
209+
.cornerRadius(10)
210+
Text(title)
211+
.font(.caption)
212+
}
213+
.frame(width: 130, height: 195)
214+
}
215+
}
216+
}
217+
.padding(.horizontal)
218+
}
219+
}
220+
}
221+
222+
} else {
223+
Text("Failed to load media details.")
224+
.padding()
225+
}
226+
}
227+
}
228+
.navigationBarTitle("")
229+
.navigationBarTitleDisplayMode(.inline)
230+
.navigationViewStyle(StackNavigationViewStyle())
231+
.onAppear {
232+
fetchDetails()
233+
}
234+
}
235+
236+
private func fetchDetails() {
237+
AnilistServiceMediaInfo.fetchAnimeDetails(animeID: animeID) { result in
238+
DispatchQueue.main.async {
239+
switch result {
240+
case .success(let media):
241+
self.mediaInfo = media
242+
case .failure(let error):
243+
print("Error: \(error)")
244+
}
245+
self.isLoading = false
246+
}
247+
}
248+
}
249+
}

0 commit comments

Comments
 (0)