mirror of
https://github.com/cranci1/Sora.git
synced 2026-05-13 21:40:43 +00:00
Revert "yeah i dont even know what is this"
This reverts commit 3478a3f11e.
This commit is contained in:
parent
3478a3f11e
commit
d0a622c2b7
1 changed files with 60 additions and 214 deletions
|
|
@ -18,12 +18,6 @@ struct MediaItem: Identifiable {
|
||||||
let airdate: String
|
let airdate: String
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EpisodeLink: Equatable {
|
|
||||||
static func == (lhs: EpisodeLink, rhs: EpisodeLink) -> Bool {
|
|
||||||
return lhs.href == rhs.href && lhs.number == rhs.number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MediaInfoView: View {
|
struct MediaInfoView: View {
|
||||||
let title: String
|
let title: String
|
||||||
@State var imageUrl: String
|
@State var imageUrl: String
|
||||||
|
|
@ -92,24 +86,8 @@ struct MediaInfoView: View {
|
||||||
@State private var bulkDownloadProgress: String = ""
|
@State private var bulkDownloadProgress: String = ""
|
||||||
@State private var tmdbType: TMDBFetcher.MediaType? = nil
|
@State private var tmdbType: TMDBFetcher.MediaType? = nil
|
||||||
|
|
||||||
@State private var cachedGroupedEpisodes: [[EpisodeLink]] = []
|
|
||||||
@State private var cachedRanges: [Range<Int>] = []
|
|
||||||
@State private var cachedBannerImage: String = ""
|
|
||||||
@State private var lastEpisodeCount: Int = 0
|
|
||||||
@State private var lastChunkSize: Int = 0
|
|
||||||
|
|
||||||
@State private var viewState: ViewState = .loading
|
|
||||||
|
|
||||||
enum ViewState {
|
|
||||||
case loading
|
|
||||||
case loaded
|
|
||||||
case error
|
|
||||||
}
|
|
||||||
|
|
||||||
@State private var currentTasks: Set<Task<Void, Never>> = []
|
|
||||||
|
|
||||||
private var isGroupedBySeasons: Bool {
|
private var isGroupedBySeasons: Bool {
|
||||||
return cachedGroupedEpisodes.count > 1
|
return groupedEpisodes().count > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isCompactLayout: Bool {
|
private var isCompactLayout: Bool {
|
||||||
|
|
@ -134,14 +112,11 @@ struct MediaInfoView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
Group {
|
Group {
|
||||||
switch viewState {
|
if isLoading {
|
||||||
case .loading:
|
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.padding()
|
.padding()
|
||||||
case .loaded:
|
} else {
|
||||||
mainScrollView
|
mainScrollView
|
||||||
case .error:
|
|
||||||
errorView
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|
@ -149,7 +124,6 @@ struct MediaInfoView: View {
|
||||||
.onAppear {
|
.onAppear {
|
||||||
buttonRefreshTrigger.toggle()
|
buttonRefreshTrigger.toggle()
|
||||||
tabBarController.hideTabBar()
|
tabBarController.hideTabBar()
|
||||||
updateCachedBannerImage()
|
|
||||||
}
|
}
|
||||||
.onChange(of: selectedRange) { newValue in
|
.onChange(of: selectedRange) { newValue in
|
||||||
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
|
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
|
||||||
|
|
@ -157,15 +131,8 @@ struct MediaInfoView: View {
|
||||||
.onChange(of: selectedSeason) { newValue in
|
.onChange(of: selectedSeason) { newValue in
|
||||||
UserDefaults.standard.set(newValue, forKey: selectedSeasonKey)
|
UserDefaults.standard.set(newValue, forKey: selectedSeasonKey)
|
||||||
}
|
}
|
||||||
.onChange(of: episodeLinks.count) { _ in
|
|
||||||
updateCachedComputations()
|
|
||||||
}
|
|
||||||
.onChange(of: episodeChunkSize) { _ in
|
|
||||||
updateCachedComputations()
|
|
||||||
}
|
|
||||||
.onDisappear(){
|
.onDisappear(){
|
||||||
tabBarController.showTabBar()
|
tabBarController.showTabBar()
|
||||||
cancelAllTasks()
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
guard !hasFetched else { return }
|
guard !hasFetched else { return }
|
||||||
|
|
@ -177,19 +144,13 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
DropManager.shared.showDrop(title: "Fetching Data", subtitle: "Please wait while fetching.", duration: 0.5, icon: UIImage(systemName: "arrow.triangle.2.circlepath"))
|
||||||
|
fetchDetails()
|
||||||
let fetchTask = Task {
|
|
||||||
await fetchDetailsAsync()
|
if savedCustomID != 0 {
|
||||||
|
itemID = savedCustomID
|
||||||
if !Task.isCancelled {
|
} else {
|
||||||
if savedCustomID != 0 {
|
fetchMetadataIDIfNeeded()
|
||||||
itemID = savedCustomID
|
|
||||||
} else {
|
|
||||||
await fetchMetadataIDIfNeededAsync()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentTasks.insert(fetchTask)
|
|
||||||
|
|
||||||
hasFetched = true
|
hasFetched = true
|
||||||
AnalyticsManager.shared.sendEvent(
|
AnalyticsManager.shared.sendEvent(
|
||||||
|
|
@ -214,7 +175,6 @@ struct MediaInfoView: View {
|
||||||
activeFetchID = nil
|
activeFetchID = nil
|
||||||
isFetchingEpisode = false
|
isFetchingEpisode = false
|
||||||
showStreamLoadingView = false
|
showStreamLoadingView = false
|
||||||
cancelAllTasks()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
|
|
@ -240,29 +200,6 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var errorView: some View {
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
Image(systemName: "exclamationmark.triangle")
|
|
||||||
.font(.system(size: 48))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Failed to Load")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Button("Retry") {
|
|
||||||
viewState = .loading
|
|
||||||
let retryTask = Task {
|
|
||||||
await fetchDetailsAsync()
|
|
||||||
}
|
|
||||||
currentTasks.insert(retryTask)
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainScrollView: some View {
|
private var mainScrollView: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
|
@ -282,7 +219,6 @@ struct MediaInfoView: View {
|
||||||
.clipped()
|
.clipped()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id("hero-image-\(imageUrl)")
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
|
@ -701,7 +637,7 @@ struct MediaInfoView: View {
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
}
|
}
|
||||||
} else if isGroupedBySeasons {
|
} else if isGroupedBySeasons {
|
||||||
let seasons = cachedGroupedEpisodes
|
let seasons = groupedEpisodes()
|
||||||
if seasons.count > 1 {
|
if seasons.count > 1 {
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(0..<seasons.count, id: \.self) { index in
|
ForEach(0..<seasons.count, id: \.self) { index in
|
||||||
|
|
@ -739,6 +675,8 @@ struct MediaInfoView: View {
|
||||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||||
|
|
||||||
|
let defaultBannerImageValue = getBannerImageBasedOnAppearance()
|
||||||
|
|
||||||
EpisodeCell(
|
EpisodeCell(
|
||||||
episodeIndex: i,
|
episodeIndex: i,
|
||||||
episode: ep.href,
|
episode: ep.href,
|
||||||
|
|
@ -746,7 +684,7 @@ struct MediaInfoView: View {
|
||||||
progress: progress,
|
progress: progress,
|
||||||
itemID: itemID ?? 0,
|
itemID: itemID ?? 0,
|
||||||
totalEpisodes: episodeLinks.count,
|
totalEpisodes: episodeLinks.count,
|
||||||
defaultBannerImage: cachedBannerImage,
|
defaultBannerImage: defaultBannerImageValue,
|
||||||
module: module,
|
module: module,
|
||||||
parentTitle: title,
|
parentTitle: title,
|
||||||
showPosterURL: imageUrl,
|
showPosterURL: imageUrl,
|
||||||
|
|
@ -768,21 +706,23 @@ struct MediaInfoView: View {
|
||||||
tmdbID: tmdbID,
|
tmdbID: tmdbID,
|
||||||
seasonNumber: 1
|
seasonNumber: 1
|
||||||
)
|
)
|
||||||
.id("episode-\(ep.href)-\(isMultiSelectMode)")
|
.disabled(isFetchingEpisode)
|
||||||
.disabled(isFetchingEpisode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var seasonsEpisodeList: some View {
|
private var seasonsEpisodeList: some View {
|
||||||
if !cachedGroupedEpisodes.isEmpty, selectedSeason < cachedGroupedEpisodes.count {
|
let seasons = groupedEpisodes()
|
||||||
|
if !seasons.isEmpty, selectedSeason < seasons.count {
|
||||||
LazyVStack(spacing: 15) {
|
LazyVStack(spacing: 15) {
|
||||||
ForEach(cachedGroupedEpisodes[selectedSeason], id: \.href) { ep in
|
ForEach(seasons[selectedSeason]) { ep in
|
||||||
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)")
|
||||||
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)")
|
||||||
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0
|
||||||
|
|
||||||
|
let defaultBannerImageValue = getBannerImageBasedOnAppearance()
|
||||||
|
|
||||||
EpisodeCell(
|
EpisodeCell(
|
||||||
episodeIndex: selectedSeason,
|
episodeIndex: selectedSeason,
|
||||||
episode: ep.href,
|
episode: ep.href,
|
||||||
|
|
@ -790,7 +730,7 @@ struct MediaInfoView: View {
|
||||||
progress: progress,
|
progress: progress,
|
||||||
itemID: itemID ?? 0,
|
itemID: itemID ?? 0,
|
||||||
totalEpisodes: episodeLinks.count,
|
totalEpisodes: episodeLinks.count,
|
||||||
defaultBannerImage: cachedBannerImage,
|
defaultBannerImage: defaultBannerImageValue,
|
||||||
module: module,
|
module: module,
|
||||||
parentTitle: title,
|
parentTitle: title,
|
||||||
showPosterURL: imageUrl,
|
showPosterURL: imageUrl,
|
||||||
|
|
@ -812,8 +752,7 @@ struct MediaInfoView: View {
|
||||||
tmdbID: tmdbID,
|
tmdbID: tmdbID,
|
||||||
seasonNumber: selectedSeason + 1
|
seasonNumber: selectedSeason + 1
|
||||||
)
|
)
|
||||||
.id("season-episode-\(ep.href)-\(isMultiSelectMode)")
|
.disabled(isFetchingEpisode)
|
||||||
.disabled(isFetchingEpisode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -821,132 +760,6 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateCachedComputations() {
|
|
||||||
let currentEpisodeCount = episodeLinks.count
|
|
||||||
let currentChunkSize = episodeChunkSize
|
|
||||||
|
|
||||||
guard currentEpisodeCount != lastEpisodeCount || currentChunkSize != lastChunkSize else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEpisodeCount = currentEpisodeCount
|
|
||||||
lastChunkSize = currentChunkSize
|
|
||||||
|
|
||||||
cachedGroupedEpisodes = groupedEpisodes()
|
|
||||||
cachedRanges = generateRanges()
|
|
||||||
updateCachedBannerImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateCachedBannerImage() {
|
|
||||||
cachedBannerImage = getBannerImageBasedOnAppearance()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchDetailsAsync() async {
|
|
||||||
await MainActor.run {
|
|
||||||
viewState = .loading
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await Task.sleep(nanoseconds: 500_000_000)
|
|
||||||
|
|
||||||
guard !Task.isCancelled else { return }
|
|
||||||
|
|
||||||
let jsContent = try moduleManager.getModuleContent(module)
|
|
||||||
|
|
||||||
await MainActor.run {
|
|
||||||
jsController.loadScript(jsContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
if module.metadata.asyncJS == true {
|
|
||||||
jsController.fetchDetailsJS(url: href) { items, episodes in
|
|
||||||
if let item = items.first {
|
|
||||||
self.synopsis = item.description
|
|
||||||
self.aliases = item.aliases
|
|
||||||
self.airdate = item.airdate
|
|
||||||
}
|
|
||||||
self.episodeLinks = episodes
|
|
||||||
self.updateCachedComputations()
|
|
||||||
self.restoreSelectionState()
|
|
||||||
self.viewState = .loaded
|
|
||||||
self.isRefetching = false
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
jsController.fetchDetails(url: href) { items, episodes in
|
|
||||||
if let item = items.first {
|
|
||||||
self.synopsis = item.description
|
|
||||||
self.aliases = item.aliases
|
|
||||||
self.airdate = item.airdate
|
|
||||||
}
|
|
||||||
self.episodeLinks = episodes
|
|
||||||
self.updateCachedComputations()
|
|
||||||
self.restoreSelectionState()
|
|
||||||
self.viewState = .loaded
|
|
||||||
self.isRefetching = false
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
await MainActor.run {
|
|
||||||
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
|
||||||
viewState = .error
|
|
||||||
isRefetching = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchMetadataIDIfNeededAsync() async {
|
|
||||||
let provider = UserDefaults.standard.string(forKey: "metadataProviders") ?? "TMDB"
|
|
||||||
let cleaned = cleanTitle(title)
|
|
||||||
|
|
||||||
if provider == "TMDB" {
|
|
||||||
await MainActor.run { tmdbID = nil }
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
tmdbFetcher.fetchBestMatchID(for: cleaned) { id, type in
|
|
||||||
Task { @MainActor in
|
|
||||||
guard !Task.isCancelled else {
|
|
||||||
continuation.resume()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.tmdbID = id
|
|
||||||
self.tmdbType = type
|
|
||||||
Logger.shared.log("Fetched TMDB ID: \(id ?? -1) (\(type?.rawValue ?? "unknown")) for title: \(cleaned)", type: "Debug")
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if provider == "Anilist" {
|
|
||||||
await MainActor.run { itemID = nil }
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
fetchItemID(byTitle: cleaned) { result in
|
|
||||||
Task { @MainActor in
|
|
||||||
guard !Task.isCancelled else {
|
|
||||||
continuation.resume()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch result {
|
|
||||||
case .success(let id):
|
|
||||||
self.itemID = id
|
|
||||||
Logger.shared.log("Fetched AniList ID: \(id) for title: \(cleaned)", type: "Debug")
|
|
||||||
case .failure(let error):
|
|
||||||
Logger.shared.log("Failed to fetch AniList ID: \(error)", type: "Error")
|
|
||||||
}
|
|
||||||
continuation.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func cancelAllTasks() {
|
|
||||||
for task in currentTasks {
|
|
||||||
task.cancel()
|
|
||||||
}
|
|
||||||
currentTasks.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func restoreSelectionState() {
|
private func restoreSelectionState() {
|
||||||
if let savedStart = UserDefaults.standard.object(forKey: selectedRangeKey) as? Int,
|
if let savedStart = UserDefaults.standard.object(forKey: selectedRangeKey) as? Int,
|
||||||
let savedRange = generateRanges().first(where: { $0.lowerBound == savedStart }) {
|
let savedRange = generateRanges().first(where: { $0.lowerBound == savedStart }) {
|
||||||
|
|
@ -956,7 +769,7 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let savedSeason = UserDefaults.standard.object(forKey: selectedSeasonKey) as? Int {
|
if let savedSeason = UserDefaults.standard.object(forKey: selectedSeasonKey) as? Int {
|
||||||
let maxIndex = max(0, cachedGroupedEpisodes.count - 1)
|
let maxIndex = max(0, groupedEpisodes().count - 1)
|
||||||
selectedSeason = min(savedSeason, maxIndex)
|
selectedSeason = min(savedSeason, maxIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1044,7 +857,7 @@ struct MediaInfoView: View {
|
||||||
var updates = [String: Double]()
|
var updates = [String: Double]()
|
||||||
|
|
||||||
if inSeason {
|
if inSeason {
|
||||||
let seasons = cachedGroupedEpisodes
|
let seasons = groupedEpisodes()
|
||||||
for ep2 in seasons[selectedSeason] where ep2.number < ep.number {
|
for ep2 in seasons[selectedSeason] where ep2.number < ep.number {
|
||||||
let href = ep2.href
|
let href = ep2.href
|
||||||
updates["lastPlayedTime_\(href)"] = 99999999.0
|
updates["lastPlayedTime_\(href)"] = 99999999.0
|
||||||
|
|
@ -1229,10 +1042,43 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchDetails() {
|
func fetchDetails() {
|
||||||
let fetchTask = Task {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
await fetchDetailsAsync()
|
Task {
|
||||||
|
do {
|
||||||
|
let jsContent = try moduleManager.getModuleContent(module)
|
||||||
|
jsController.loadScript(jsContent)
|
||||||
|
if module.metadata.asyncJS == true {
|
||||||
|
jsController.fetchDetailsJS(url: href) { items, episodes in
|
||||||
|
if let item = items.first {
|
||||||
|
self.synopsis = item.description
|
||||||
|
self.aliases = item.aliases
|
||||||
|
self.airdate = item.airdate
|
||||||
|
}
|
||||||
|
self.episodeLinks = episodes
|
||||||
|
self.restoreSelectionState()
|
||||||
|
self.isLoading = false
|
||||||
|
self.isRefetching = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsController.fetchDetails(url: href) { items, episodes in
|
||||||
|
if let item = items.first {
|
||||||
|
self.synopsis = item.description
|
||||||
|
self.aliases = item.aliases
|
||||||
|
self.airdate = item.airdate
|
||||||
|
}
|
||||||
|
self.episodeLinks = episodes
|
||||||
|
self.restoreSelectionState()
|
||||||
|
self.isLoading = false
|
||||||
|
self.isRefetching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Logger.shared.log("Error loading module: \(error)", type: "Error")
|
||||||
|
self.isLoading = false
|
||||||
|
self.isRefetching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
currentTasks.insert(fetchTask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStream(href: String) {
|
func fetchStream(href: String) {
|
||||||
|
|
@ -1625,7 +1471,7 @@ struct MediaInfoView: View {
|
||||||
|
|
||||||
private func selectAllVisibleEpisodes() {
|
private func selectAllVisibleEpisodes() {
|
||||||
if isGroupedBySeasons {
|
if isGroupedBySeasons {
|
||||||
let seasons = cachedGroupedEpisodes
|
let seasons = groupedEpisodes()
|
||||||
if !seasons.isEmpty, selectedSeason < seasons.count {
|
if !seasons.isEmpty, selectedSeason < seasons.count {
|
||||||
for episode in seasons[selectedSeason] {
|
for episode in seasons[selectedSeason] {
|
||||||
selectedEpisodes.insert(episode.number)
|
selectedEpisodes.insert(episode.number)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue