mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
fixed stuffs idk
This commit is contained in:
parent
c85b6690da
commit
21a08e4aa3
10 changed files with 293 additions and 539 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// UserDefaults.swift
|
||||
// Sulfur
|
||||
//
|
||||
// Created by Francesco on 11/05/25.
|
||||
// Created by Francesco on 23/05/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Int> = []
|
||||
@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<Bool, Never>
|
||||
) {
|
||||
// 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<Bool, Never>) {
|
||||
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)"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
1359ED132D76F49900C13034 /* finTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = finTopView.swift; sourceTree = "<group>"; };
|
||||
135CCBE12D4D1138008B9C0E /* SettingsViewPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewPlayer.swift; sourceTree = "<group>"; };
|
||||
13637B892DE0EA1100BDA2FC /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
|
||||
136BBE7F2DB1038000906B5E /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = "<group>"; };
|
||||
138AA1B62D2D66FD0021F9DF /* EpisodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeCell.swift; sourceTree = "<group>"; };
|
||||
138AA1B72D2D66FD0021F9DF /* CircularProgressBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue