diff --git a/Feather.xcodeproj/project.pbxproj b/Feather.xcodeproj/project.pbxproj index d13a8189..62aad15e 100644 --- a/Feather.xcodeproj/project.pbxproj +++ b/Feather.xcodeproj/project.pbxproj @@ -390,7 +390,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Feather/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Feather; - INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -418,15 +417,15 @@ SDKROOT = auto; STRIP_INSTALLED_PRODUCT = NO; STRIP_PNG_TEXT = NO; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 16.0; XROS_DEPLOYMENT_TARGET = 2.2; }; @@ -454,7 +453,6 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Feather/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Feather; - INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Samara M"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -482,14 +480,14 @@ SDKROOT = auto; STRIPFLAGS = "-rSTx"; STRIP_PNG_TEXT = NO; - SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; - SUPPORTS_MACCATALYST = NO; - SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "Feather/Supporting Files/Feather-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,3"; + TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 16.0; XROS_DEPLOYMENT_TARGET = 2.2; }; diff --git a/Feather/Backend/Observable/DownloadManager.swift b/Feather/Backend/Observable/DownloadManager.swift index 6930ad72..199c2f47 100644 --- a/Feather/Backend/Observable/DownloadManager.swift +++ b/Feather/Backend/Observable/DownloadManager.swift @@ -18,8 +18,8 @@ class Download: Identifiable, @unchecked Sendable { var overallProgress: Double { onlyArchiving - ? unpackageProgress - : (0.3 * unpackageProgress) + (0.7 * progress) + ? unpackageProgress + : (0.3 * unpackageProgress) + (0.7 * progress) } var task: URLSessionDownloadTask? @@ -29,7 +29,7 @@ class Download: Identifiable, @unchecked Sendable { let url: URL let fileName: String let onlyArchiving: Bool - + init( id: String, url: URL, @@ -52,7 +52,8 @@ class DownloadManager: NSObject, ObservableObject { } private var _session: URLSession! - + + #if !targetEnvironment(macCatalyst) private func _updateBackgroundAudioState() { if #unavailable(iOS 26.0){ if !downloads.isEmpty { @@ -62,13 +63,14 @@ class DownloadManager: NSObject, ObservableObject { } } } - + #endif + override init() { super.init() let configuration = URLSessionConfiguration.default _session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } - + func startDownload( from url: URL, id: String = UUID().uuidString @@ -77,19 +79,23 @@ class DownloadManager: NSObject, ObservableObject { resumeDownload(existingDownload) return existingDownload } - + let download = Download(id: id, url: url) - + let task = _session.downloadTask(with: url) download.task = task task.resume() - + downloads.append(download) + + #if !targetEnvironment(macCatalyst) if #available(iOS 26.0, *) { BackgroundTaskManager.shared.startTask(for: id, filename: url.lastPathComponent) } else { _updateBackgroundAudioState() } + #endif + return download } @@ -99,36 +105,50 @@ class DownloadManager: NSObject, ObservableObject { ) -> Download { let download = Download(id: id, url: url, onlyArchiving: true) downloads.append(download) + + #if !targetEnvironment(macCatalyst) _updateBackgroundAudioState() + #endif + return download } - + func resumeDownload(_ download: Download) { if let resumeData = download.resumeData { let task = _session.downloadTask(withResumeData: resumeData) download.task = task task.resume() + + #if !targetEnvironment(macCatalyst) _updateBackgroundAudioState() + #endif } else if let url = download.task?.originalRequest?.url { let task = _session.downloadTask(with: url) download.task = task task.resume() + + #if !targetEnvironment(macCatalyst) _updateBackgroundAudioState() + #endif } } - + func cancelDownload(_ download: Download) { download.task?.cancel() - + if let index = downloads.firstIndex(where: { $0.id == download.id }) { downloads.remove(at: index) + + #if !targetEnvironment(macCatalyst) _updateBackgroundAudioState() + if #available(iOS 26.0, *) { BackgroundTaskManager.shared.stopTask(for: download.id, success: false) } + #endif } } - + func isManualDownload(_ string: String) -> Bool { return string.contains("FeatherManualDownload") } @@ -158,10 +178,14 @@ extension DownloadManager: URLSessionDownloadDelegate { DispatchQueue.main.async { if let index = DownloadManager.shared.getDownloadIndex(by: dl.id) { DownloadManager.shared.downloads.remove(at: index) + + #if !targetEnvironment(macCatalyst) if #available(iOS 26.0, *) { BackgroundTaskManager.shared.updateProgress(for: dl.id, progress: 1.0) } + self._updateBackgroundAudioState() + #endif } } } @@ -188,22 +212,25 @@ extension DownloadManager: URLSessionDownloadDelegate { print("Error handling downloaded file: \(error.localizedDescription)") } } - + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { guard let download = getDownloadTask(by: downloadTask) else { return } - + DispatchQueue.main.async { download.progress = totalBytesExpectedToWrite > 0 - ? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) - : 0 + ? Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) + : 0 download.bytesDownloaded = totalBytesWritten download.totalBytes = totalBytesExpectedToWrite + + #if !targetEnvironment(macCatalyst) if #available(iOS 26.0, *) { BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress) } + #endif } } - + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { guard let _ = error, diff --git a/Feather/Utilities/BackgroundAudioManager.swift b/Feather/Utilities/BackgroundAudioManager.swift index d4e953ed..93732fee 100644 --- a/Feather/Utilities/BackgroundAudioManager.swift +++ b/Feather/Utilities/BackgroundAudioManager.swift @@ -5,19 +5,21 @@ // Created by Nagata Asami on 12/10/25. // +#if !targetEnvironment(macCatalyst) + import AVFoundation class BackgroundAudioManager { static let shared = BackgroundAudioManager() private let _engine = AVAudioEngine() - + private init() {} - + func start() { do { let session = AVAudioSession.sharedInstance() - + try session.setCategory(.playback, options: [.mixWithOthers]) try session.setActive(true) let silence = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in @@ -27,7 +29,7 @@ class BackgroundAudioManager { } return noErr } - + _engine.attach(silence) _engine.connect(silence, to: _engine.mainMixerNode, format: nil) try _engine.start() @@ -35,9 +37,11 @@ class BackgroundAudioManager { print("failed to start engine:", error) } } - + func stop() { _engine.stop() try? AVAudioSession.sharedInstance().setActive(false) } } + +#endif diff --git a/Feather/Utilities/BackgroundTaskManager.swift b/Feather/Utilities/BackgroundTaskManager.swift index ec7bba06..08c56e59 100644 --- a/Feather/Utilities/BackgroundTaskManager.swift +++ b/Feather/Utilities/BackgroundTaskManager.swift @@ -5,6 +5,8 @@ // Created by Nagata Asami on 4/1/26. // +#if !targetEnvironment(macCatalyst) + import Foundation import BackgroundTasks import CryptoKit @@ -12,20 +14,20 @@ import CryptoKit @available(iOS 26.0, *) class BackgroundTaskManager: ObservableObject { static let shared = BackgroundTaskManager() - + private let baseId = "\(Bundle.main.bundleIdentifier!).userTask" - + private var activeTasks: [String: BGContinuedProcessingTask] = [:] private var registeredTasks: Set = [] - + func startTask(for downloadId: String, filename: String) { let taskIdentifier = "\(baseId).\(downloadId.md5)" - + if !registeredTasks.contains(taskIdentifier) { BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in guard let task = task as? BGContinuedProcessingTask else { return } self.activeTasks[task.identifier] = task - + task.expirationHandler = { if let download = DownloadManager.shared.getDownload(by: downloadId) { DownloadManager.shared.cancelDownload(download) @@ -35,7 +37,7 @@ class BackgroundTaskManager: ObservableObject { } self.registeredTasks.insert(taskIdentifier) } - + let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: filename, subtitle: .localized("Downloading")) request.strategy = .queue do { @@ -44,25 +46,25 @@ class BackgroundTaskManager: ObservableObject { print(error) } } - + func updateProgress(for downloadId: String, progress: Double) { let taskIdentifier = "\(baseId).\(downloadId.md5)" - + guard let task = activeTasks[taskIdentifier] else { return } task.progress.totalUnitCount = 100 task.progress.completedUnitCount = Int64(progress * 100) - + task.updateTitle(task.title, subtitle: "\(Int(progress * 100))%") - + if task.progress.completedUnitCount == task.progress.totalUnitCount { stopTask(for: downloadId, success: true) } } - + func stopTask(for downloadId: String, success: Bool) { let taskIdentifier = "\(baseId).\(downloadId.md5)" guard let task = activeTasks[taskIdentifier] else { return } - + task.setTaskCompleted(success: success) activeTasks.removeValue(forKey: taskIdentifier) } @@ -73,3 +75,5 @@ extension String { Insecure.MD5.hash(data: Data(self.utf8)).map { String(format: "%02hhx", $0) }.joined() } } + +#endif diff --git a/Feather/Utilities/Handlers/AppFileHandler.swift b/Feather/Utilities/Handlers/AppFileHandler.swift index 238149ef..e8d7d742 100644 --- a/Feather/Utilities/Handlers/AppFileHandler.swift +++ b/Feather/Utilities/Handlers/AppFileHandler.swift @@ -69,9 +69,12 @@ final class AppFileHandler: NSObject, @unchecked Sendable { if let download = download { DispatchQueue.main.async { download.unpackageProgress = progress + + #if !targetEnvironment(macCatalyst) if #available(iOS 26.0, *) { BackgroundTaskManager.shared.updateProgress(for: download.id, progress: download.overallProgress) } + #endif } } } diff --git a/Feather/Views/Library/Install/InstallPreviewView.swift b/Feather/Views/Library/Install/InstallPreviewView.swift index ac2dd8ab..9d18fe1d 100644 --- a/Feather/Views/Library/Install/InstallPreviewView.swift +++ b/Feather/Views/Library/Install/InstallPreviewView.swift @@ -78,25 +78,34 @@ struct InstallPreviewView: View { ) } } - + switch newStatus { case .completed, .broken(_): progressTask?.cancel() progressTask = nil + #if !targetEnvironment(macCatalyst) BackgroundAudioManager.shared.stop() + #endif default: break } } } .onAppear(perform: _install) + + #if !targetEnvironment(macCatalyst) .onAppear { BackgroundAudioManager.shared.start() } + #endif + .onDisappear { progressTask?.cancel() progressTask = nil + + #if !targetEnvironment(macCatalyst) BackgroundAudioManager.shared.stop() + #endif } }