diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index bc3042f..37ba4c1 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -8,32 +8,6 @@ import SwiftUI import UIKit -// Add missing extension for UserDefaults -extension UserDefaults { - func color(forKey key: String) -> UIColor? { - guard let colorData = data(forKey: key) else { return nil } - do { - return try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: colorData) - } catch { - return nil - } - } - - func set(_ color: UIColor?, forKey key: String) { - guard let color = color else { - removeObject(forKey: key) - return - } - - do { - let data = try NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: false) - set(data, forKey: key) - } catch { - print("Error archiving color: \(error)") - } - } -} - @main struct SoraApp: App { @StateObject private var settings = Settings() @@ -43,7 +17,6 @@ struct SoraApp: App { @StateObject private var jsController = JSController.shared init() { - // Initialize caching systems _ = MetadataCacheManager.shared _ = KingfisherCacheManager.shared diff --git a/Sora/Utils/ContinueWatching/DownloadManager.swift b/Sora/Utils/DownloadUtils/DownloadManager.swift similarity index 100% rename from Sora/Utils/ContinueWatching/DownloadManager.swift rename to Sora/Utils/DownloadUtils/DownloadManager.swift diff --git a/Sora/Utils/Extensions/UserDefaults.swift b/Sora/Utils/Extensions/UserDefaults.swift index 30c82a6..5c27e66 100644 --- a/Sora/Utils/Extensions/UserDefaults.swift +++ b/Sora/Utils/Extensions/UserDefaults.swift @@ -2,7 +2,7 @@ // UserDefaults.swift // Sulfur // -// Created by Francesco on 11/05/25. +// Created by Francesco on 23/05/25. // import UIKit diff --git a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift index d6737d8..1dbfc78 100644 --- a/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift +++ b/Sora/Utils/MediaPlayer/CustomPlayer/CustomPlayer.swift @@ -1139,24 +1139,23 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele } private func setupLockButton() { - // copy dim-button styling - let cfg = UIImage.SymbolConfiguration(pointSize: 24, weight: .regular) - lockButton = UIButton(type: .system) - lockButton.setImage( - UIImage(systemName: "lock.open.fill", withConfiguration: cfg), - for: .normal - ) - lockButton.tintColor = .white - lockButton.layer.shadowColor = UIColor.black.cgColor - lockButton.layer.shadowOffset = CGSize(width: 0, height: 2) - lockButton.layer.shadowOpacity = 0.6 - lockButton.layer.shadowRadius = 4 - lockButton.layer.masksToBounds = false - - lockButton.addTarget(self, action: #selector(lockTapped), for: .touchUpInside) - - view.addSubview(lockButton) - lockButton.translatesAutoresizingMaskIntoConstraints = false + let cfg = UIImage.SymbolConfiguration(pointSize: 24, weight: .regular) + lockButton = UIButton(type: .system) + lockButton.setImage( + UIImage(systemName: "lock.open.fill", withConfiguration: cfg), + for: .normal + ) + lockButton.tintColor = .white + lockButton.layer.shadowColor = UIColor.black.cgColor + lockButton.layer.shadowOffset = CGSize(width: 0, height: 2) + lockButton.layer.shadowOpacity = 0.6 + lockButton.layer.shadowRadius = 4 + lockButton.layer.masksToBounds = false + + lockButton.addTarget(self, action: #selector(lockTapped), for: .touchUpInside) + + view.addSubview(lockButton) + lockButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ lockButton.topAnchor.constraint(equalTo: volumeSliderHostingView!.bottomAnchor, constant: 60), lockButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor), @@ -1531,34 +1530,33 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele updateSkipButtonsVisibility() return } - + if isDimmed { - // show the dim button - dimButton.isHidden = false - dimButton.alpha = 1.0 - dimButtonTimer?.invalidate() - dimButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in - UIView.animate(withDuration: 0.3) { - self?.dimButton.alpha = 0 - } + dimButton.isHidden = false + dimButton.alpha = 1.0 + dimButtonTimer?.invalidate() + dimButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in + UIView.animate(withDuration: 0.3) { + self?.dimButton.alpha = 0 } - - updateSkipButtonsVisibility() - return } - + + updateSkipButtonsVisibility() + return + } + isControlsVisible.toggle() - UIView.animate(withDuration: 0.2) { - let alpha: CGFloat = self.isControlsVisible ? 1.0 : 0.0 - self.controlsContainerView.alpha = alpha - self.skip85Button.alpha = alpha - self.lockButton.alpha = alpha // Fade lock button with controls - self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible - self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible - self.view.layoutIfNeeded() - } - updateSkipButtonsVisibility() - } + UIView.animate(withDuration: 0.2) { + let alpha: CGFloat = self.isControlsVisible ? 1.0 : 0.0 + self.controlsContainerView.alpha = alpha + self.skip85Button.alpha = alpha + self.lockButton.alpha = alpha + self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible + self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible + self.view.layoutIfNeeded() + } + updateSkipButtonsVisibility() + } @objc func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) { if gesture.state == .began { @@ -1645,10 +1643,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele @objc private func lockTapped() { controlsLocked.toggle() - + isControlsVisible = !controlsLocked lockButtonTimer?.invalidate() - + if controlsLocked { UIView.animate(withDuration: 0.25) { self.controlsContainerView.alpha = 0 @@ -1658,13 +1656,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.skipOutroButton.alpha = 0 self.skip85Button.alpha = 0 self.lockButton.alpha = 0 - + self.subtitleBottomToSafeAreaConstraint?.isActive = true self.subtitleBottomToSliderConstraint?.isActive = false - + self.view.layoutIfNeeded() } - + lockButton.setImage(UIImage(systemName: "lock.fill"), for: .normal) } else { @@ -1672,13 +1670,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele self.controlsContainerView.alpha = 1 self.dimButton.alpha = 1 for v in self.controlsToHide { v.alpha = 1 } - + self.subtitleBottomToSafeAreaConstraint?.isActive = false self.subtitleBottomToSliderConstraint?.isActive = true - + self.view.layoutIfNeeded() } - + lockButton.setImage(UIImage(systemName: "lock.open.fill"), for: .normal) updateSkipButtonsVisibility() } @@ -1733,14 +1731,14 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele for v in self.controlsToHide { v.alpha = self.isDimmed ? 0 : 1 } self.dimButton.alpha = self.isDimmed ? 0 : 1 self.lockButton.alpha = self.isDimmed ? 0 : 1 - + // switch subtitle constraints just like toggleControls() self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible - + self.view.layoutIfNeeded() } - + // slide the dim-icon over dimButtonToSlider.isActive = !isDimmed dimButtonToRight.isActive = isDimmed diff --git a/Sora/Views/DownloadView.swift b/Sora/Views/DownloadView.swift index 9168771..0d5584a 100644 --- a/Sora/Views/DownloadView.swift +++ b/Sora/Views/DownloadView.swift @@ -28,7 +28,6 @@ struct DownloadView: View { var body: some View { NavigationView { VStack(spacing: 0) { - // Tab selector Picker("Download Status", selection: $selectedTab) { Text("Active").tag(0) Text("Downloaded").tag(1) @@ -37,10 +36,9 @@ struct DownloadView: View { .padding(.horizontal) .padding(.top, 8) - // Content if selectedTab == 0 { activeDownloadsView - } else { + } else { downloadedContentView } } @@ -69,10 +67,9 @@ struct DownloadView: View { Text("Are you sure you want to delete '\(asset.episodeDisplayName)'?") } } - } - } + } + } - // MARK: - Active Downloads View private var activeDownloadsView: some View { Group { if jsController.activeDownloads.isEmpty && jsController.downloadQueue.isEmpty { @@ -80,13 +77,11 @@ struct DownloadView: View { } else { ScrollView { LazyVStack(spacing: 12) { - // Show queued downloads first ForEach(jsController.downloadQueue) { download in ActiveDownloadCard(download: download) .padding(.horizontal) } - // Then show active downloads ForEach(jsController.activeDownloads) { download in ActiveDownloadCard(download: download) .padding(.horizontal) @@ -98,33 +93,31 @@ struct DownloadView: View { } } - // MARK: - Downloaded Content View private var downloadedContentView: some View { Group { if filteredAndSortedAssets.isEmpty { emptyDownloadsView } else { - ScrollView { + ScrollView { LazyVStack(spacing: 12) { ForEach(groupedAssets, id: \.title) { group in DownloadGroupCard( - group: group, + group: group, onDelete: { asset in assetToDelete = asset showDeleteAlert = true }, onPlay: playAsset ) - .padding(.horizontal) - } - } + .padding(.horizontal) + } + } .padding(.vertical) } } } } - // MARK: - Empty States private var emptyActiveDownloadsView: some View { VStack { Image(systemName: "arrow.down.circle") @@ -165,14 +158,13 @@ struct DownloadView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } - // MARK: - Data Processing private var filteredAndSortedAssets: [DownloadedAsset] { - let filtered = searchText.isEmpty - ? jsController.savedAssets - : jsController.savedAssets.filter { asset in - asset.name.localizedCaseInsensitiveContains(searchText) || - (asset.metadata?.showTitle?.localizedCaseInsensitiveContains(searchText) ?? false) - } + let filtered = searchText.isEmpty + ? jsController.savedAssets + : jsController.savedAssets.filter { asset in + asset.name.localizedCaseInsensitiveContains(searchText) || + (asset.metadata?.showTitle?.localizedCaseInsensitiveContains(searchText) ?? false) + } switch sortOption { case .newest: @@ -198,80 +190,72 @@ struct DownloadView: View { }.sorted { $0.title < $1.title } } - // MARK: - Actions private func playAsset(_ asset: DownloadedAsset) { - // Verify file exists guard jsController.verifyAssetFileExists(asset) else { return } - // Determine stream type let streamType = asset.localURL.pathExtension.lowercased() == "mp4" ? "mp4" : "hls" - // Create dummy metadata for player - let dummyMetadata = ModuleMetadata( - sourceName: "", - author: ModuleMetadata.Author(name: "", icon: ""), - iconUrl: "", - version: "", - language: "", - baseUrl: "", + let dummyMetadata = ModuleMetadata( + sourceName: "", + author: ModuleMetadata.Author(name: "", icon: ""), + iconUrl: "", + version: "", + language: "", + baseUrl: "", streamType: streamType, - quality: "", - searchBaseUrl: "", - scriptUrl: "", - asyncJS: nil, - streamAsyncJS: nil, - softsub: nil, - multiStream: nil, - multiSubs: nil, - type: nil - ) + quality: "", + searchBaseUrl: "", + scriptUrl: "", + asyncJS: nil, + streamAsyncJS: nil, + softsub: nil, + multiStream: nil, + multiSubs: nil, + type: nil + ) + + let dummyModule = ScrapingModule( + metadata: dummyMetadata, + localPath: "", + metadataUrl: "" + ) + + if streamType == "mp4" { + let playerItem = AVPlayerItem(url: asset.localURL) + let player = AVPlayer(playerItem: playerItem) + let playerController = AVPlayerViewController() + playerController.player = player - let dummyModule = ScrapingModule( - metadata: dummyMetadata, - localPath: "", - metadataUrl: "" - ) - - // Present player - if streamType == "mp4" { - // Use system player for MP4 - let playerItem = AVPlayerItem(url: asset.localURL) - let player = AVPlayer(playerItem: playerItem) - let playerController = AVPlayerViewController() - playerController.player = player - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(playerController, animated: true) { - player.play() - } + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(playerController, animated: true) { + player.play() } - } else { - // Use custom player for HLS - let customPlayer = CustomMediaPlayerViewController( - module: dummyModule, - urlString: asset.localURL.absoluteString, - fullUrl: asset.originalURL.absoluteString, - title: asset.name, - episodeNumber: asset.metadata?.episode ?? 0, - onWatchNext: {}, + } + } else { + let customPlayer = CustomMediaPlayerViewController( + module: dummyModule, + urlString: asset.localURL.absoluteString, + fullUrl: asset.originalURL.absoluteString, + title: asset.name, + episodeNumber: asset.metadata?.episode ?? 0, + onWatchNext: {}, subtitlesURL: asset.localSubtitleURL?.absoluteString, - aniListID: 0, - episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "", - headers: nil - ) - - customPlayer.modalPresentationStyle = UIModalPresentationStyle.fullScreen - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let rootViewController = windowScene.windows.first?.rootViewController { - rootViewController.present(customPlayer, animated: true) - } + aniListID: 0, + episodeImageUrl: asset.metadata?.posterURL?.absoluteString ?? "", + headers: nil + ) + + customPlayer.modalPresentationStyle = UIModalPresentationStyle.fullScreen + + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController { + rootViewController.present(customPlayer, animated: true) } } } - -// MARK: - Supporting Types +} + struct SimpleDownloadGroup { let title: String let assets: [DownloadedAsset] @@ -279,12 +263,10 @@ struct SimpleDownloadGroup { var assetCount: Int { assets.count } var totalFileSize: Int64 { - // Simple summation without complex caching to avoid navigation issues assets.reduce(0) { $0 + $1.fileSize } } } -// MARK: - ActiveDownloadCard struct ActiveDownloadCard: View { let download: JSActiveDownload @State private var currentProgress: Double @@ -298,7 +280,6 @@ struct ActiveDownloadCard: View { var body: some View { HStack(spacing: 12) { - // Thumbnail if let imageURL = download.imageURL { KFImage(imageURL) .placeholder { @@ -316,13 +297,11 @@ struct ActiveDownloadCard: View { .cornerRadius(8) } - // Content VStack(alignment: .leading, spacing: 6) { Text(download.title ?? download.originalURL.lastPathComponent) .font(.headline) .lineLimit(1) - // Progress VStack(alignment: .leading, spacing: 4) { if download.queueStatus == .queued { ProgressView() @@ -344,47 +323,46 @@ struct ActiveDownloadCard: View { .font(.caption) .foregroundColor(.secondary) } - - Spacer() - - if taskState == .running { - Text("Downloading") - .font(.caption) - .foregroundColor(.blue) - } else if taskState == .suspended { - Text("Paused") - .font(.caption) - .foregroundColor(.orange) - } + + Spacer() + + if taskState == .running { + Text("Downloading") + .font(.caption) + .foregroundColor(.blue) + } else if taskState == .suspended { + Text("Paused") + .font(.caption) + .foregroundColor(.orange) } } } + } Spacer() - // Controls HStack(spacing: 8) { - if download.queueStatus == .queued { + if download.queueStatus == .queued { Button(action: cancelDownload) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - .font(.title2) - } - } else { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + .font(.title2) + } + } else { Button(action: toggleDownload) { - Image(systemName: taskState == .running ? "pause.circle.fill" : "play.circle.fill") - .foregroundColor(taskState == .running ? .orange : .blue) - .font(.title2) - } - + Image(systemName: taskState == .running ? "pause.circle.fill" : "play.circle.fill") + .foregroundColor(taskState == .running ? .orange : .blue) + .font(.title2) + } + Button(action: cancelDownload) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - .font(.title2) + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + .font(.title2) + } } } } - } .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(12) @@ -423,7 +401,6 @@ struct ActiveDownloadCard: View { } } -// MARK: - DownloadGroupCard struct DownloadGroupCard: View { let group: SimpleDownloadGroup let onDelete: (DownloadedAsset) -> Void @@ -432,7 +409,6 @@ struct DownloadGroupCard: View { var body: some View { NavigationLink(destination: ShowEpisodesView(group: group, onDelete: onDelete, onPlay: onPlay)) { HStack(spacing: 12) { - // Poster if let posterURL = group.posterURL { KFImage(posterURL) .placeholder { @@ -443,14 +419,13 @@ struct DownloadGroupCard: View { .aspectRatio(contentMode: .fill) .frame(width: 50, height: 75) .cornerRadius(6) - } else { + } else { Rectangle() .fill(Color.gray.opacity(0.3)) .frame(width: 50, height: 75) .cornerRadius(6) } - // Info VStack(alignment: .leading, spacing: 4) { Text(group.title) .font(.headline) @@ -467,7 +442,6 @@ struct DownloadGroupCard: View { Spacer() - // Navigation chevron Image(systemName: "chevron.right") .foregroundColor(.gray) .font(.caption) @@ -484,7 +458,6 @@ struct DownloadGroupCard: View { } } -// MARK: - EpisodeRow struct EpisodeRow: View { let asset: DownloadedAsset let onDelete: () -> Void @@ -492,7 +465,6 @@ struct EpisodeRow: View { var body: some View { HStack(spacing: 12) { - // Thumbnail if let backdropURL = asset.metadata?.backdropURL { KFImage(backdropURL) .placeholder { @@ -510,7 +482,6 @@ struct EpisodeRow: View { .cornerRadius(6) } - // Info VStack(alignment: .leading, spacing: 2) { Text(asset.episodeDisplayName) .font(.subheadline) @@ -521,28 +492,27 @@ struct EpisodeRow: View { .font(.caption) .foregroundColor(.secondary) - if asset.localSubtitleURL != nil { - Image(systemName: "captions.bubble") - .foregroundColor(.blue) - .font(.caption) - } - - if !asset.fileExists { - Image(systemName: "exclamationmark.triangle") - .foregroundColor(.orange) - .font(.caption) + if asset.localSubtitleURL != nil { + Image(systemName: "captions.bubble") + .foregroundColor(.blue) + .font(.caption) + } + + if !asset.fileExists { + Image(systemName: "exclamationmark.triangle") + .foregroundColor(.orange) + .font(.caption) } } } Spacer() - // Play button Button(action: onPlay) { - Image(systemName: "play.circle.fill") - .foregroundColor(asset.fileExists ? .blue : .gray) - .font(.title3) - } + Image(systemName: "play.circle.fill") + .foregroundColor(asset.fileExists ? .blue : .gray) + .font(.title3) + } .disabled(!asset.fileExists) } .padding(.horizontal, 12) @@ -562,7 +532,6 @@ struct EpisodeRow: View { } } -// MARK: - ShowEpisodesView struct ShowEpisodesView: View { let group: SimpleDownloadGroup let onDelete: (DownloadedAsset) -> Void @@ -572,10 +541,8 @@ struct ShowEpisodesView: View { @State private var assetToDelete: DownloadedAsset? @EnvironmentObject var jsController: JSController - // Episode sorting state @State private var episodeSortOption: EpisodeSortOption = .downloadDate - // Episode sort options enum enum EpisodeSortOption: String, CaseIterable, Identifiable { case downloadDate = "Download Date" case episodeOrder = "Episode Order" @@ -592,7 +559,6 @@ struct ShowEpisodesView: View { } } - // Computed property for sorted episodes private var sortedEpisodes: [DownloadedAsset] { switch episodeSortOption { case .downloadDate: @@ -605,9 +571,7 @@ struct ShowEpisodesView: View { var body: some View { ScrollView { VStack(spacing: 20) { - // Header with poster and info HStack(alignment: .top, spacing: 16) { - // Larger poster image if let posterURL = group.posterURL { KFImage(posterURL) .placeholder { @@ -625,7 +589,6 @@ struct ShowEpisodesView: View { .cornerRadius(10) } - // Show info VStack(alignment: .leading, spacing: 8) { Text(group.title) .font(.title2) @@ -636,9 +599,9 @@ struct ShowEpisodesView: View { .font(.headline) .foregroundColor(.secondary) - Text(formatFileSize(group.totalFileSize)) - .font(.subheadline) - .foregroundColor(.secondary) + Text(formatFileSize(group.totalFileSize)) + .font(.subheadline) + .foregroundColor(.secondary) Spacer() } @@ -646,7 +609,6 @@ struct ShowEpisodesView: View { } .padding(.horizontal) - // Episodes section VStack(alignment: .leading, spacing: 16) { HStack { Text("Episodes") @@ -655,7 +617,6 @@ struct ShowEpisodesView: View { Spacer() - // Sort toggle menu Menu { ForEach(EpisodeSortOption.allCases) { option in Button(action: { @@ -688,7 +649,6 @@ struct ShowEpisodesView: View { } .padding(.horizontal) - // Episodes list if group.assets.isEmpty { Text("No episodes available") .foregroundColor(.gray) @@ -699,27 +659,27 @@ struct ShowEpisodesView: View { LazyVStack(spacing: 8) { ForEach(sortedEpisodes) { asset in DetailedEpisodeRow(asset: asset) - .padding(.horizontal) - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(10) - .padding(.horizontal) - .contextMenu { + .padding(.horizontal) + .background(Color(UIColor.secondarySystemBackground)) + .cornerRadius(10) + .padding(.horizontal) + .contextMenu { Button(action: { onPlay(asset) }) { - Label("Play", systemImage: "play.fill") - } + Label("Play", systemImage: "play.fill") + } .disabled(!asset.fileExists) - - Button(role: .destructive, action: { + + Button(role: .destructive, action: { assetToDelete = asset showDeleteAlert = true }) { - Label("Delete", systemImage: "trash") + Label("Delete", systemImage: "trash") + } } - } - .onTapGesture { + .onTapGesture { onPlay(asset) } - } + } } } } @@ -764,13 +724,11 @@ struct ShowEpisodesView: View { } } -// MARK: - DetailedEpisodeRow struct DetailedEpisodeRow: View { let asset: DownloadedAsset var body: some View { HStack(spacing: 12) { - // Episode thumbnail if let backdropURL = asset.metadata?.backdropURL ?? asset.metadata?.posterURL { KFImage(backdropURL) .placeholder { @@ -788,7 +746,6 @@ struct DetailedEpisodeRow: View { .cornerRadius(8) } - // Episode info VStack(alignment: .leading, spacing: 4) { Text(asset.episodeDisplayName) .font(.headline) @@ -819,7 +776,6 @@ struct DetailedEpisodeRow: View { Spacer() - // Play button Image(systemName: "play.circle.fill") .foregroundColor(asset.fileExists ? .blue : .gray) .font(.title2) diff --git a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift index fbe34be..677e9fe 100644 --- a/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift +++ b/Sora/Views/MediaInfoView/EpisodeCell/EpisodeCell.swift @@ -25,9 +25,8 @@ struct EpisodeCell: View { var defaultBannerImage: String var module: ScrapingModule var parentTitle: String - var showPosterURL: String? // Add show poster URL for downloads + var showPosterURL: String? - // Multi-select support (Task MD-3) var isMultiSelectMode: Bool = false var isSelected: Bool = false var onSelectionChanged: ((Bool) -> Void)? @@ -51,7 +50,6 @@ struct EpisodeCell: View { @State private var lastLoggedStatus: EpisodeDownloadStatus? @State private var downloadAnimationScale: CGFloat = 1.0 - // Add retry configuration @State private var retryAttempts: Int = 0 private let maxRetryAttempts: Int = 3 private let initialBackoffDelay: TimeInterval = 1.0 @@ -62,7 +60,6 @@ struct EpisodeCell: View { @Environment(\.colorScheme) private var colorScheme @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system - // Simple download status for UI updates private var downloadStatusString: String { switch downloadStatus { case .notDownloaded: @@ -87,15 +84,14 @@ struct EpisodeCell: View { self.itemID = itemID self.totalEpisodes = totalEpisodes - // Initialize banner image based on appearance - let isLightMode = (UserDefaults.standard.string(forKey: "selectedAppearance") == "light") || - ((UserDefaults.standard.string(forKey: "selectedAppearance") == "system") && - UITraitCollection.current.userInterfaceStyle == .light) + let isLightMode = (UserDefaults.standard.string(forKey: "selectedAppearance") == "light") || + ((UserDefaults.standard.string(forKey: "selectedAppearance") == "system") && + UITraitCollection.current.userInterfaceStyle == .light) let defaultLightBanner = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" let defaultDarkBanner = "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png" - self.defaultBannerImage = defaultBannerImage.isEmpty ? - (isLightMode ? defaultLightBanner : defaultDarkBanner) : defaultBannerImage + self.defaultBannerImage = defaultBannerImage.isEmpty ? + (isLightMode ? defaultLightBanner : defaultDarkBanner) : defaultBannerImage self.module = module self.parentTitle = parentTitle @@ -109,7 +105,6 @@ struct EpisodeCell: View { var body: some View { HStack { - // Multi-select checkbox (Task MD-3) if isMultiSelectMode { Button(action: { onSelectionChanged?(!isSelected) @@ -135,30 +130,15 @@ struct EpisodeCell: View { contextMenuContent } .onAppear { - // Stagger operations for better scroll performance updateProgress() - - // Check download status when cell appears (less frequently) updateDownloadStatus() - - // Slightly delay loading episode details to prioritize smooth scrolling DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { fetchEpisodeDetails() } - // Prefetch next episodes when this one becomes visible if let totalEpisodes = totalEpisodes, episodeID + 1 < totalEpisodes { - // Prefetch the next 5 episodes when this one appears let nextEpisodeStart = episodeID + 1 let count = min(5, totalEpisodes - episodeID - 1) - - // Also prefetch images for the next few episodes - // Commented out prefetching until ImagePrefetchManager is ready - // ImagePrefetchManager.shared.prefetchEpisodeImages( - // anilistId: itemID, - // startEpisode: nextEpisodeStart, - // count: count - // ) } } .onDisappear { @@ -168,7 +148,6 @@ struct EpisodeCell: View { updateProgress() } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in - // Update download status less frequently to reduce jitter DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { updateDownloadStatus() updateProgress() @@ -198,17 +177,14 @@ struct EpisodeCell: View { } } - // MARK: - View Components - private var episodeThumbnail: some View { ZStack { if let url = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) { KFImage.optimizedEpisodeThumbnail(url: url) - // Convert back to the regular KFImage since the extension isn't available yet .setProcessor(DownsamplingImageProcessor(size: CGSize(width: 100, height: 56))) - .memoryCacheExpiration(.seconds(600)) // Increase cache duration to reduce loading + .memoryCacheExpiration(.seconds(600)) .cacheOriginalImage() - .fade(duration: 0.1) // Shorter fade for better performance + .fade(duration: 0.1) .onFailure { error in Logger.shared.log("Failed to load episode image: \(error)", type: "Error") } @@ -217,9 +193,6 @@ struct EpisodeCell: View { .aspectRatio(16/9, contentMode: .fill) .frame(width: 100, height: 56) .cornerRadius(8) - .onAppear { - // Image loading logic if needed - } } else { Rectangle() .fill(Color.gray.opacity(0.3)) @@ -299,9 +272,7 @@ struct EpisodeCell: View { .foregroundColor(.green) .font(.title3) .padding(.horizontal, 8) - // Add animation to stand out more .scaleEffect(1.1) - // Use more straightforward animation .animation(.default, value: downloadStatusString) } @@ -354,36 +325,29 @@ struct EpisodeCell: View { } private func updateDownloadStatus() { - // Check the current download status with JSController let newStatus = jsController.isEpisodeDownloadedOrInProgress( showTitle: parentTitle, episodeNumber: episodeID + 1 ) - // Only update if status actually changed to reduce unnecessary UI updates if downloadStatus != newStatus { downloadStatus = newStatus } } private func downloadEpisode() { - // Check the current download status updateDownloadStatus() - // Don't proceed if the episode is already downloaded or being downloaded if case .notDownloaded = downloadStatus, !isDownloading { isDownloading = true let downloadID = UUID() - // Use the new consolidated download notification DropManager.shared.downloadStarted(episodeNumber: episodeID + 1) Task { do { let jsContent = try moduleManager.getModuleContent(module) jsController.loadScript(jsContent) - - // Try download methods sequentially instead of in parallel tryNextDownloadMethod(methodIndex: 0, downloadID: downloadID, softsub: module.metadata.softsub == true) } catch { DropManager.shared.error("Failed to start download: \(error.localizedDescription)") @@ -391,7 +355,6 @@ struct EpisodeCell: View { } } } else { - // Handle case where download is already in progress or completed if case .downloaded = downloadStatus { DropManager.shared.info("Episode \(episodeID + 1) is already downloaded") } else if case .downloading = downloadStatus { @@ -400,7 +363,6 @@ struct EpisodeCell: View { } } - // Try each download method sequentially private func tryNextDownloadMethod(methodIndex: Int, downloadID: UUID, softsub: Bool) { if !isDownloading { return @@ -410,73 +372,60 @@ struct EpisodeCell: View { switch methodIndex { case 0: - // First try fetchStreamUrlJS if asyncJS is true if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: episode, softsub: softsub, module: module) { result in self.handleSequentialDownloadResult(result, downloadID: downloadID, methodIndex: methodIndex, softsub: softsub) } } else { - // Skip to next method if not applicable tryNextDownloadMethod(methodIndex: methodIndex + 1, downloadID: downloadID, softsub: softsub) } case 1: - // Then try fetchStreamUrlJSSecond if streamAsyncJS is true if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: episode, softsub: softsub, module: module) { result in self.handleSequentialDownloadResult(result, downloadID: downloadID, methodIndex: methodIndex, softsub: softsub) } } else { - // Skip to next method if not applicable tryNextDownloadMethod(methodIndex: methodIndex + 1, downloadID: downloadID, softsub: softsub) } case 2: - // Finally try fetchStreamUrl (most reliable method) jsController.fetchStreamUrl(episodeUrl: episode, softsub: softsub, module: module) { result in self.handleSequentialDownloadResult(result, downloadID: downloadID, methodIndex: methodIndex, softsub: softsub) } default: - // We've tried all methods and none worked DropManager.shared.error("Failed to find a valid stream for download after trying all methods") isDownloading = false } } - // Handle result from sequential download attempts private func handleSequentialDownloadResult(_ result: (streams: [String]?, subtitles: [String]?, sources: [[String:Any]]?), downloadID: UUID, methodIndex: Int, softsub: Bool) { - // Skip if we're no longer downloading if !isDownloading { return } - // Check if we have valid streams if let streams = result.streams, !streams.isEmpty, let url = URL(string: streams[0]) { - // Check if it's a Promise object if streams[0] == "[object Promise]" { print("[Download] Method #\(methodIndex+1) returned a Promise object, trying next method") tryNextDownloadMethod(methodIndex: methodIndex + 1, downloadID: downloadID, softsub: softsub) return } - // We found a valid stream URL, proceed with download print("[Download] Method #\(methodIndex+1) returned valid stream URL: \(streams[0])") - // Get subtitle URL if available let subtitleURL = result.subtitles?.first.flatMap { URL(string: $0) } if let subtitleURL = subtitleURL { print("[Download] Found subtitle URL: \(subtitleURL.absoluteString)") } startActualDownload(url: url, streamUrl: streams[0], downloadID: downloadID, subtitleURL: subtitleURL) - } else if let sources = result.sources, !sources.isEmpty, - let streamUrl = sources[0]["streamUrl"] as? String, - let url = URL(string: streamUrl) { + } else if let sources = result.sources, !sources.isEmpty, + let streamUrl = sources[0]["streamUrl"] as? String, + let url = URL(string: streamUrl) { print("[Download] Method #\(methodIndex+1) returned valid stream URL with headers: \(streamUrl)") - // Get subtitle URL if available let subtitleURLString = sources[0]["subtitle"] as? String let subtitleURL = subtitleURLString.flatMap { URL(string: $0) } if let subtitleURL = subtitleURL { @@ -485,22 +434,17 @@ struct EpisodeCell: View { startActualDownload(url: url, streamUrl: streamUrl, downloadID: downloadID, subtitleURL: subtitleURL) } else { - // No valid streams from this method, try the next one print("[Download] Method #\(methodIndex+1) did not return valid streams, trying next method") tryNextDownloadMethod(methodIndex: methodIndex + 1, downloadID: downloadID, softsub: softsub) } } - // Start the actual download process once we have a valid URL private func startActualDownload(url: URL, streamUrl: String, downloadID: UUID, subtitleURL: URL? = nil) { - // Extract base URL for headers var headers: [String: String] = [:] - // Always use the module's baseUrl for Origin and Referer if !module.metadata.baseUrl.isEmpty && !module.metadata.baseUrl.contains("undefined") { print("Using module baseUrl: \(module.metadata.baseUrl)") - // Create comprehensive headers prioritizing the module's baseUrl headers = [ "Origin": module.metadata.baseUrl, "Referer": module.metadata.baseUrl, @@ -512,7 +456,6 @@ struct EpisodeCell: View { "Sec-Fetch-Site": "same-origin" ] } else { - // Fallback to using the stream URL's domain if module.baseUrl isn't available if let scheme = url.scheme, let host = url.host { let baseUrl = scheme + "://" + host @@ -527,7 +470,6 @@ struct EpisodeCell: View { "Sec-Fetch-Site": "same-origin" ] } else { - // Missing URL components DropManager.shared.error("Invalid stream URL - missing scheme or host") isDownloading = false return @@ -536,18 +478,14 @@ struct EpisodeCell: View { print("Download headers: \(headers)") - // Use episode thumbnail for the individual episode, show poster for grouping let episodeThumbnailURL = URL(string: episodeImageUrl.isEmpty ? defaultBannerImage : episodeImageUrl) let showPosterImageURL = URL(string: showPosterURL ?? defaultBannerImage) - // Get the episode title and information let episodeName = episodeTitle.isEmpty ? "Episode \(episodeID + 1)" : episodeTitle let fullEpisodeTitle = "Episode \(episodeID + 1): \(episodeName)" - // Extract show title from the parent view let animeTitle = parentTitle.isEmpty ? "Unknown Anime" : parentTitle - // Use streamType-aware download method instead of M3U8-specific method jsController.downloadWithStreamTypeSupport( url: url, headers: headers, @@ -556,13 +494,12 @@ struct EpisodeCell: View { module: module, isEpisode: true, showTitle: animeTitle, - season: 1, // Default to season 1 if not known + season: 1, episode: episodeID + 1, subtitleURL: subtitleURL, showPosterURL: showPosterImageURL, completionHandler: { success, message in if success { - // Log the download for analytics Logger.shared.log("Started download for Episode \(self.episodeID + 1): \(self.episode)", type: "Download") AnalyticsManager.shared.sendEvent( event: "download", @@ -571,8 +508,6 @@ struct EpisodeCell: View { } else { DropManager.shared.error(message) } - - // Mark that we've handled this download self.isDownloading = false } ) @@ -606,19 +541,15 @@ struct EpisodeCell: View { } private func fetchEpisodeDetails() { - // Check if metadata caching is enabled - if MetadataCacheManager.shared.isCachingEnabled && - (UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || - UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata")) { + if MetadataCacheManager.shared.isCachingEnabled && + (UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || + UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata")) { - // Create a cache key using the anilist ID and episode number let cacheKey = "anilist_\(itemID)_episode_\(episodeID + 1)" - // Try to get from cache first if let cachedData = MetadataCacheManager.shared.getMetadata(forKey: cacheKey), let metadata = EpisodeMetadata.fromData(cachedData) { - // Successfully loaded from cache DispatchQueue.main.async { self.episodeTitle = metadata.title["en"] ?? "" self.episodeImageUrl = metadata.imageUrl @@ -629,7 +560,6 @@ struct EpisodeCell: View { } } - // Cache miss or caching disabled, fetch from network fetchAnimeEpisodeDetails() } @@ -640,7 +570,6 @@ struct EpisodeCell: View { return } - // For debugging if retryAttempts > 0 { Logger.shared.log("Retrying episode details fetch (attempt \(retryAttempts)/\(maxRetryAttempts))", type: "Debug") } @@ -664,10 +593,8 @@ struct EpisodeCell: View { return } - // Check if episodes object exists guard let episodes = json["episodes"] as? [String: Any] else { Logger.shared.log("Missing 'episodes' object in response", type: "Error") - // Still proceed with empty data rather than failing DispatchQueue.main.async { self.isLoading = false self.retryAttempts = 0 @@ -675,11 +602,9 @@ struct EpisodeCell: View { return } - // Check if this specific episode exists in the response let episodeKey = "\(episodeID + 1)" guard let episodeDetails = episodes[episodeKey] as? [String: Any] else { Logger.shared.log("Episode \(episodeKey) not found in response", type: "Error") - // Still proceed with empty data rather than failing DispatchQueue.main.async { self.isLoading = false self.retryAttempts = 0 @@ -687,7 +612,6 @@ struct EpisodeCell: View { return } - // Extract available fields, log if they're missing but continue anyway var title: [String: String] = [:] var image: String = "" var missingFields: [String] = [] @@ -695,7 +619,6 @@ struct EpisodeCell: View { if let titleData = episodeDetails["title"] as? [String: String], !titleData.isEmpty { title = titleData - // Check if we have any non-empty title values if title.values.allSatisfy({ $0.isEmpty }) { missingFields.append("title (all values empty)") } @@ -709,12 +632,10 @@ struct EpisodeCell: View { missingFields.append("image") } - // Log missing fields but continue processing if !missingFields.isEmpty { Logger.shared.log("Episode \(episodeKey) missing fields: \(missingFields.joined(separator: ", "))", type: "Warning") } - // Cache whatever metadata we have if caching is enabled if MetadataCacheManager.shared.isCachingEnabled && (!title.isEmpty || !image.isEmpty) { let metadata = EpisodeMetadata( title: title, @@ -731,17 +652,14 @@ struct EpisodeCell: View { } } - // Update UI with whatever data we have DispatchQueue.main.async { self.isLoading = false - self.retryAttempts = 0 // Reset retry counter on success (even partial) + self.retryAttempts = 0 if UserDefaults.standard.object(forKey: "fetchEpisodeMetadata") == nil || UserDefaults.standard.bool(forKey: "fetchEpisodeMetadata") { - // Use whatever title we have, or leave as empty string self.episodeTitle = title["en"] ?? title.values.first ?? "" - // Use image if available, otherwise leave current value if !image.isEmpty { self.episodeImageUrl = image } @@ -749,7 +667,6 @@ struct EpisodeCell: View { } } catch { Logger.shared.log("JSON parsing error: \(error.localizedDescription)", type: "Error") - // Still continue with empty data rather than failing DispatchQueue.main.async { self.isLoading = false self.retryAttempts = 0 @@ -762,22 +679,17 @@ struct EpisodeCell: View { Logger.shared.log("Episode details fetch error: \(error.localizedDescription)", type: "Error") DispatchQueue.main.async { - // Check if we should retry if self.retryAttempts < self.maxRetryAttempts { - // Increment retry counter self.retryAttempts += 1 - // Calculate backoff delay with exponential backoff let backoffDelay = self.initialBackoffDelay * pow(2.0, Double(self.retryAttempts - 1)) Logger.shared.log("Will retry episode details fetch in \(backoffDelay) seconds", type: "Debug") - // Schedule retry after backoff delay DispatchQueue.main.asyncAfter(deadline: .now() + backoffDelay) { self.fetchAnimeEpisodeDetails() } } else { - // Max retries reached, give up but still update UI with what we have Logger.shared.log("Failed to fetch episode details after \(self.maxRetryAttempts) attempts", type: "Error") self.isLoading = false self.retryAttempts = 0 diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index 54635c1..d23f437 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -67,7 +67,6 @@ struct MediaInfoView: View { @Environment(\.verticalSizeClass) private var verticalSizeClass @AppStorage("selectedAppearance") private var selectedAppearance: Appearance = .system - // MARK: - Multi-Download State Management (Task MD-1) @State private var isMultiSelectMode: Bool = false @State private var selectedEpisodes: Set = [] @State private var showRangeInput: Bool = false @@ -78,16 +77,15 @@ struct MediaInfoView: View { return groupedEpisodes().count > 1 } - // MARK: - Responsive Layout Properties private var isCompactLayout: Bool { return verticalSizeClass == .compact } private var useIconOnlyButtons: Bool { if UIDevice.current.userInterfaceIdiom == .pad { - return false // iPad has more space + return false } - return verticalSizeClass == .regular // Portrait mode on iPhone + return verticalSizeClass == .regular } private var multiselectButtonSpacing: CGFloat { @@ -404,7 +402,6 @@ struct MediaInfoView: View { episodeNavigationSection } - // Multi-select action bar if isMultiSelectMode && !selectedEpisodes.isEmpty { multiSelectActionBar } @@ -416,11 +413,8 @@ struct MediaInfoView: View { @ViewBuilder private var multiSelectControls: some View { if isMultiSelectMode { - // Responsive multiselect toolbar if useIconOnlyButtons { - // Compact layout for portrait mode HStack(spacing: multiselectButtonSpacing) { - // Secondary actions menu Menu { Button(action: { selectedEpisodes.removeAll() @@ -444,7 +438,6 @@ struct MediaInfoView: View { Spacer() - // Select All button (icon only) Button(action: { selectAllVisibleEpisodes() }) { @@ -456,7 +449,6 @@ struct MediaInfoView: View { .clipShape(Circle()) } - // Done button Button(action: { isMultiSelectMode = false selectedEpisodes.removeAll() @@ -473,9 +465,7 @@ struct MediaInfoView: View { .padding(.horizontal, multiselectPadding) .padding(.vertical, 8) } else { - // Expanded layout for landscape mode or iPad HStack(spacing: multiselectButtonSpacing) { - // Clear All button Button(action: { selectedEpisodes.removeAll() }) { @@ -491,7 +481,6 @@ struct MediaInfoView: View { .cornerRadius(8) } - // Range button Button(action: { showRangeInput = true }) { @@ -509,7 +498,6 @@ struct MediaInfoView: View { Spacer() - // Select All button Button(action: { selectAllVisibleEpisodes() }) { @@ -525,7 +513,6 @@ struct MediaInfoView: View { .cornerRadius(8) } - // Done button Button(action: { isMultiSelectMode = false selectedEpisodes.removeAll() @@ -545,7 +532,6 @@ struct MediaInfoView: View { .cornerRadius(12) } } else { - // Select button to enter multi-select mode HStack { Spacer() Button(action: { @@ -570,13 +556,11 @@ struct MediaInfoView: View { @ViewBuilder private var multiSelectActionBar: some View { VStack(spacing: 0) { - // Divider Rectangle() .fill(Color(UIColor.separator)) .frame(height: 0.5) HStack(spacing: 12) { - // Selection count HStack(spacing: 6) { Image(systemName: "checkmark.circle.fill") .foregroundColor(.accentColor) @@ -588,7 +572,6 @@ struct MediaInfoView: View { Spacer() if isBulkDownloading { - // Progress indicator HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) @@ -597,7 +580,6 @@ struct MediaInfoView: View { .foregroundColor(.secondary) } } else { - // Download button Button(action: { startBulkDownload() }) { @@ -705,7 +687,7 @@ struct MediaInfoView: View { markAllPreviousEpisodesAsWatched(ep: ep, inSeason: true) } ) - .disabled(isFetchingEpisode) + .disabled(isFetchingEpisode) } } else { Text("No episodes available") @@ -715,8 +697,8 @@ struct MediaInfoView: View { private func getBannerImageBasedOnAppearance() -> String { let isLightMode = selectedAppearance == .light || (selectedAppearance == .system && colorScheme == .light) return isLightMode - ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" - : "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png" + ? "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner1.png" + : "https://raw.githubusercontent.com/cranci1/Sora/refs/heads/dev/assets/banner2.png" } private func episodeTapAction(ep: EpisodeLink, imageUrl: String) { @@ -787,7 +769,7 @@ struct MediaInfoView: View { markAllPreviousEpisodesInFlatList(ep: ep, index: i) } ) - .disabled(isFetchingEpisode) + .disabled(isFetchingEpisode) } } @@ -850,7 +832,7 @@ struct MediaInfoView: View { if let finishedIndex = finished, finishedIndex < episodeLinks.count - 1 { let nextEp = episodeLinks[finishedIndex + 1] return "Start Watching Episode \(nextEp.number)" - } + } if let unfinishedIndex = unfinished { let currentEp = episodeLinks[unfinishedIndex] @@ -870,7 +852,7 @@ struct MediaInfoView: View { selectedEpisodeNumber = nextEp.number fetchStream(href: nextEp.href) return - } + } if let unfinishedIndex = unfinished { let ep = episodeLinks[unfinishedIndex] @@ -1322,8 +1304,6 @@ struct MediaInfoView: View { } } - // MARK: - Multi-Download Helper Functions (Task MD-1 & MD-4) - private func selectEpisodeRange(start: Int, end: Int) { selectedEpisodes.removeAll() for episodeNumber in start...end { @@ -1352,11 +1332,8 @@ struct MediaInfoView: View { isBulkDownloading = true bulkDownloadProgress = "Starting downloads..." - - // Convert selected episode numbers to EpisodeLink objects let episodesToDownload = episodeLinks.filter { selectedEpisodes.contains($0.number) } - // Start bulk download process Task { await processBulkDownload(episodes: episodesToDownload) } @@ -1371,7 +1348,6 @@ struct MediaInfoView: View { for (index, episode) in episodes.enumerated() { bulkDownloadProgress = "Downloading episode \(episode.number) (\(index + 1)/\(totalCount))" - // Check if episode is already downloaded or queued let downloadStatus = jsController.isEpisodeDownloadedOrInProgress( showTitle: title, episodeNumber: episode.number, @@ -1384,7 +1360,6 @@ struct MediaInfoView: View { case .downloading: Logger.shared.log("Episode \(episode.number) already downloading, skipping", type: "Info") case .notDownloaded: - // Start download for this episode let downloadSuccess = await downloadSingleEpisode(episode: episode) if downloadSuccess { successCount += 1 @@ -1393,17 +1368,14 @@ struct MediaInfoView: View { completedCount += 1 - // Small delay between downloads to avoid overwhelming the system - try? await Task.sleep(nanoseconds: 500_000_000) // 500 milliseconds = 500,000,000 nanoseconds + try? await Task.sleep(nanoseconds: 500_000_000) } - // Update UI and provide feedback isBulkDownloading = false bulkDownloadProgress = "" isMultiSelectMode = false selectedEpisodes.removeAll() - // Show completion notification DropManager.shared.showDrop( title: "Bulk Download Complete", subtitle: "\(successCount)/\(totalCount) episodes queued for download", @@ -1419,7 +1391,6 @@ struct MediaInfoView: View { let jsContent = try moduleManager.getModuleContent(module) jsController.loadScript(jsContent) - // Use the same comprehensive stream fetching approach as manual downloads self.tryNextDownloadMethodForBulk( episode: episode, methodIndex: 0, @@ -1434,7 +1405,6 @@ struct MediaInfoView: View { } } - // Replicate the same multi-method approach used in EpisodeCell for bulk downloads private func tryNextDownloadMethodForBulk( episode: EpisodeLink, methodIndex: Int, @@ -1445,61 +1415,44 @@ struct MediaInfoView: View { switch methodIndex { case 0: - // First try fetchStreamUrlJS if asyncJS is true if module.metadata.asyncJS == true { jsController.fetchStreamUrlJS(episodeUrl: episode.href, softsub: softsub, module: module) { result in self.handleBulkDownloadResult(result, episode: episode, methodIndex: methodIndex, softsub: softsub, continuation: continuation) } } else { - // Skip to next method if not applicable tryNextDownloadMethodForBulk(episode: episode, methodIndex: methodIndex + 1, softsub: softsub, continuation: continuation) } case 1: - // Then try fetchStreamUrlJSSecond if streamAsyncJS is true if module.metadata.streamAsyncJS == true { jsController.fetchStreamUrlJSSecond(episodeUrl: episode.href, softsub: softsub, module: module) { result in self.handleBulkDownloadResult(result, episode: episode, methodIndex: methodIndex, softsub: softsub, continuation: continuation) } } else { - // Skip to next method if not applicable tryNextDownloadMethodForBulk(episode: episode, methodIndex: methodIndex + 1, softsub: softsub, continuation: continuation) } case 2: - // Finally try fetchStreamUrl (most reliable method) jsController.fetchStreamUrl(episodeUrl: episode.href, softsub: softsub, module: module) { result in self.handleBulkDownloadResult(result, episode: episode, methodIndex: methodIndex, softsub: softsub, continuation: continuation) } default: - // We've tried all methods and none worked Logger.shared.log("Failed to find a valid stream for bulk download after trying all methods", type: "Error") continuation.resume(returning: false) } } - // Handle result from sequential download attempts (same logic as EpisodeCell) - private func handleBulkDownloadResult( - _ result: (streams: [String]?, subtitles: [String]?, sources: [[String:Any]]?), - episode: EpisodeLink, - methodIndex: Int, - softsub: Bool, - continuation: CheckedContinuation - ) { - // Check if we have valid streams + private func handleBulkDownloadResult(_ result: (streams: [String]?, subtitles: [String]?, sources: [[String:Any]]?), episode: EpisodeLink, methodIndex: Int, softsub: Bool, continuation: CheckedContinuation) { if let streams = result.streams, !streams.isEmpty, let url = URL(string: streams[0]) { - // Check if it's a Promise object if streams[0] == "[object Promise]" { print("[Bulk Download] Method #\(methodIndex+1) returned a Promise object, trying next method") tryNextDownloadMethodForBulk(episode: episode, methodIndex: methodIndex + 1, softsub: softsub, continuation: continuation) return } - // We found a valid stream URL, proceed with download print("[Bulk Download] Method #\(methodIndex+1) returned valid stream URL: \(streams[0])") - // Get subtitle URL if available let subtitleURL = result.subtitles?.first.flatMap { URL(string: $0) } if let subtitleURL = subtitleURL { print("[Bulk Download] Found subtitle URL: \(subtitleURL.absoluteString)") @@ -1508,13 +1461,12 @@ struct MediaInfoView: View { startEpisodeDownloadWithProcessedStream(episode: episode, url: url, streamUrl: streams[0], subtitleURL: subtitleURL) continuation.resume(returning: true) - } else if let sources = result.sources, !sources.isEmpty, - let streamUrl = sources[0]["streamUrl"] as? String, - let url = URL(string: streamUrl) { + } else if let sources = result.sources, !sources.isEmpty, + let streamUrl = sources[0]["streamUrl"] as? String, + let url = URL(string: streamUrl) { print("[Bulk Download] Method #\(methodIndex+1) returned valid stream URL with headers: \(streamUrl)") - // Get subtitle URL if available let subtitleURLString = sources[0]["subtitle"] as? String let subtitleURL = subtitleURLString.flatMap { URL(string: $0) } if let subtitleURL = subtitleURL { @@ -1525,27 +1477,17 @@ struct MediaInfoView: View { continuation.resume(returning: true) } else { - // No valid streams from this method, try the next one print("[Bulk Download] Method #\(methodIndex+1) did not return valid streams, trying next method") tryNextDownloadMethodForBulk(episode: episode, methodIndex: methodIndex + 1, softsub: softsub, continuation: continuation) } } - // Start download with processed stream URL and proper headers (same logic as EpisodeCell) - private func startEpisodeDownloadWithProcessedStream( - episode: EpisodeLink, - url: URL, - streamUrl: String, - subtitleURL: URL? = nil - ) { - // Generate comprehensive headers using the same logic as EpisodeCell + private func startEpisodeDownloadWithProcessedStream(episode: EpisodeLink, url: URL, streamUrl: String, subtitleURL: URL? = nil) { var headers: [String: String] = [:] - // Always use the module's baseUrl for Origin and Referer if !module.metadata.baseUrl.isEmpty && !module.metadata.baseUrl.contains("undefined") { print("Using module baseUrl: \(module.metadata.baseUrl)") - // Create comprehensive headers prioritizing the module's baseUrl headers = [ "Origin": module.metadata.baseUrl, "Referer": module.metadata.baseUrl, @@ -1557,7 +1499,6 @@ struct MediaInfoView: View { "Sec-Fetch-Site": "same-origin" ] } else { - // Fallback to using the stream URL's domain if module.baseUrl isn't available if let scheme = url.scheme, let host = url.host { let baseUrl = scheme + "://" + host @@ -1572,7 +1513,6 @@ struct MediaInfoView: View { "Sec-Fetch-Site": "same-origin" ] } else { - // Missing URL components - use minimal headers headers = [ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" ] @@ -1581,17 +1521,13 @@ struct MediaInfoView: View { } print("Bulk download headers: \(headers)") - - // Fetch episode metadata first (same as EpisodeCell does) fetchEpisodeMetadataForDownload(episode: episode) { metadata in let episodeTitle = metadata?.title["en"] ?? metadata?.title.values.first ?? "" let episodeImageUrl = metadata?.imageUrl ?? "" - // Create episode title using same logic as EpisodeCell let episodeName = episodeTitle.isEmpty ? "Episode \(episode.number)" : episodeTitle let fullEpisodeTitle = "Episode \(episode.number): \(episodeName)" - // Use episode-specific thumbnail if available, otherwise use default banner let episodeThumbnailURL: URL? if !episodeImageUrl.isEmpty { episodeThumbnailURL = URL(string: episodeImageUrl) @@ -1626,16 +1562,13 @@ struct MediaInfoView: View { } } - // Fetch episode metadata for downloads (same logic as EpisodeCell.fetchEpisodeDetails) private func fetchEpisodeMetadataForDownload(episode: EpisodeLink, completion: @escaping (EpisodeMetadataInfo?) -> Void) { - // Check if we have an itemID for metadata fetching guard let anilistId = itemID else { Logger.shared.log("No AniList ID available for episode metadata", type: "Warning") completion(nil) return } - // Check if metadata caching is enabled and try cache first if MetadataCacheManager.shared.isCachingEnabled { let cacheKey = "anilist_\(anilistId)_episode_\(episode.number)" @@ -1654,11 +1587,9 @@ struct MediaInfoView: View { } } - // Cache miss or caching disabled, fetch from network fetchEpisodeMetadataFromNetwork(anilistId: anilistId, episodeNumber: episode.number, completion: completion) } - // Fetch episode metadata from ani.zip API (same logic as EpisodeCell.fetchAnimeEpisodeDetails) private func fetchEpisodeMetadataFromNetwork(anilistId: Int, episodeNumber: Int, completion: @escaping (EpisodeMetadataInfo?) -> Void) { guard let url = URL(string: "https://api.ani.zip/mappings?anilist_id=\(anilistId)") else { Logger.shared.log("Invalid URL for anilistId: \(anilistId)", type: "Error") @@ -1689,14 +1620,12 @@ struct MediaInfoView: View { return } - // Check if episodes object exists guard let episodes = json["episodes"] as? [String: Any] else { Logger.shared.log("Missing 'episodes' object in metadata response", type: "Error") completion(nil) return } - // Check if this specific episode exists in the response let episodeKey = "\(episodeNumber)" guard let episodeDetails = episodes[episodeKey] as? [String: Any] else { Logger.shared.log("Episode \(episodeKey) not found in metadata response", type: "Warning") @@ -1704,23 +1633,18 @@ struct MediaInfoView: View { return } - // Extract available fields var title: [String: String] = [:] var image: String = "" if let titleData = episodeDetails["title"] as? [String: String], !titleData.isEmpty { title = titleData } else { - // Use default title if none available title = ["en": "Episode \(episodeNumber)"] } if let imageUrl = episodeDetails["image"] as? String, !imageUrl.isEmpty { image = imageUrl } - // If no image, leave empty and let the caller use default banner - - // Cache whatever metadata we have if caching is enabled if MetadataCacheManager.shared.isCachingEnabled { let metadata = EpisodeMetadata( title: title, @@ -1738,7 +1662,6 @@ struct MediaInfoView: View { } } - // Create metadata info object let metadataInfo = EpisodeMetadataInfo( title: title, imageUrl: image, @@ -1757,7 +1680,6 @@ struct MediaInfoView: View { } } -// MARK: - Range Selection Sheet (Task MD-5) struct RangeSelectionSheet: View { let totalEpisodes: Int let onSelectionComplete: (Int, Int) -> Void @@ -1871,10 +1793,10 @@ struct RangeSelectionSheet: View { private func validateAndSelect() { guard let start = Int(startEpisode), let end = Int(endEpisode) else { - errorMessage = "Please enter valid episode numbers" - showError = true - return - } + errorMessage = "Please enter valid episode numbers" + showError = true + return + } guard start >= 1 && end <= totalEpisodes else { errorMessage = "Episode numbers must be between 1 and \(totalEpisodes)" diff --git a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift index afaaf29..4ffdc9f 100644 --- a/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift +++ b/Sora/Views/SettingsView/SettingsSubViews/SettingsViewDownloads.swift @@ -8,11 +8,9 @@ import SwiftUI import Drops -// No need to import DownloadQualityPreference as it's in the same module - struct SettingsViewDownloads: View { @EnvironmentObject private var jsController: JSController - @AppStorage(DownloadQualityPreference.userDefaultsKey) + @AppStorage(DownloadQualityPreference.userDefaultsKey) private var downloadQuality = DownloadQualityPreference.defaultPreference.rawValue @AppStorage("allowCellularDownloads") private var allowCellularDownloads: Bool = true @AppStorage("maxConcurrentDownloads") private var maxConcurrentDownloads: Int = 3 @@ -39,7 +37,6 @@ struct SettingsViewDownloads: View { Spacer() Stepper("\(maxConcurrentDownloads)", value: $maxConcurrentDownloads, in: 1...10) .onChange(of: maxConcurrentDownloads) { newValue in - // Update JSController when the setting changes jsController.updateMaxConcurrentDownloads(newValue) } } @@ -79,7 +76,6 @@ struct SettingsViewDownloads: View { } Button(action: { - // Recalculate sizes in case files were externally modified calculateTotalStorage() }) { HStack { @@ -114,7 +110,6 @@ struct SettingsViewDownloads: View { .navigationTitle("Downloads") .onAppear { calculateTotalStorage() - // Sync the max concurrent downloads setting with JSController jsController.updateMaxConcurrentDownloads(maxConcurrentDownloads) } } @@ -128,11 +123,9 @@ struct SettingsViewDownloads: View { isCalculating = true - // Clear any cached file sizes before recalculating DownloadedAsset.clearFileSizeCache() DownloadGroup.clearFileSizeCache() - // Use background task to avoid UI freezes with many files DispatchQueue.global(qos: .userInitiated).async { let total = jsController.savedAssets.reduce(0) { $0 + $1.fileSize } let existing = jsController.savedAssets.filter { $0.fileExists }.count @@ -149,22 +142,17 @@ struct SettingsViewDownloads: View { let assetsToDelete = jsController.savedAssets for asset in assetsToDelete { if preservePersistentDownloads { - // Only remove from library without deleting files jsController.removeAssetFromLibrary(asset) } else { - // Delete both library entry and files jsController.deleteAsset(asset) } } - // Reset calculated values totalStorageSize = 0 existingDownloadCount = 0 - // Post a notification so all views can update - use libraryChange since assets were deleted NotificationCenter.default.post(name: NSNotification.Name("downloadLibraryChanged"), object: nil) - // Show confirmation message DispatchQueue.main.async { if preservePersistentDownloads { DropManager.shared.success("Library cleared successfully") @@ -180,4 +168,4 @@ struct SettingsViewDownloads: View { formatter.countStyle = .file return formatter.string(fromByteCount: size) } -} \ No newline at end of file +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index e5f40d1..8e88296 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -17,8 +17,6 @@ 132AF1212D99951700A0140B /* JSController-Streams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1202D99951700A0140B /* JSController-Streams.swift */; }; 132AF1232D9995C300A0140B /* JSController-Details.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1222D9995C300A0140B /* JSController-Details.swift */; }; 132AF1252D9995F900A0140B /* JSController-Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132AF1242D9995F900A0140B /* JSController-Search.swift */; }; - 132E351D2D959DDB0007800E /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 132E351C2D959DDB0007800E /* Drops */; }; - 132E35232D959E410007800E /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 132E35222D959E410007800E /* Kingfisher */; }; 133D7C6E2D2BE2500075467E /* SoraApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6D2D2BE2500075467E /* SoraApp.swift */; }; 133D7C702D2BE2500075467E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133D7C6F2D2BE2500075467E /* ContentView.swift */; }; 133D7C722D2BE2520075467E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 133D7C712D2BE2520075467E /* Assets.xcassets */; }; @@ -34,13 +32,16 @@ 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 133F55BA2D33B55100E08EEA /* LibraryManager.swift */; }; 1359ED142D76F49900C13034 /* finTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1359ED132D76F49900C13034 /* finTopView.swift */; }; 135CCBE22D4D1138008B9C0E /* SettingsViewPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */; }; + 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */; }; + 13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */; }; + 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B8F2DE0ECD200BDA2FC /* Drops */; }; + 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */; }; 136BBE802DB1038000906B5E /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136BBE7F2DB1038000906B5E /* Notification+Name.swift */; }; 138AA1B82D2D66FD0021F9DF /* EpisodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */; }; 138AA1B92D2D66FD0021F9DF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */; }; 139935662D468C450065CEFF /* ModuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 139935652D468C450065CEFF /* ModuleManager.swift */; }; 1399FAD42D3AB38C00E97C31 /* SettingsViewLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */; }; 1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1399FAD52D3AB3DB00E97C31 /* Logger.swift */; }; - 13B77E192DA44F8300126FDF /* MarqueeLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 13B77E182DA44F8300126FDF /* MarqueeLabel */; }; 13B77E202DA457AA00126FDF /* AniListPushUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B77E1F2DA457AA00126FDF /* AniListPushUpdates.swift */; }; 13B7F4C12D58FFDD0045714A /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13B7F4C02D58FFDD0045714A /* Shimmer.swift */; }; 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */; }; @@ -112,6 +113,7 @@ 133F55BA2D33B55100E08EEA /* LibraryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryManager.swift; sourceTree = ""; }; 1359ED132D76F49900C13034 /* finTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = finTopView.swift; sourceTree = ""; }; 135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = ""; }; + 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; 136BBE7F2DB1038000906B5E /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; 138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = ""; }; 138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = ""; }; @@ -167,9 +169,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 13B77E192DA44F8300126FDF /* MarqueeLabel in Frameworks */, - 132E35232D959E410007800E /* Kingfisher in Frameworks */, - 132E351D2D959DDB0007800E /* Drops in Frameworks */, + 13637B8D2DE0ECCC00BDA2FC /* Kingfisher in Frameworks */, + 13637B902DE0ECD200BDA2FC /* Drops in Frameworks */, + 13637B932DE0ECDB00BDA2FC /* MarqueeLabel in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -293,16 +295,16 @@ isa = PBXGroup; children = ( 7205AEDA2DCCEF9500943F3F /* Cache */, + 13D842532D45266900EBBFA6 /* Drops */, + 1399FAD12D3AB33D00E97C31 /* Logger */, + 133D7C882D2BE2640075467E /* Modules */, + 133D7C8A2D2BE2640075467E /* JSLoader */, + 1327FBA52D758CEA00FC6689 /* Analytics */, + 133D7C862D2BE2640075467E /* Extensions */, + 13DC0C442D302C6A00D0F966 /* MediaPlayer */, + 13103E8C2D58E037000F0673 /* SkeletonCells */, 72443C832DC8046500A61321 /* DownloadUtils */, 13C0E5E82D5F85DD00E7F619 /* ContinueWatching */, - 13103E8C2D58E037000F0673 /* SkeletonCells */, - 13DC0C442D302C6A00D0F966 /* MediaPlayer */, - 133D7C862D2BE2640075467E /* Extensions */, - 1327FBA52D758CEA00FC6689 /* Analytics */, - 133D7C8A2D2BE2640075467E /* JSLoader */, - 133D7C882D2BE2640075467E /* Modules */, - 1399FAD12D3AB33D00E97C31 /* Logger */, - 13D842532D45266900EBBFA6 /* Drops */, ); path = Utils; sourceTree = ""; @@ -313,6 +315,7 @@ 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */, 136BBE7F2DB1038000906B5E /* Notification+Name.swift */, 1327FBA82D758DEA00FC6689 /* UIDevice+Model.swift */, + 13637B892DE0EA1100BDA2FC /* UserDefaults.swift */, 133D7C872D2BE2640075467E /* URLSession.swift */, 1359ED132D76F49900C13034 /* finTopView.swift */, 13CBEFD92D5F7D1200D011EE /* String.swift */, @@ -406,7 +409,6 @@ children = ( 13C0E5E92D5F85EA00E7F619 /* ContinueWatchingManager.swift */, 13C0E5EB2D5F85F800E7F619 /* ContinueWatchingItem.swift */, - 131270162DC13A010093AA9C /* DownloadManager.swift */, ); path = ContinueWatching; sourceTree = ""; @@ -498,6 +500,7 @@ isa = PBXGroup; children = ( 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */, + 131270162DC13A010093AA9C /* DownloadManager.swift */, 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */, ); path = DownloadUtils; @@ -530,9 +533,9 @@ ); name = Sulfur; packageProductDependencies = ( - 132E351C2D959DDB0007800E /* Drops */, - 132E35222D959E410007800E /* Kingfisher */, - 13B77E182DA44F8300126FDF /* MarqueeLabel */, + 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */, + 13637B8F2DE0ECD200BDA2FC /* Drops */, + 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */, ); productName = Sora; productReference = 133D7C6A2D2BE2500075467E /* Sulfur.app */; @@ -562,9 +565,9 @@ ); mainGroup = 133D7C612D2BE2500075467E; packageReferences = ( - 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */, - 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */, - 13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */, + 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */, + 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */, + 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */, ); productRefGroup = 133D7C6B2D2BE2500075467E /* Products */; projectDirPath = ""; @@ -662,6 +665,7 @@ 7222485F2DCBAA2C00CABE2D /* DownloadModels.swift in Sources */, 722248602DCBAA2C00CABE2D /* M3U8StreamExtractor.swift in Sources */, 13C0E5EA2D5F85EA00E7F619 /* ContinueWatchingManager.swift in Sources */, + 13637B8A2DE0EA1100BDA2FC /* UserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -800,7 +804,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = V9MT5Y43YG; + DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -821,7 +825,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.2; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur.test; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; @@ -842,7 +846,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Sora/Preview Content\""; - DEVELOPMENT_TEAM = V9MT5Y43YG; + DEVELOPMENT_TEAM = 399LMK6Q2Y; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -863,7 +867,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.2.2; - PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur.test; + PRODUCT_BUNDLE_IDENTIFIER = me.cranci.sulfur; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; @@ -898,15 +902,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/omaralbeik/Drops.git"; - requirement = { - branch = main; - kind = branch; - }; - }; - 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -914,7 +910,15 @@ version = 7.9.1; }; }; - 13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */ = { + 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/omaralbeik/Drops.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/cbpowell/MarqueeLabel"; requirement = { @@ -925,19 +929,19 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 132E351C2D959DDB0007800E /* Drops */ = { + 13637B8C2DE0ECCC00BDA2FC /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = 132E351B2D959DDB0007800E /* XCRemoteSwiftPackageReference "Drops" */; - productName = Drops; - }; - 132E35222D959E410007800E /* Kingfisher */ = { - isa = XCSwiftPackageProductDependency; - package = 132E35212D959E410007800E /* XCRemoteSwiftPackageReference "Kingfisher" */; + package = 13637B8B2DE0ECCC00BDA2FC /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - 13B77E182DA44F8300126FDF /* MarqueeLabel */ = { + 13637B8F2DE0ECD200BDA2FC /* Drops */ = { isa = XCSwiftPackageProductDependency; - package = 13B77E172DA44F8300126FDF /* XCRemoteSwiftPackageReference "MarqueeLabel" */; + package = 13637B8E2DE0ECD200BDA2FC /* XCRemoteSwiftPackageReference "Drops" */; + productName = Drops; + }; + 13637B922DE0ECDB00BDA2FC /* MarqueeLabel */ = { + isa = XCSwiftPackageProductDependency; + package = 13637B912DE0ECDB00BDA2FC /* XCRemoteSwiftPackageReference "MarqueeLabel" */; productName = MarqueeLabel; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a4a1b44..a843cd1 100644 --- a/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sulfur.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,33 +1,34 @@ { - "originHash" : "60d5882290a22b3286d882ec649bd11b12151e9ee052d03237e8071773244b7f", - "pins" : [ - { - "identity" : "drops", - "kind" : "remoteSourceControl", - "location" : "https://github.com/omaralbeik/Drops.git", - "state" : { - "branch" : "main", - "revision" : "5824681795286c36bdc4a493081a63e64e2a064e" + "object": { + "pins": [ + { + "package": "Drops", + "repositoryURL": "https://github.com/omaralbeik/Drops.git", + "state": { + "branch": "main", + "revision": "5824681795286c36bdc4a493081a63e64e2a064e", + "version": null + } + }, + { + "package": "Kingfisher", + "repositoryURL": "https://github.com/onevcat/Kingfisher.git", + "state": { + "branch": null, + "revision": "b6f62758f21a8c03cd64f4009c037cfa580a256e", + "version": "7.9.1" + } + }, + { + "package": "MarqueeLabel", + "repositoryURL": "https://github.com/cbpowell/MarqueeLabel", + "state": { + "branch": null, + "revision": "cffb6938940d3242882e6a2f9170b7890a4729ea", + "version": "4.2.1" + } } - }, - { - "identity" : "kingfisher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onevcat/Kingfisher.git", - "state" : { - "revision" : "b6f62758f21a8c03cd64f4009c037cfa580a256e", - "version" : "7.9.1" - } - }, - { - "identity" : "marqueelabel", - "kind" : "remoteSourceControl", - "location" : "https://github.com/cbpowell/MarqueeLabel", - "state" : { - "revision" : "cffb6938940d3242882e6a2f9170b7890a4729ea", - "version" : "4.2.1" - } - } - ], - "version" : 3 + ] + }, + "version": 1 }