mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-20 16:12:50 +00:00
164 lines
5.4 KiB
Swift
164 lines
5.4 KiB
Swift
//
|
|
// EpisodeCell.swift
|
|
// Sora
|
|
//
|
|
// Created by Francesco on 18/12/24.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Kingfisher
|
|
|
|
struct EpisodeLink: Identifiable {
|
|
let id = UUID()
|
|
let number: Int
|
|
let href: String
|
|
}
|
|
|
|
struct EpisodeCell: View {
|
|
let episodeIndex: Int
|
|
let episode: String
|
|
let episodeID: Int
|
|
let progress: Double
|
|
let itemID: Int
|
|
|
|
let onTap: (String) -> Void
|
|
let onMarkAllPrevious: () -> Void
|
|
|
|
@State private var episodeTitle: String = ""
|
|
@State private var episodeImageUrl: String = ""
|
|
@State private var isLoading: Bool = true
|
|
@State private var currentProgress: Double = 0.0
|
|
|
|
var body: some View {
|
|
HStack {
|
|
ZStack {
|
|
KFImage(URL(string: episodeImageUrl.isEmpty ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/main/assets/banner2.png" : episodeImageUrl))
|
|
.resizable()
|
|
.aspectRatio(16/9, contentMode: .fill)
|
|
.frame(width: 100, height: 56)
|
|
.cornerRadius(8)
|
|
|
|
if isLoading {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle())
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading) {
|
|
Text("Episode \(episodeID + 1)")
|
|
.font(.system(size: 15))
|
|
if !episodeTitle.isEmpty {
|
|
Text(episodeTitle)
|
|
.font(.system(size: 13))
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
CircularProgressBar(progress: currentProgress)
|
|
.frame(width: 40, height: 40)
|
|
}
|
|
.contentShape(Rectangle())
|
|
.contextMenu {
|
|
if progress <= 0.9 {
|
|
Button(action: markAsWatched) {
|
|
Label("Mark as Watched", systemImage: "checkmark.circle")
|
|
}
|
|
}
|
|
|
|
if progress != 0 {
|
|
Button(action: resetProgress) {
|
|
Label("Reset Progress", systemImage: "arrow.counterclockwise")
|
|
}
|
|
}
|
|
|
|
if episodeIndex > 0 {
|
|
Button(action: onMarkAllPrevious) {
|
|
Label("Mark All Previous Watched", systemImage: "checkmark.circle.fill")
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
updateProgress()
|
|
|
|
if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil
|
|
|| UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") {
|
|
fetchEpisodeDetails()
|
|
}
|
|
}
|
|
.onChange(of: progress) { newProgress in
|
|
updateProgress()
|
|
}
|
|
.onTapGesture {
|
|
onTap(episodeImageUrl)
|
|
}
|
|
}
|
|
|
|
private func markAsWatched() {
|
|
UserDefaults.standard.set(99999999.0, forKey: "lastPlayedTime_\(episode)")
|
|
UserDefaults.standard.set(99999999.0, forKey: "totalTime_\(episode)")
|
|
updateProgress()
|
|
}
|
|
|
|
private func resetProgress() {
|
|
UserDefaults.standard.set(0.0, forKey: "lastPlayedTime_\(episode)")
|
|
UserDefaults.standard.set(0.0, forKey: "totalTime_\(episode)")
|
|
updateProgress()
|
|
}
|
|
|
|
private func updateProgress() {
|
|
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(episode)")
|
|
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(episode)")
|
|
currentProgress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
|
}
|
|
|
|
private func fetchEpisodeDetails() {
|
|
guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(itemID)") else {
|
|
isLoading = false
|
|
return
|
|
}
|
|
|
|
URLSession.custom.dataTask(with: url) { data, _, error in
|
|
if let error = error {
|
|
Logger.shared.log("Failed to fetch episode details: \(error)", type: "Error")
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
}
|
|
return
|
|
}
|
|
|
|
guard let data = data else {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
}
|
|
return
|
|
}
|
|
|
|
do {
|
|
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
|
|
guard let json = jsonObject as? [String: Any],
|
|
let episodes = json["episodes"] as? [String: Any],
|
|
let episodeDetails = episodes["\(episodeID + 1)"] as? [String: Any],
|
|
let title = episodeDetails["title"] as? [String: String],
|
|
let image = episodeDetails["image"] as? String else {
|
|
Logger.shared.log("Invalid response format", type: "Error")
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
}
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.episodeTitle = title["en"] ?? ""
|
|
self.episodeImageUrl = image
|
|
self.isLoading = false
|
|
}
|
|
} catch {
|
|
DispatchQueue.main.async {
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}.resume()
|
|
}
|
|
}
|