mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-21 00:22:12 +00:00
test
This commit is contained in:
parent
e9d9101526
commit
e14b00fc13
3 changed files with 252 additions and 124 deletions
|
|
@ -75,11 +75,11 @@ extension JSController {
|
||||||
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
|
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
|
||||||
let destinationURL = downloadDirectory.appendingPathComponent(filename)
|
let destinationURL = downloadDirectory.appendingPathComponent(filename)
|
||||||
|
|
||||||
// Create an active download object
|
// Create an active download object with proper initial status
|
||||||
let activeDownload = JSActiveDownload(
|
var activeDownload = JSActiveDownload(
|
||||||
id: downloadID,
|
id: downloadID,
|
||||||
originalURL: url,
|
originalURL: url,
|
||||||
task: nil,
|
task: nil, // Will be set after task creation
|
||||||
queueStatus: .downloading,
|
queueStatus: .downloading,
|
||||||
type: downloadType,
|
type: downloadType,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
|
@ -89,123 +89,27 @@ extension JSController {
|
||||||
headers: headers
|
headers: headers
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add to active downloads
|
// Enhanced session configuration for background downloads
|
||||||
activeDownloads.append(activeDownload)
|
let sessionConfig = URLSessionConfiguration.background(withIdentifier: "mp4-download-\(downloadID.uuidString)")
|
||||||
|
|
||||||
// Create request with headers
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.timeoutInterval = 30.0
|
|
||||||
for (key, value) in headers {
|
|
||||||
request.addValue(value, forHTTPHeaderField: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhanced session configuration
|
|
||||||
let sessionConfig = URLSessionConfiguration.default
|
|
||||||
sessionConfig.timeoutIntervalForRequest = 60.0
|
sessionConfig.timeoutIntervalForRequest = 60.0
|
||||||
sessionConfig.timeoutIntervalForResource = 1800.0
|
sessionConfig.timeoutIntervalForResource = 3600.0 // 1 hour for large files
|
||||||
sessionConfig.httpMaximumConnectionsPerHost = 1
|
sessionConfig.httpMaximumConnectionsPerHost = 1
|
||||||
sessionConfig.allowsCellularAccess = true
|
sessionConfig.allowsCellularAccess = true
|
||||||
|
sessionConfig.shouldUseExtendedBackgroundIdleMode = true
|
||||||
|
sessionConfig.waitsForConnectivity = true
|
||||||
|
|
||||||
// Create custom session with delegate
|
// Create custom session with delegate
|
||||||
let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
|
let customSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: .main)
|
||||||
|
|
||||||
// Create the download task
|
// Create the download task
|
||||||
let downloadTask = customSession.downloadTask(with: request) { [weak self] (tempURL, response, error) in
|
let downloadTask = customSession.downloadTask(with: request)
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
defer {
|
|
||||||
// Clean up resources
|
|
||||||
self.cleanupDownloadResources(for: downloadID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle error cases - just remove from active downloads
|
|
||||||
if let error = error {
|
|
||||||
print("MP4 Download Error: \(error.localizedDescription)")
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
completionHandler?(false, "Download failed: \(error.localizedDescription)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response
|
|
||||||
guard let httpResponse = response as? HTTPURLResponse else {
|
|
||||||
print("MP4 Download: Invalid response")
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
completionHandler?(false, "Invalid server response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard (200...299).contains(httpResponse.statusCode) else {
|
|
||||||
print("MP4 Download HTTP Error: \(httpResponse.statusCode)")
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
completionHandler?(false, "Server error: \(httpResponse.statusCode)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let tempURL = tempURL else {
|
|
||||||
print("MP4 Download: No temporary file URL")
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
completionHandler?(false, "Download data not available")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move file to final destination
|
|
||||||
do {
|
|
||||||
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
||||||
try FileManager.default.removeItem(at: destinationURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
try FileManager.default.moveItem(at: tempURL, to: destinationURL)
|
|
||||||
print("MP4 Download: Successfully saved to \(destinationURL.path)")
|
|
||||||
|
|
||||||
// Verify file size
|
|
||||||
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
|
||||||
guard fileSize > 0 else {
|
|
||||||
throw NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Downloaded file is empty"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create downloaded asset
|
|
||||||
let downloadedAsset = DownloadedAsset(
|
|
||||||
name: title ?? url.lastPathComponent,
|
|
||||||
downloadDate: Date(),
|
|
||||||
originalURL: url,
|
|
||||||
localURL: destinationURL,
|
|
||||||
type: downloadType,
|
|
||||||
metadata: metadata,
|
|
||||||
subtitleURL: subtitleURL
|
|
||||||
)
|
|
||||||
|
|
||||||
// Save asset
|
|
||||||
self.savedAssets.append(downloadedAsset)
|
|
||||||
self.saveAssets()
|
|
||||||
|
|
||||||
// Update progress to complete and remove after delay
|
|
||||||
self.updateDownloadProgress(downloadID: downloadID, progress: 1.0)
|
|
||||||
|
|
||||||
// Download subtitle if provided
|
|
||||||
if let subtitleURL = subtitleURL {
|
|
||||||
self.downloadSubtitle(subtitleURL: subtitleURL, assetID: downloadedAsset.id.uuidString)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify completion
|
|
||||||
NotificationCenter.default.post(name: NSNotification.Name("downloadCompleted"), object: downloadedAsset)
|
|
||||||
completionHandler?(true, "Download completed successfully")
|
|
||||||
|
|
||||||
// Remove from active downloads after success
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
print("MP4 Download Error saving file: \(error.localizedDescription)")
|
|
||||||
self.removeActiveDownload(downloadID: downloadID)
|
|
||||||
completionHandler?(false, "Error saving download: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up progress observation
|
// Update active download with the task
|
||||||
setupProgressObservation(for: downloadTask, downloadID: downloadID)
|
activeDownload.task = downloadTask
|
||||||
|
|
||||||
|
// Add to active downloads and create task mapping
|
||||||
|
activeDownloads.append(activeDownload)
|
||||||
|
activeDownloadMap[downloadTask] = downloadID
|
||||||
|
|
||||||
// Store session reference
|
// Store session reference
|
||||||
storeSessionReference(session: customSession, for: downloadID)
|
storeSessionReference(session: customSession, for: downloadID)
|
||||||
|
|
@ -214,6 +118,18 @@ extension JSController {
|
||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
print("MP4 Download: Task started for \(filename)")
|
print("MP4 Download: Task started for \(filename)")
|
||||||
|
|
||||||
|
// Post initial status notification
|
||||||
|
postDownloadNotification(.statusChange)
|
||||||
|
|
||||||
|
// If this is an episode, post initial progress update
|
||||||
|
if let episodeNumber = metadata?.episode {
|
||||||
|
postDownloadNotification(.progress, userInfo: [
|
||||||
|
"episodeNumber": episodeNumber,
|
||||||
|
"progress": 0.0,
|
||||||
|
"status": "downloading"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
// Initial success callback
|
// Initial success callback
|
||||||
completionHandler?(true, "Download started")
|
completionHandler?(true, "Download started")
|
||||||
}
|
}
|
||||||
|
|
@ -221,12 +137,47 @@ extension JSController {
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func removeActiveDownload(downloadID: UUID) {
|
private func removeActiveDownload(downloadID: UUID) {
|
||||||
activeDownloads.removeAll { $0.id == downloadID }
|
// Find and remove the download
|
||||||
|
if let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) {
|
||||||
|
let download = activeDownloads[index]
|
||||||
|
activeDownloads.remove(at: index)
|
||||||
|
|
||||||
|
// Clean up task mapping
|
||||||
|
if let task = download.task {
|
||||||
|
activeDownloadMap.removeValue(forKey: task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
|
cleanupDownloadResources(for: downloadID)
|
||||||
|
|
||||||
|
// Post status change notification
|
||||||
|
postDownloadNotification(.statusChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateDownloadProgress(downloadID: UUID, progress: Double) {
|
private func updateDownloadProgress(downloadID: UUID, progress: Double) {
|
||||||
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else { return }
|
guard let index = activeDownloads.firstIndex(where: { $0.id == downloadID }) else { return }
|
||||||
activeDownloads[index].progress = progress
|
|
||||||
|
let previousProgress = activeDownloads[index].progress
|
||||||
|
activeDownloads[index].progress = min(max(progress, 0.0), 1.0)
|
||||||
|
|
||||||
|
// Only post notifications for meaningful progress changes (every 1% or completion)
|
||||||
|
let progressDifference = progress - previousProgress
|
||||||
|
if progressDifference >= 0.01 || progress >= 1.0 || previousProgress == 0.0 {
|
||||||
|
// Post general progress notification
|
||||||
|
postDownloadNotification(.progress)
|
||||||
|
|
||||||
|
// Post detailed episode progress if applicable
|
||||||
|
if let download = activeDownloads.first(where: { $0.id == downloadID }),
|
||||||
|
let episodeNumber = download.metadata?.episode {
|
||||||
|
let status = progress >= 1.0 ? "completed" : "downloading"
|
||||||
|
postDownloadNotification(.progress, userInfo: [
|
||||||
|
"episodeNumber": episodeNumber,
|
||||||
|
"progress": progress,
|
||||||
|
"status": status
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupProgressObservation(for task: URLSessionDownloadTask, downloadID: UUID) {
|
private func setupProgressObservation(for task: URLSessionDownloadTask, downloadID: UUID) {
|
||||||
|
|
@ -253,10 +204,126 @@ extension JSController {
|
||||||
|
|
||||||
private func cleanupDownloadResources(for downloadID: UUID) {
|
private func cleanupDownloadResources(for downloadID: UUID) {
|
||||||
mp4ProgressObservations?[downloadID] = nil
|
mp4ProgressObservations?[downloadID] = nil
|
||||||
|
mp4CustomSessions?[downloadID]?.invalidateAndCancel()
|
||||||
mp4CustomSessions?[downloadID] = nil
|
mp4CustomSessions?[downloadID] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - URLSessionDownloadDelegate for MP4 Downloads
|
||||||
|
extension JSController: URLSessionDownloadDelegate {
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||||
|
// Check if this is an MP4 download by checking if we have a custom session for it
|
||||||
|
guard let downloadID = activeDownloadMap[downloadTask] else {
|
||||||
|
// If not found in our mapping, it might be an AVAssetDownloadTask
|
||||||
|
// Let the existing AVAssetDownloadDelegate handle it
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let downloadIndex = activeDownloads.firstIndex(where: { $0.id == downloadID }) else {
|
||||||
|
print("MP4 Download: Couldn't find download for completed task")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this download was cancelled
|
||||||
|
if cancelledDownloadIDs.contains(downloadID) {
|
||||||
|
print("MP4 Download: Ignoring completion for cancelled download")
|
||||||
|
try? FileManager.default.removeItem(at: location)
|
||||||
|
removeActiveDownload(downloadID: downloadID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let download = activeDownloads[downloadIndex]
|
||||||
|
|
||||||
|
// Move file to final destination
|
||||||
|
guard let downloadDirectory = getPersistentDownloadDirectory() else {
|
||||||
|
print("MP4 Download: Failed to get download directory")
|
||||||
|
removeActiveDownload(downloadID: downloadID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitizedTitle = download.title?.replacingOccurrences(of: "[^A-Za-z0-9 ._-]", with: "", options: .regularExpression) ?? "download"
|
||||||
|
let filename = "\(sanitizedTitle)_\(downloadID.uuidString.prefix(8)).mp4"
|
||||||
|
let destinationURL = downloadDirectory.appendingPathComponent(filename)
|
||||||
|
|
||||||
|
do {
|
||||||
|
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
||||||
|
try FileManager.default.removeItem(at: destinationURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
try FileManager.default.moveItem(at: location, to: destinationURL)
|
||||||
|
print("MP4 Download: Successfully saved to \(destinationURL.path)")
|
||||||
|
|
||||||
|
// Verify file size
|
||||||
|
let fileSize = try FileManager.default.attributesOfItem(atPath: destinationURL.path)[.size] as? Int64 ?? 0
|
||||||
|
guard fileSize > 0 else {
|
||||||
|
throw NSError(domain: "DownloadError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Downloaded file is empty"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create downloaded asset
|
||||||
|
let downloadedAsset = DownloadedAsset(
|
||||||
|
name: download.title ?? download.originalURL.lastPathComponent,
|
||||||
|
downloadDate: Date(),
|
||||||
|
originalURL: download.originalURL,
|
||||||
|
localURL: destinationURL,
|
||||||
|
type: download.type,
|
||||||
|
metadata: download.metadata,
|
||||||
|
subtitleURL: download.subtitleURL
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save asset
|
||||||
|
savedAssets.append(downloadedAsset)
|
||||||
|
saveAssets()
|
||||||
|
|
||||||
|
// Update progress to complete
|
||||||
|
updateDownloadProgress(downloadID: downloadID, progress: 1.0)
|
||||||
|
|
||||||
|
// Download subtitle if provided
|
||||||
|
if let subtitleURL = download.subtitleURL {
|
||||||
|
downloadSubtitle(subtitleURL: subtitleURL, assetID: downloadedAsset.id.uuidString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify completion
|
||||||
|
postDownloadNotification(.completed)
|
||||||
|
|
||||||
|
// Clean up after a brief delay
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
|
self.removeActiveDownload(downloadID: downloadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print("MP4 Download Error saving file: \(error.localizedDescription)")
|
||||||
|
removeActiveDownload(downloadID: downloadID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||||
|
// Check if this is one of our MP4 downloads
|
||||||
|
guard let downloadID = activeDownloadMap[downloadTask] else { return }
|
||||||
|
|
||||||
|
// Calculate progress
|
||||||
|
let progress = totalBytesExpectedToWrite > 0 ? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) : 0.0
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateDownloadProgress(downloadID: downloadID, progress: progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
|
||||||
|
// Handle resume for MP4 downloads
|
||||||
|
guard let downloadID = activeDownloadMap[downloadTask] else { return }
|
||||||
|
|
||||||
|
let progress = expectedTotalBytes > 0 ? Double(fileOffset) / Double(expectedTotalBytes) : 0.0
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.updateDownloadProgress(downloadID: downloadID, progress: progress)
|
||||||
|
self.postDownloadNotification(.statusChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("MP4 Download: Resumed at offset \(fileOffset) of \(expectedTotalBytes)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - URLSessionDelegate
|
// MARK: - URLSessionDelegate
|
||||||
extension JSController: URLSessionDelegate {
|
extension JSController: URLSessionDelegate {
|
||||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||||
|
|
|
||||||
|
|
@ -1124,9 +1124,32 @@ extension JSController {
|
||||||
print("Cancelled active download: \(downloadTitle)")
|
print("Cancelled active download: \(downloadTitle)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mp4CustomSessions: [UUID: URLSession]? {
|
||||||
|
get {
|
||||||
|
return objc_getAssociatedObject(self, &AssociatedKeys.mp4CustomSessions) as? [UUID: URLSession]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.mp4CustomSessions, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mp4ProgressObservations: [UUID: NSKeyValueObservation]? {
|
||||||
|
get {
|
||||||
|
return objc_getAssociatedObject(self, &AssociatedKeys.mp4ProgressObservations) as? [UUID: NSKeyValueObservation]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &AssociatedKeys.mp4ProgressObservations, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - AVAssetDownloadDelegate
|
private struct AssociatedKeys {
|
||||||
|
static var mp4CustomSessions = "mp4CustomSessions"
|
||||||
|
static var mp4ProgressObservations = "mp4ProgressObservations"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
extension JSController: AVAssetDownloadDelegate {
|
extension JSController: AVAssetDownloadDelegate {
|
||||||
|
|
||||||
/// Called when a download task finishes downloading the asset
|
/// Called when a download task finishes downloading the asset
|
||||||
|
|
@ -1552,4 +1575,4 @@ enum DownloadQueueStatus: Equatable {
|
||||||
case downloading
|
case downloading
|
||||||
/// Download has been completed
|
/// Download has been completed
|
||||||
case completed
|
case completed
|
||||||
}
|
}
|
||||||
|
|
@ -729,6 +729,7 @@ struct EnhancedActiveDownloadCard: View {
|
||||||
let download: JSActiveDownload
|
let download: JSActiveDownload
|
||||||
@State private var currentProgress: Double
|
@State private var currentProgress: Double
|
||||||
@State private var taskState: URLSessionTask.State
|
@State private var taskState: URLSessionTask.State
|
||||||
|
@State private var progressUpdateTimer: Timer?
|
||||||
|
|
||||||
init(download: JSActiveDownload) {
|
init(download: JSActiveDownload) {
|
||||||
self.download = download
|
self.download = download
|
||||||
|
|
@ -842,6 +843,17 @@ struct EnhancedActiveDownloadCard: View {
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadProgressChanged"))) { _ in
|
||||||
updateProgress()
|
updateProgress()
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("downloadStatusChanged"))) { _ in
|
||||||
|
updateStatus()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
updateProgress()
|
||||||
|
updateStatus()
|
||||||
|
startProgressTimer()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
stopProgressTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusColor: Color {
|
private var statusColor: Color {
|
||||||
|
|
@ -849,8 +861,10 @@ struct EnhancedActiveDownloadCard: View {
|
||||||
return .orange
|
return .orange
|
||||||
} else if taskState == .running {
|
} else if taskState == .running {
|
||||||
return .green
|
return .green
|
||||||
} else {
|
} else if taskState == .suspended {
|
||||||
return .orange
|
return .orange
|
||||||
|
} else {
|
||||||
|
return .red
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,30 +873,54 @@ struct EnhancedActiveDownloadCard: View {
|
||||||
return "Queued"
|
return "Queued"
|
||||||
} else if taskState == .running {
|
} else if taskState == .running {
|
||||||
return "Downloading"
|
return "Downloading"
|
||||||
} else {
|
} else if taskState == .suspended {
|
||||||
return "Paused"
|
return "Paused"
|
||||||
|
} else {
|
||||||
|
return "Stopped"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startProgressTimer() {
|
||||||
|
progressUpdateTimer?.invalidate()
|
||||||
|
progressUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||||
|
updateProgress()
|
||||||
|
updateStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopProgressTimer() {
|
||||||
|
progressUpdateTimer?.invalidate()
|
||||||
|
progressUpdateTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func updateProgress() {
|
private func updateProgress() {
|
||||||
if let currentDownload = JSController.shared.activeDownloads.first(where: { $0.id == download.id }) {
|
if let currentDownload = JSController.shared.activeDownloads.first(where: { $0.id == download.id }) {
|
||||||
withAnimation(.easeInOut(duration: 0.1)) {
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
currentProgress = currentDownload.progress
|
currentProgress = currentDownload.progress
|
||||||
}
|
}
|
||||||
if let task = currentDownload.task {
|
}
|
||||||
taskState = task.state
|
}
|
||||||
}
|
|
||||||
|
private func updateStatus() {
|
||||||
|
if let currentDownload = JSController.shared.activeDownloads.first(where: { $0.id == download.id }),
|
||||||
|
let task = currentDownload.task {
|
||||||
|
taskState = task.state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func toggleDownload() {
|
private func toggleDownload() {
|
||||||
|
guard let task = download.task else { return }
|
||||||
|
|
||||||
if taskState == .running {
|
if taskState == .running {
|
||||||
download.task?.suspend()
|
task.suspend()
|
||||||
taskState = .suspended
|
taskState = .suspended
|
||||||
} else if taskState == .suspended {
|
} else if taskState == .suspended {
|
||||||
download.task?.resume()
|
task.resume()
|
||||||
taskState = .running
|
taskState = .running
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post status change notification
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("downloadStatusChanged"), object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cancelDownload() {
|
private func cancelDownload() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue