holy moly hopefully this works

This commit is contained in:
cranci1 2025-02-16 09:28:02 +01:00
parent 972af358ca
commit bdd82f3f57

View file

@ -8,34 +8,80 @@
import SwiftUI
import Kingfisher
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)
}
}
struct AniListDetailsView: View {
let animeID: Int
@State private var mediaInfo: [String: Any]?
@State private var isLoading: Bool = true
@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 isLoading {
if viewModel.isLoading {
ProgressView()
.padding()
} else if let media = mediaInfo {
} 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,
@ -83,9 +129,13 @@ struct AniListDetailsView: View {
Spacer()
}
.padding()
}
}
Divider()
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 {
@ -127,19 +177,29 @@ struct AniListDetailsView: View {
}
}
}
}
}
Divider()
struct SynopsisView: View {
let synopsis: String?
if let synopsis = media["description"] as? String {
var body: some View {
if let synopsis = synopsis {
Text(synopsis.strippedHTML)
.padding(.horizontal)
.foregroundColor(.secondary)
.font(.system(size: 14))
} else {
EmptyView()
}
}
}
Divider()
struct CharactersView: View {
let characters: [String: Any]?
if let charactersDict = media["characters"] as? [String: Any],
var body: some View {
if let charactersDict = characters,
let edges = charactersDict["edges"] as? [[String: Any]] {
VStack(alignment: .leading, spacing: 8) {
Text("Characters")
@ -147,13 +207,31 @@ struct AniListDetailsView: View {
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(Array(edges.enumerated()), id: \.offset) { _, edge in
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 {
@ -166,54 +244,40 @@ struct AniListDetailsView: View {
.scaledToFill()
.frame(width: 90, height: 90)
.clipShape(Circle())
Text(fullName)
Text(name)
.font(.caption)
.lineLimit(1)
}
.frame(width: 105, height: 105)
}
}
}
.padding(.horizontal)
}
}
.frame(width: 105, height: 110)
}
}
Divider()
struct ScoreDistributionView: View {
let stats: [String: Any]?
if let stats = media["stats"] as? [String: Any],
let scoreDistribution = stats["scoreDistribution"] as? [[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 totalScore = scoreDistribution.reduce(0) { partialResult, dataPoint in
if let score = dataPoint["score"] as? Int, let amount = dataPoint["amount"] as? Int {
return partialResult + score * amount
}
return partialResult
let calculatedHeights = scoreDistribution.map { dataPoint -> CGFloat in
guard let amount = dataPoint["amount"] as? Int else { return 0 }
return CGFloat(amount) / CGFloat(maxValue) * 100
}
let totalCount = scoreDistribution.reduce(0) { partialResult, dataPoint in
if let amount = dataPoint["amount"] as? Int {
return partialResult + amount
}
return partialResult
}
let computedAverage = totalCount > 0 ? Double(totalScore) / Double(totalCount) : 0.0
VStack {
Text("Score Distribution")
.font(.headline)
HStack(alignment: .center) {
MediaDetailItem(title: "Average Score", value: String(format: "%.1f", computedAverage))
.frame(width: 120, alignment: .leading)
VStack(alignment: .center) {
HStack(alignment: .bottom, spacing: 8) {
ForEach(Array(scoreDistribution.enumerated()), id: \.offset) { _, dataPoint in
if let score = dataPoint["score"] as? Int,
let amount = dataPoint["amount"] as? Int {
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: CGFloat(amount) / CGFloat(maxValue) * 100)
.frame(width: 20, height: calculatedHeights[index])
Text("\(score)")
.font(.caption)
}
@ -221,77 +285,32 @@ struct AniListDetailsView: View {
}
}
}
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
}
Divider()
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)
.placeholder {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
.frame(width: 100, height: 150)
.shimmering()
}
.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()
barHeights = calculatedHeights
}
.onChange(of: scoreDistribution) { _ in
barHeights = calculatedHeights
}
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
}
} 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)
}
}