mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 16:42:01 +00:00
holy moly hopefully this works
This commit is contained in:
parent
972af358ca
commit
bdd82f3f57
1 changed files with 291 additions and 272 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue