Neues Icon, parallele Downloads

This commit is contained in:
Scarriffle
2026-06-08 19:32:18 +02:00
parent 9497c6e315
commit f053f1d320
7 changed files with 316 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
#if os(iOS)
import ActivityKit
import Foundation
/// Shared between the main app (to drive the activity lifecycle) and the
/// DownloadWidgetExtension (to render the Lock Screen / Dynamic Island UI).
/// Both targets must compile this exact definition keep them in sync.
struct DownloadActivityAttributes: ActivityAttributes {
/// Item title set once at activity start; never changes during the download.
let itemTitle: String
struct ContentState: Codable, Hashable {
/// Overall fraction across all tracks (0.0 1.0).
var progress: Double
/// Total bytes written to disk so far, shown as "X MB downloaded".
var bytesDownloaded: Int64
}
}
#endif

View File

@@ -0,0 +1,63 @@
#if os(iOS)
import ActivityKit
import Foundation
import os.log
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "ABS", category: "LiveActivity")
/// Manages one `Activity<DownloadActivityAttributes>` per active download key.
/// All methods must be called from the MainActor (same isolation as DownloadManager).
@MainActor
final class DownloadActivityManager {
// Keyed by downloadKey (matches DownloadManager's key scheme).
private var activities: [String: Activity<DownloadActivityAttributes>] = [:]
/// Requests a new Live Activity for a download that is starting.
/// Logs the reason to the console (visible in Xcode) if the request fails.
func startActivity(downloadKey: String, itemTitle: String) {
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
logger.warning("Live Activities disabled (areActivitiesEnabled=false) — skipping for '\(itemTitle)'")
return
}
let attributes = DownloadActivityAttributes(itemTitle: itemTitle)
let initial = DownloadActivityAttributes.ContentState(
progress: 0,
bytesDownloaded: 0
)
let content = ActivityContent(state: initial, staleDate: nil)
do {
let activity = try Activity.request(attributes: attributes, content: content)
activities[downloadKey] = activity
logger.info("Live Activity started: id=\(activity.id) title='\(itemTitle)'")
} catch {
// Most common reason: no widget extension is installed/embedded that
// provides an ActivityConfiguration for DownloadActivityAttributes.
logger.error("Activity.request() failed: \(error.localizedDescription)")
}
}
/// Pushes an incremental progress update to the running Live Activity.
func updateActivity(downloadKey: String, progress: Double, bytesDownloaded: Int64) {
guard let activity = activities[downloadKey] else { return }
let state = DownloadActivityAttributes.ContentState(
progress: progress,
bytesDownloaded: bytesDownloaded
)
let content = ActivityContent(state: state, staleDate: nil)
// activity.update() is async; fire-and-forget is intentional a skipped
// frame doesn't hurt the download and avoids blocking the download loop.
Task { await activity.update(content) }
}
/// Ends the Live Activity with `.immediate` dismissal so the Lock Screen
/// badge disappears right away instead of lingering for the default 4 hours.
func endActivity(downloadKey: String) {
guard let activity = activities.removeValue(forKey: downloadKey) else { return }
Task { await activity.end(nil, dismissalPolicy: .immediate) }
}
}
#endif

View File

@@ -0,0 +1,42 @@
import Foundation
import UserNotifications
#if canImport(UIKit)
import UIKit
#endif
/// Manages local UNUserNotificationCenter notifications for download events.
@MainActor
final class NotificationManager {
static let shared = NotificationManager()
private init() {}
/// Requests notification authorization the first time the app launches.
/// Skips the system prompt if permission has already been granted or denied.
func requestPermissionIfNeeded() async {
let center = UNUserNotificationCenter.current()
let settings = await center.notificationSettings()
guard settings.authorizationStatus == .notDetermined else { return }
_ = try? await center.requestAuthorization(options: [.alert, .sound])
}
/// Fires a local notification when a download fails.
/// Suppressed while the app is in the foreground an in-app alert already
/// surfaces the error there, so a notification would be redundant.
func sendDownloadFailedNotification(itemTitle: String) async {
#if canImport(UIKit)
guard UIApplication.shared.applicationState != .active else { return }
#endif
let content = UNMutableNotificationContent()
content.title = "Download fehlgeschlagen"
content.body = "\(itemTitle)" konnte nicht heruntergeladen werden."
content.sound = .default
let request = UNNotificationRequest(
identifier: "download-failed-\(UUID().uuidString)",
content: content,
trigger: nil // deliver immediately
)
try? await UNUserNotificationCenter.current().add(request)
}
}