From 4f30dfb5886dc73c3c5e4620eeef154b2d4f504e Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:58:07 +0100 Subject: [PATCH] test cuz my mac doesnt build --- Sora/ContentView.swift | 4 + Sora/DownloadManager/DownloadManager.swift | 96 +++++++++++++---- Sora/Views/DownloadsView.swift | 117 +++++++++++++++++++++ Sulfur.xcodeproj/project.pbxproj | 6 ++ 4 files changed, 202 insertions(+), 21 deletions(-) create mode 100644 Sora/Views/DownloadsView.swift diff --git a/Sora/ContentView.swift b/Sora/ContentView.swift index afbd4ea..8569830 100644 --- a/Sora/ContentView.swift +++ b/Sora/ContentView.swift @@ -15,6 +15,10 @@ struct ContentView: View { .tabItem { Label("Library", systemImage: "books.vertical") } + DownloadsView() + .tabItem { + Label("Downloads", systemImage: "arrow.down.circle.fill") + } SearchView() .tabItem { Label("Search", systemImage: "magnifyingglass") diff --git a/Sora/DownloadManager/DownloadManager.swift b/Sora/DownloadManager/DownloadManager.swift index 59bbdcf..c488525 100644 --- a/Sora/DownloadManager/DownloadManager.swift +++ b/Sora/DownloadManager/DownloadManager.swift @@ -38,31 +38,15 @@ class DownloadManager { let outputFileName = "\(title)_Episode\(episode).mp4" let outputFileURL = folderURL.appendingPathComponent(outputFileName) + let downloadID = UUID() + NotificationCenter.default.post(name: .downloadStarted, object: nil, userInfo: ["fileName": outputFileName, "id": downloadID]) let fileExtension = url.pathExtension.lowercased() if fileExtension == "mp4" { - let task = URLSession.shared.downloadTask(with: url) { tempLocalURL, response, error in - if let tempLocalURL = tempLocalURL { - do { - try FileManager.default.moveItem(at: tempLocalURL, to: outputFileURL) - DispatchQueue.main.async { - Logger.shared.log("✅ Download successful: \(outputFileURL)") - completion(true, outputFileURL) - } - } catch { - DispatchQueue.main.async { - Logger.shared.log("❌ Download failed: \(error)") - completion(false, nil) - } - } - } else { - DispatchQueue.main.async { - Logger.shared.log("❌ Download failed: \(error?.localizedDescription ?? "Unknown error")") - completion(false, nil) - } - } - } + let delegate = DownloadTaskDelegate(downloadID: downloadID, fileName: outputFileName, outputFileURL: outputFileURL, completion: completion) + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let task = session.downloadTask(with: url) task.resume() } else if fileExtension == "m3u8" { DispatchQueue.global(qos: .background).async { @@ -92,9 +76,11 @@ class DownloadManager { DispatchQueue.main.async { if success == 0 { Logger.shared.log("✅ Conversion successful: \(outputFileURL)") + NotificationCenter.default.post(name: .downloadCompleted, object: nil, userInfo: ["id": downloadID, "success": true]) completion(true, outputFileURL) } else { Logger.shared.log("❌ Conversion failed") + NotificationCenter.default.post(name: .downloadCompleted, object: nil, userInfo: ["id": downloadID, "success": false]) completion(false, nil) } } @@ -105,3 +91,71 @@ class DownloadManager { } } } + +class DownloadTaskDelegate: NSObject, URLSessionDownloadDelegate { + let downloadID: UUID + let outputFileURL: URL + let completion: (Bool, URL?) -> Void + let fileName: String + let startTime: Date + var lastTime: Date + + init(downloadID: UUID, fileName: String, outputFileURL: URL, completion: @escaping (Bool, URL?) -> Void) { + self.downloadID = downloadID + self.fileName = fileName + self.outputFileURL = outputFileURL + self.completion = completion + self.startTime = Date() + self.lastTime = Date() + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { + let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) + let now = Date() + let timeInterval = now.timeIntervalSince(lastTime) + var speed: Double = 0.0 + if timeInterval > 0 { + speed = Double(bytesWritten) / timeInterval + } + lastTime = now + + let downloadedMB = Double(totalBytesWritten) / 1024.0 / 1024.0 + let speedMB = speed / 1024.0 / 1024.0 + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .downloadProgressUpdate, object: nil, userInfo: [ + "id": self.downloadID, + "progress": progress, + "downloadedSize": String(format: "%.2f MB", downloadedMB), + "downloadSpeed": String(format: "%.2f MB/s", speedMB) + ]) + } + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + do { + try FileManager.default.moveItem(at: location, to: outputFileURL) + DispatchQueue.main.async { + Logger.shared.log("✅ Download successful: \(self.outputFileURL)") + NotificationCenter.default.post(name: .downloadCompleted, object: nil, userInfo: ["id": self.downloadID, "success": true]) + self.completion(true, self.outputFileURL) + } + } catch { + DispatchQueue.main.async { + Logger.shared.log("❌ Download failed: \(error)") + NotificationCenter.default.post(name: .downloadCompleted, object: nil, userInfo: ["id": self.downloadID, "success": false]) + self.completion(false, nil) + } + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let error = error { + DispatchQueue.main.async { + Logger.shared.log("❌ Download failed: \(error.localizedDescription)") + NotificationCenter.default.post(name: .downloadCompleted, object: nil, userInfo: ["id": self.downloadID, "success": false]) + self.completion(false, nil) + } + } + } +} diff --git a/Sora/Views/DownloadsView.swift b/Sora/Views/DownloadsView.swift new file mode 100644 index 0000000..c2f1f80 --- /dev/null +++ b/Sora/Views/DownloadsView.swift @@ -0,0 +1,117 @@ +// +// DownloadsView.swift +// Sulfur +// +// Created by Francesco on 10/03/25. +// + +import SwiftUI +import Combine + +extension Notification.Name { + static let downloadStarted = Notification.Name("downloadStarted") + static let downloadProgressUpdate = Notification.Name("downloadProgressUpdate") + static let downloadCompleted = Notification.Name("downloadCompleted") +} + +struct DownloadItem: Identifiable { + let id = UUID() + let fileName: String + var status: String = "Downloading" + var progress: Double = 0.0 + var downloadedSize: String = "0 MB" + var downloadSpeed: String = "0 MB/s" +} + +class DownloadsViewModel: ObservableObject { + @Published var downloads: [DownloadItem] = [] + private var cancellables = Set() + + init() { + NotificationCenter.default.publisher(for: .downloadStarted) + .sink { [weak self] notification in + if let fileName = notification.userInfo?["fileName"] as? String { + let newDownload = DownloadItem(fileName: fileName) + self?.downloads.append(newDownload) + } + } + .store(in: &cancellables) + + NotificationCenter.default.publisher(for: .downloadProgressUpdate) + .sink { [weak self] notification in + guard let id = notification.userInfo?["id"] as? UUID, + let progress = notification.userInfo?["progress"] as? Double, + let downloadedSize = notification.userInfo?["downloadedSize"] as? String, + let downloadSpeed = notification.userInfo?["downloadSpeed"] as? String else { + return + } + if let index = self?.downloads.firstIndex(where: { $0.id == id }) { + self?.downloads[index].progress = progress + self?.downloads[index].downloadedSize = downloadedSize + self?.downloads[index].downloadSpeed = downloadSpeed + } + } + .store(in: &cancellables) + + NotificationCenter.default.publisher(for: .downloadCompleted) + .sink { [weak self] notification in + guard let id = notification.userInfo?["id"] as? UUID, + let success = notification.userInfo?["success"] as? Bool else { + return + } + if let index = self?.downloads.firstIndex(where: { $0.id == id }) { + self?.downloads[index].status = success ? "Completed" : "Failed" + self?.downloads[index].progress = success ? 1.0 : self?.downloads[index].progress ?? 0.0 + } + } + .store(in: &cancellables) + } +} + +struct DownloadsView: View { + @StateObject var viewModel = DownloadsViewModel() + + var body: some View { + NavigationView { + List(viewModel.downloads) { download in + DownloadRow(download: download) + } + .navigationTitle("Downloads") + } + } +} + +struct DownloadRow: View { + let download: DownloadItem + + var iconName: String { + switch download.status { + case "Downloading": + return "arrow.down.circle" + case "Completed": + return "checkmark.circle" + default: + return "exclamationmark.triangle" + } + } + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemName: iconName) + .foregroundColor(.blue) + Text(download.fileName) + .font(.headline) + } + ProgressView(value: download.progress) + .progressViewStyle(LinearProgressViewStyle()) + HStack { + Text("Speed: \(download.downloadSpeed)") + Spacer() + Text("Size: \(download.downloadedSize)") + } + .font(.subheadline) + } + .padding(.vertical, 8) + } +} diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index aba11bf..c6ce8fb 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 13CBA0882D60F19C00EFE70A /* VTTSubtitlesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */; }; 13CBEFDA2D5F7D1200D011EE /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13CBEFD92D5F7D1200D011EE /* String.swift */; }; 13D842552D45267500EBBFA6 /* DropManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D842542D45267500EBBFA6 /* DropManager.swift */; }; + 13D949EB2D7F412B00C92959 /* DownloadsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D949EA2D7F412B00C92959 /* DownloadsView.swift */; }; 13D99CF72D4E73C300250A86 /* ModuleAdditionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */; }; 13DB7CC32D7D99C0004371D3 /* SubtitleSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */; }; 13DB7CC62D7DC7D2004371D3 /* FFmpeg-iOS-Lame in Frameworks */ = {isa = PBXBuildFile; productRef = 13DB7CC52D7DC7D2004371D3 /* FFmpeg-iOS-Lame */; }; @@ -106,6 +107,7 @@ 13CBA0872D60F19C00EFE70A /* VTTSubtitlesLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VTTSubtitlesLoader.swift; sourceTree = ""; }; 13CBEFD92D5F7D1200D011EE /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 13D842542D45267500EBBFA6 /* DropManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropManager.swift; sourceTree = ""; }; + 13D949EA2D7F412B00C92959 /* DownloadsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsView.swift; sourceTree = ""; }; 13D99CF62D4E73C300250A86 /* ModuleAdditionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleAdditionSettingsView.swift; sourceTree = ""; }; 13DB7CC22D7D99C0004371D3 /* SubtitleSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSettingsManager.swift; sourceTree = ""; }; 13DB7CEB2D7DED5D004371D3 /* DownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManager.swift; sourceTree = ""; }; @@ -261,6 +263,7 @@ 1399FAD22D3AB34F00E97C31 /* SettingsView */, 133F55B92D33B53E00E08EEA /* LibraryView */, 133D7C7C2D2BE2630075467E /* SearchView.swift */, + 13D949EA2D7F412B00C92959 /* DownloadsView.swift */, ); path = Views; sourceTree = ""; @@ -557,6 +560,7 @@ 133D7C922D2BE2640075467E /* URLSession.swift in Sources */, 133D7C912D2BE2640075467E /* SettingsViewModule.swift in Sources */, 136F21BC2D5B8F29006409AC /* AniList-DetailsView.swift in Sources */, + 13D949EB2D7F412B00C92959 /* DownloadsView.swift in Sources */, 133F55BB2D33B55100E08EEA /* LibraryManager.swift in Sources */, 13103E842D589D8B000F0673 /* AniList-Seasonal.swift in Sources */, 133D7C8E2D2BE2640075467E /* LibraryView.swift in Sources */, @@ -607,6 +611,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_BITCODE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -669,6 +674,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11;