mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +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,6 +8,297 @@
|
|||
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
|
||||
|
|
@ -23,275 +314,3 @@ struct MediaDetailItem: View {
|
|||
.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 {
|
||||
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()
|
||||
|
||||
Divider()
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
if let synopsis = media["description"] as? String {
|
||||
Text(synopsis.strippedHTML)
|
||||
.padding(.horizontal)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
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: 8) {
|
||||
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)
|
||||
.placeholder {
|
||||
Circle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 90, height: 90)
|
||||
.shimmering()
|
||||
}
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 90, height: 90)
|
||||
.clipShape(Circle())
|
||||
Text(fullName)
|
||||
.font(.caption)
|
||||
}
|
||||
.frame(width: 105, height: 105)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
if let stats = media["stats"] as? [String: Any],
|
||||
let scoreDistribution = stats["scoreDistribution"] as? [[String: Any]] {
|
||||
|
||||
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 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 {
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(Color.accentColor)
|
||||
.frame(width: 20, height: CGFloat(amount) / CGFloat(maxValue) * 100)
|
||||
Text("\(score)")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue