diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index 8ffd621..4bcd995 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -126,6 +126,29 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele private var loadedTimeRangesObservation: NSKeyValueObservation? private var playerTimeControlStatusObserver: NSKeyValueObservation? + private var isDimmed = false + private var dimButton: UIButton! + private var dimButtonToSlider: NSLayoutConstraint! + private var dimButtonToRight: NSLayoutConstraint! + private var dimButtonTimer: Timer? + + private lazy var controlsToHide: [UIView] = [ + dismissButton, + playPauseButton, + backwardButton, + forwardButton, + sliderHostingController!.view, + skip85Button, + marqueeLabel, + menuButton, + qualityButton, + speedButton, + watchNextButton, + volumeSliderHostingView! + ] + + private var originalHiddenStates: [UIView: Bool] = [:] + private var volumeObserver: NSKeyValueObservation? private var audioSession = AVAudioSession.sharedInstance() private var hiddenVolumeView = MPVolumeView(frame: .zero) @@ -195,6 +218,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele setupSubtitleLabel() setupDismissButton() volumeSlider() + setupDimButton() setupSpeedButton() setupQualityButton() setupMenuButton() @@ -204,6 +228,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele startUpdateTimer() setupAudioSession() + controlsToHide.forEach { originalHiddenStates[$0] = $0.isHidden } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.checkForHLSStream() @@ -641,7 +666,6 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele subtitleLabel.textAlignment = .center subtitleLabel.numberOfLines = 0 subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - updateSubtitleLabelAppearance() view.addSubview(subtitleLabel) subtitleLabel.translatesAutoresizingMaskIntoConstraints = false @@ -662,6 +686,8 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele view.addSubview(topSubtitleLabel) topSubtitleLabel.translatesAutoresizingMaskIntoConstraints = false + updateSubtitleLabelAppearance() + NSLayoutConstraint.activate([ topSubtitleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), topSubtitleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 30), @@ -783,6 +809,38 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele ]) } + private func setupDimButton() { + let cfg = UIImage.SymbolConfiguration(pointSize: 24, weight: .regular) + dimButton = UIButton(type: .system) + dimButton.setImage(UIImage(systemName: "moon.fill", withConfiguration: cfg), for: .normal) + dimButton.tintColor = .white + dimButton.addTarget(self, action: #selector(dimTapped), for: .touchUpInside) + controlsContainerView.addSubview(dimButton) + dimButton.translatesAutoresizingMaskIntoConstraints = false + + dimButton.layer.shadowColor = UIColor.black.cgColor + dimButton.layer.shadowOffset = CGSize(width: 0, height: 2) + dimButton.layer.shadowOpacity = 0.6 + dimButton.layer.shadowRadius = 4 + dimButton.layer.masksToBounds = false + + NSLayoutConstraint.activate([ + dimButton.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor), + dimButton.widthAnchor.constraint(equalToConstant: 24), + dimButton.heightAnchor.constraint(equalToConstant: 24), + ]) + + dimButtonToSlider = dimButton.trailingAnchor.constraint( + equalTo: volumeSliderHostingView!.leadingAnchor, + constant: -8 + ) + dimButtonToRight = dimButton.trailingAnchor.constraint( + equalTo: controlsContainerView.trailingAnchor, + constant: -16 + ) + + dimButtonToSlider.isActive = true + } func updateMarqueeConstraints() { UIView.performWithoutAnimation { @@ -956,25 +1014,31 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } func updateSubtitleLabelAppearance() { - subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - subtitleLabel.textColor = subtitleUIColor() - subtitleLabel.backgroundColor = subtitleBackgroundEnabled ? UIColor.black.withAlphaComponent(0.6) : .clear - subtitleLabel.layer.cornerRadius = 5 - subtitleLabel.clipsToBounds = true - subtitleLabel.layer.shadowColor = UIColor.black.cgColor - subtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) - subtitleLabel.layer.shadowOpacity = 1.0 - subtitleLabel.layer.shadowOffset = CGSize.zero - - topSubtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) - topSubtitleLabel.textColor = subtitleUIColor() - topSubtitleLabel.backgroundColor = subtitleBackgroundEnabled ? UIColor.black.withAlphaComponent(0.6) : .clear - topSubtitleLabel.layer.cornerRadius = 5 - topSubtitleLabel.clipsToBounds = true - topSubtitleLabel.layer.shadowColor = UIColor.black.cgColor - topSubtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) - topSubtitleLabel.layer.shadowOpacity = 1.0 - topSubtitleLabel.layer.shadowOffset = CGSize.zero + // subtitleLabel always exists here: + subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + subtitleLabel.textColor = subtitleUIColor() + subtitleLabel.backgroundColor = subtitleBackgroundEnabled + ? UIColor.black.withAlphaComponent(0.6) + : .clear + subtitleLabel.layer.cornerRadius = 5 + subtitleLabel.clipsToBounds = true + subtitleLabel.layer.shadowColor = UIColor.black.cgColor + subtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius) + subtitleLabel.layer.shadowOpacity = 1.0 + subtitleLabel.layer.shadowOffset = .zero + + // only style it if it’s been created already + topSubtitleLabel?.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize)) + topSubtitleLabel?.textColor = subtitleUIColor() + topSubtitleLabel?.backgroundColor = subtitleBackgroundEnabled + ? UIColor.black.withAlphaComponent(0.6) + : .clear + topSubtitleLabel?.layer.cornerRadius = 5 + topSubtitleLabel?.clipsToBounds = true + topSubtitleLabel?.layer.shadowColor = UIColor.black.cgColor + topSubtitleLabel?.layer.shadowRadius = CGFloat(subtitleShadowRadius) + topSubtitleLabel?.layer.shadowOpacity = 1.0 + topSubtitleLabel?.layer.shadowOffset = .zero } func subtitleUIColor() -> UIColor { @@ -1119,12 +1183,24 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } @objc func toggleControls() { - isControlsVisible.toggle() - UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: { - let alphaVal: CGFloat = self.isControlsVisible ? 1 : 0 - self.controlsContainerView.alpha = alphaVal - self.skip85Button.alpha = alphaVal - }) + if isDimmed { + dimButton.isHidden = false + dimButton.alpha = 1.0 + dimButtonTimer?.invalidate() + dimButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in + guard let self = self else { return } + UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) { + self.dimButton.alpha = 0 + } + } + } else { + isControlsVisible.toggle() + UIView.animate(withDuration: 0.2) { + let a: CGFloat = self.isControlsVisible ? 1 : 0 + self.controlsContainerView.alpha = a + self.skip85Button.alpha = a + } + } } @objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) { @@ -1234,6 +1310,43 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } } + @objc private func dimTapped() { + isDimmed.toggle() + + if isDimmed { + originalHiddenStates = [:] + for view in controlsToHide { + originalHiddenStates[view] = view.isHidden + view.isHidden = true + } + + blackCoverView.alpha = 1.0 + + dimButtonToSlider.isActive = false + dimButtonToRight.isActive = true + + dimButton.isHidden = true + + dimButtonTimer?.invalidate() + } else { + for view in controlsToHide { + if let wasHidden = originalHiddenStates[view] { + view.isHidden = wasHidden + } + } + + blackCoverView.alpha = 0.4 + + dimButtonToRight.isActive = false + dimButtonToSlider.isActive = true + + dimButton.isHidden = false + dimButton.alpha = 1.0 + + dimButtonTimer?.invalidate() + } + } + func speedChangerMenu() -> UIMenu { let speeds: [Double] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] let playbackSpeedActions = speeds.map { speed in diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 9b08b39..902228e 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -52,242 +52,298 @@ struct MediaInfoView: View { @State private var selectedRange: Range = 0..<100 @State private var showSettingsMenu = false @State private var customAniListID: Int? + @State private var showStreamLoadingView: Bool = false + @State private var currentStreamTitle: String = "" + + @State private var activeFetchID: UUID? = nil + @Environment(\.dismiss) private var dismiss private var isGroupedBySeasons: Bool { return groupedEpisodes().count > 1 } var body: some View { - Group { - if isLoading { - ProgressView() - .padding() - } else { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - HStack(alignment: .top, spacing: 10) { - KFImage(URL(string: imageUrl)) - .placeholder { - RoundedRectangle(cornerRadius: 10) - .fill(Color.gray.opacity(0.3)) - .frame(width: 150, height: 225) - .shimmering() - } - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 150, height: 225) - .clipped() - .cornerRadius(10) - - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.system(size: 17)) - .fontWeight(.bold) - .onLongPressGesture { - UIPasteboard.general.string = title - DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) + ZStack { + Group { + if isLoading { + ProgressView() + .padding() + } else { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .top, spacing: 10) { + KFImage(URL(string: imageUrl)) + .placeholder { + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + .frame(width: 150, height: 225) + .shimmering() } + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 150, height: 225) + .clipped() + .cornerRadius(10) - if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { - Text(aliases) - .font(.system(size: 13)) - .foregroundColor(.secondary) - } - - Spacer() - - if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { - HStack(alignment: .center, spacing: 12) { - HStack(spacing: 4) { - Image(systemName: "calendar") - .resizable() - .frame(width: 15, height: 15) - .foregroundColor(.secondary) - - Text(airdate) - .font(.system(size: 12)) - .foregroundColor(.secondary) + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.system(size: 17)) + .fontWeight(.bold) + .onLongPressGesture { + UIPasteboard.general.string = title + DropManager.shared.showDrop(title: "Copied to Clipboard", subtitle: "", duration: 1.0, icon: UIImage(systemName: "doc.on.clipboard.fill")) } - .padding(4) + + if !aliases.isEmpty && aliases != title && aliases != "N/A" && aliases != "No Data" { + Text(aliases) + .font(.system(size: 13)) + .foregroundColor(.secondary) } - } - - HStack(alignment: .center, spacing: 12) { - Button(action: { - openSafariViewController(with: href) - }) { - HStack(spacing: 4) { - Text(module.metadata.sourceName) - .font(.system(size: 13)) - .foregroundColor(.primary) + + Spacer() + + if !airdate.isEmpty && airdate != "N/A" && airdate != "No Data" { + HStack(alignment: .center, spacing: 12) { + HStack(spacing: 4) { + Image(systemName: "calendar") + .resizable() + .frame(width: 15, height: 15) + .foregroundColor(.secondary) + + Text(airdate) + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .padding(4) + } + } + + HStack(alignment: .center, spacing: 12) { + Button(action: { + openSafariViewController(with: href) + }) { + HStack(spacing: 4) { + Text(module.metadata.sourceName) + .font(.system(size: 13)) + .foregroundColor(.primary) + + Image(systemName: "safari") + .resizable() + .frame(width: 20, height: 20) + .foregroundColor(.primary) + } + .padding(4) + .background(Capsule().fill(Color.accentColor.opacity(0.4))) + } + + Menu { + Button(action: { + showCustomIDAlert() + }) { + Label("Set Custom AniList ID", systemImage: "number") + } - Image(systemName: "safari") + if let customID = customAniListID { + Button(action: { + customAniListID = nil + itemID = nil + fetchItemID(byTitle: cleanTitle(title)) { result in + switch result { + case .success(let id): + itemID = id + case .failure(let error): + Logger.shared.log("Failed to fetch AniList ID: \(error)") + } + } + }) { + Label("Reset AniList ID", systemImage: "arrow.clockwise") + } + } + + if let id = itemID ?? customAniListID { + Button(action: { + if let url = URL(string: "https://anilist.co/anime/\(id)") { + openSafariViewController(with: url.absoluteString) + } + }) { + Label("Open in AniList", systemImage: "link") + } + } + + Divider() + + Button(action: { + Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") + DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) + }) { + Label("Log Debug Info", systemImage: "terminal") + } + } label: { + Image(systemName: "ellipsis.circle") .resizable() .frame(width: 20, height: 20) .foregroundColor(.primary) } - .padding(4) - .background(Capsule().fill(Color.accentColor.opacity(0.4))) + } + } + } + + if !synopsis.isEmpty { + VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .center) { + Text("Synopsis") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + Button(action: { + showFullSynopsis.toggle() + }) { + Text(showFullSynopsis ? "Less" : "More") + .font(.system(size: 14)) + } } - Menu { - Button(action: { - showCustomIDAlert() - }) { - Label("Set Custom AniList ID", systemImage: "number") - } - - if let customID = customAniListID { - Button(action: { - customAniListID = nil - itemID = nil - fetchItemID(byTitle: cleanTitle(title)) { result in - switch result { - case .success(let id): - itemID = id - case .failure(let error): - Logger.shared.log("Failed to fetch AniList ID: \(error)") - } - } - }) { - Label("Reset AniList ID", systemImage: "arrow.clockwise") - } - } - - if let id = itemID ?? customAniListID { - Button(action: { - if let url = URL(string: "https://anilist.co/anime/\(id)") { - openSafariViewController(with: url.absoluteString) - } - }) { - Label("Open in AniList", systemImage: "link") - } - } - - Divider() - - Button(action: { - Logger.shared.log("Debug Info:\nTitle: \(title)\nHref: \(href)\nModule: \(module.metadata.sourceName)\nAniList ID: \(itemID ?? -1)\nCustom ID: \(customAniListID ?? -1)", type: "Debug") - DropManager.shared.showDrop(title: "Debug Info Logged", subtitle: "", duration: 1.0, icon: UIImage(systemName: "terminal")) - }) { - Label("Log Debug Info", systemImage: "terminal") - } - } label: { - Image(systemName: "ellipsis.circle") - .resizable() - .frame(width: 20, height: 20) + Text(synopsis) + .lineLimit(showFullSynopsis ? nil : 4) + .font(.system(size: 14)) + } + } + + HStack { + Button(action: { + playFirstUnwatchedEpisode() + }) { + HStack { + Image(systemName: "play.fill") + .foregroundColor(.primary) + Text(startWatchingText) + .font(.headline) .foregroundColor(.primary) } + .padding() + .frame(maxWidth: .infinity) + .background(Color.accentColor) + .cornerRadius(10) } - } - } - - if !synopsis.isEmpty { - VStack(alignment: .leading, spacing: 2) { - HStack(alignment: .center) { - Text("Synopsis") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - Button(action: { - showFullSynopsis.toggle() - }) { - Text(showFullSynopsis ? "Less" : "More") - .font(.system(size: 14)) - } - } + .disabled(isFetchingEpisode) + .id(buttonRefreshTrigger) - Text(synopsis) - .lineLimit(showFullSynopsis ? nil : 4) - .font(.system(size: 14)) - } - } - - HStack { - Button(action: { - playFirstUnwatchedEpisode() - }) { - HStack { - Image(systemName: "play.fill") - .foregroundColor(.primary) - Text(startWatchingText) - .font(.headline) - .foregroundColor(.primary) + Button(action: { + libraryManager.toggleBookmark( + title: title, + imageUrl: imageUrl, + href: href, + moduleId: module.id.uuidString, + moduleName: module.metadata.sourceName + ) + }) { + Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark") + .resizable() + .frame(width: 20, height: 27) + .foregroundColor(Color.accentColor) } - .padding() - .frame(maxWidth: .infinity) - .background(Color.accentColor) - .cornerRadius(10) } - .disabled(isFetchingEpisode) - .id(buttonRefreshTrigger) - Button(action: { - libraryManager.toggleBookmark( - title: title, - imageUrl: imageUrl, - href: href, - moduleId: module.id.uuidString, - moduleName: module.metadata.sourceName - ) - }) { - Image(systemName: libraryManager.isBookmarked(href: href, moduleName: module.metadata.sourceName) ? "bookmark.fill" : "bookmark") - .resizable() - .frame(width: 20, height: 27) - .foregroundColor(Color.accentColor) - } - } - - if !episodeLinks.isEmpty { - VStack(alignment: .leading, spacing: 10) { - HStack { - Text("Episodes") - .font(.system(size: 18)) - .fontWeight(.bold) - - Spacer() - - if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { - Menu { - ForEach(generateRanges(), id: \.self) { range in - Button(action: { selectedRange = range }) { - Text("\(range.lowerBound + 1)-\(range.upperBound)") - } - } - } label: { - Text("\(selectedRange.lowerBound + 1)-\(selectedRange.upperBound)") - .font(.system(size: 14)) - .foregroundColor(.accentColor) - } - } else if isGroupedBySeasons { - let seasons = groupedEpisodes() - if seasons.count > 1 { + if !episodeLinks.isEmpty { + VStack(alignment: .leading, spacing: 10) { + HStack { + Text("Episodes") + .font(.system(size: 18)) + .fontWeight(.bold) + + Spacer() + + if !isGroupedBySeasons, episodeLinks.count > episodeChunkSize { Menu { - ForEach(0.. 1 { + Menu { + ForEach(0.. 0 ? lastPlayedTime / totalTime : 0 + + EpisodeCell( + episodeIndex: selectedSeason, + episode: ep.href, + episodeID: ep.number - 1, + progress: progress, + itemID: itemID ?? 0, + onTap: { imageUrl in + if !isFetchingEpisode { + selectedEpisodeNumber = ep.number + selectedEpisodeImage = imageUrl + fetchStream(href: ep.href) + AnalyticsManager.shared.sendEvent( + event: "watch", + additionalData: ["title": title, "episode": ep.number] + ) + } + }, + onMarkAllPrevious: { + let userDefaults = UserDefaults.standard + var updates = [String: Double]() + + for ep2 in seasons[selectedSeason] where ep2.number < ep.number { + let href = ep2.href + updates["lastPlayedTime_\(href)"] = 99999999.0 + updates["totalTime_\(href)"] = 99999999.0 + } + + for (key, value) in updates { + userDefaults.set(value, forKey: key) + } + + userDefaults.synchronize() + + refreshTrigger.toggle() + Logger.shared.log("Marked episodes watched within season \(selectedSeason + 1) of \"\(title)\".", type: "General") + } + ) + .id(refreshTrigger) + .disabled(isFetchingEpisode) + } + } else { + Text("No episodes available") + } + } else { + ForEach(episodeLinks.indices.filter { selectedRange.contains($0) }, id: \.self) { i in + let ep = episodeLinks[i] let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(ep.href)") let totalTime = UserDefaults.standard.double(forKey: "totalTime_\(ep.href)") let progress = totalTime > 0 ? lastPlayedTime / totalTime : 0 EpisodeCell( - episodeIndex: selectedSeason, + episodeIndex: i, episode: ep.href, episodeID: ep.number - 1, progress: progress, @@ -307,142 +363,148 @@ struct MediaInfoView: View { let userDefaults = UserDefaults.standard var updates = [String: Double]() - for ep2 in seasons[selectedSeason] where ep2.number < ep.number { - let href = ep2.href - updates["lastPlayedTime_\(href)"] = 99999999.0 - updates["totalTime_\(href)"] = 99999999.0 + for idx in 0.. 0 ? lastPlayedTime / totalTime : 0 - - EpisodeCell( - episodeIndex: i, - episode: ep.href, - episodeID: ep.number - 1, - progress: progress, - itemID: itemID ?? 0, - onTap: { imageUrl in - if !isFetchingEpisode { - selectedEpisodeNumber = ep.number - selectedEpisodeImage = imageUrl - fetchStream(href: ep.href) - AnalyticsManager.shared.sendEvent( - event: "watch", - additionalData: ["title": title, "episode": ep.number] - ) - } - }, - onMarkAllPrevious: { - let userDefaults = UserDefaults.standard - var updates = [String: Double]() - - for idx in 0.. 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -599,6 +666,8 @@ struct MediaInfoView: View { } } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, softsub: true, module: module) { result in + guard self.activeFetchID == fetchID else { return } + if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -614,6 +683,8 @@ struct MediaInfoView: View { } } else { jsController.fetchStreamUrl(episodeUrl: href, softsub: true, module: module) { result in + guard self.activeFetchID == fetchID else { return } + if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -631,6 +702,8 @@ struct MediaInfoView: View { } else { if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: href, module: module) { result in + guard self.activeFetchID == fetchID else { return } + if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -646,6 +719,8 @@ struct MediaInfoView: View { } } else if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: href, module: module) { result in + guard self.activeFetchID == fetchID else { return } + if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -661,6 +736,8 @@ struct MediaInfoView: View { } } else { jsController.fetchStreamUrl(episodeUrl: href, module: module) { result in + guard self.activeFetchID == fetchID else { return } + if let streams = result.streams, !streams.isEmpty { if streams.count > 1 { self.showStreamSelectionAlert(streams: streams, fullURL: href, subtitles: result.subtitles?.first) @@ -687,6 +764,8 @@ struct MediaInfoView: View { } func handleStreamFailure(error: Error? = nil) { + self.isFetchingEpisode = false + self.showStreamLoadingView = false if let error = error { Logger.shared.log("Error loading module: \(error)", type: "Error") AnalyticsManager.shared.sendEvent(event: "error", additionalData: ["error": error, "message": "Failed to fetch stream"]) @@ -698,6 +777,8 @@ struct MediaInfoView: View { } func showStreamSelectionAlert(streams: [String], fullURL: String, subtitles: String? = nil) { + self.isFetchingEpisode = false + self.showStreamLoadingView = false DispatchQueue.main.async { let alert = UIAlertController(title: "Select Server", message: "Choose a server to play from", preferredStyle: .actionSheet) @@ -760,6 +841,8 @@ struct MediaInfoView: View { } func playStream(url: String, fullURL: String, subtitles: String? = nil) { + self.isFetchingEpisode = false + self.showStreamLoadingView = false DispatchQueue.main.async { let externalPlayer = UserDefaults.standard.string(forKey: "externalPlayer") ?? "Sora" var scheme: String?