tv-anarchy/Sources/TVAnarchyCore/NotificationsService.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

52 lines
2.1 KiB
Swift

import Foundation
import Observation
#if canImport(UserNotifications)
import UserNotifications
#endif
/// Surfaces download events two ways: a macOS Notification Center alert (works when
/// the app is backgrounded; one-time permission prompt) AND an in-app banner via the
/// observable `lastBanner` (always set, even when notifications are denied or
/// unavailable e.g. a non-bundled test run). Posting is a no-op when
/// `notifyDownloads` is off.
@Observable
@MainActor
public final class NotificationsService {
public static let shared = NotificationsService()
public init() {}
@ObservationIgnored private var authorized = false
@ObservationIgnored private var asked = false
/// Most recent in-app banner message (the UI shows + auto-clears it).
public private(set) var lastBanner: String?
public func clearBanner() { lastBanner = nil }
/// In-app toast only always shown, independent of `notifyDownloads`.
public func showBanner(_ message: String) { lastBanner = message }
/// Ask once for Notification Center permission (the in-app banner needs none).
public func requestAuthorizationIfNeeded() {
#if canImport(UserNotifications)
guard !asked else { return }
asked = true
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { [weak self] ok, _ in
Task { @MainActor in self?.authorized = ok }
}
#endif
}
/// Post a notification (+ banner). Gated by the `notifyDownloads` setting.
public func post(title: String, body: String = "") {
guard SettingsStore.load().notifyDownloads else { return }
lastBanner = body.isEmpty ? title : "\(title)\(body)"
#if canImport(UserNotifications)
guard authorized else { return }
let content = UNMutableNotificationContent()
content.title = title
if !body.isEmpty { content.body = body }
content.sound = .default
UNUserNotificationCenter.current().add(
UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil))
#endif
}
}