mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
Yattee v2 rewrite
This commit is contained in:
307
Yattee/Views/Settings/NotificationSettingsView.swift
Normal file
307
Yattee/Views/Settings/NotificationSettingsView.swift
Normal file
@@ -0,0 +1,307 @@
|
||||
//
|
||||
// NotificationSettingsView.swift
|
||||
// Yattee
|
||||
//
|
||||
// Settings view for background notifications configuration.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct NotificationSettingsView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@State private var authorizationChecked = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if let settings = appEnvironment?.settingsManager,
|
||||
let notificationManager = appEnvironment?.notificationManager {
|
||||
// Master toggle section
|
||||
EnableSection(
|
||||
settings: settings,
|
||||
notificationManager: notificationManager,
|
||||
appEnvironment: appEnvironment
|
||||
)
|
||||
|
||||
// Permission status section
|
||||
if authorizationChecked {
|
||||
PermissionSection(notificationManager: notificationManager)
|
||||
}
|
||||
|
||||
// Default for new subscriptions
|
||||
if settings.backgroundNotificationsEnabled {
|
||||
DefaultsSection(settings: settings)
|
||||
|
||||
// Manage channels section
|
||||
ManageChannelsSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "settings.notifications.title"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.task {
|
||||
await appEnvironment?.notificationManager.refreshAuthorizationStatus()
|
||||
authorizationChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Enable Section
|
||||
|
||||
private struct EnableSection: View {
|
||||
@Bindable var settings: SettingsManager
|
||||
let notificationManager: NotificationManager
|
||||
let appEnvironment: AppEnvironment?
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "settings.notifications.enable"),
|
||||
isOn: Binding(
|
||||
get: { settings.backgroundNotificationsEnabled },
|
||||
set: { newValue in
|
||||
if newValue {
|
||||
enableNotifications()
|
||||
} else {
|
||||
disableNotifications()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} footer: {
|
||||
Text(String(localized: "settings.notifications.footer"))
|
||||
}
|
||||
}
|
||||
|
||||
private func enableNotifications() {
|
||||
Task {
|
||||
let granted = await notificationManager.requestAuthorization()
|
||||
if granted {
|
||||
settings.backgroundNotificationsEnabled = true
|
||||
appEnvironment?.backgroundRefreshManager.handleNotificationsEnabledChanged(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disableNotifications() {
|
||||
settings.backgroundNotificationsEnabled = false
|
||||
appEnvironment?.backgroundRefreshManager.handleNotificationsEnabledChanged(false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Permission Section
|
||||
|
||||
private struct PermissionSection: View {
|
||||
let notificationManager: NotificationManager
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
HStack {
|
||||
Text(String(localized: "settings.notifications.permission"))
|
||||
Spacer()
|
||||
if notificationManager.isAuthorized {
|
||||
HStack(spacing: 4) {
|
||||
Text(String(localized: "settings.notifications.permission.granted"))
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Button(String(localized: "settings.notifications.openSettings")) {
|
||||
notificationManager.openNotificationSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Defaults Section
|
||||
|
||||
private struct DefaultsSection: View {
|
||||
@Bindable var settings: SettingsManager
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
Toggle(
|
||||
String(localized: "settings.notifications.defaultForNew"),
|
||||
isOn: $settings.defaultNotificationsForNewChannels
|
||||
)
|
||||
} header: {
|
||||
Text(String(localized: "settings.notifications.defaults.header"))
|
||||
} footer: {
|
||||
Text(String(localized: "settings.notifications.defaultForNew.footer"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manage Channels Section
|
||||
|
||||
private struct ManageChannelsSection: View {
|
||||
var body: some View {
|
||||
Section {
|
||||
NavigationLink {
|
||||
ManageChannelNotificationsView()
|
||||
} label: {
|
||||
Label(
|
||||
String(localized: "settings.notifications.manageChannels"),
|
||||
systemImage: "bell.badge"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Manage Channel Notifications View
|
||||
|
||||
struct ManageChannelNotificationsView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@State private var subscriptions: [Subscription] = []
|
||||
@State private var isLoading = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
private var subscriptionService: SubscriptionService? { appEnvironment?.subscriptionService }
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
if isLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
} else if let errorMessage {
|
||||
ContentUnavailableView {
|
||||
Label(
|
||||
String(localized: "settings.notifications.loadError.title"),
|
||||
systemImage: "exclamationmark.triangle"
|
||||
)
|
||||
} description: {
|
||||
Text(errorMessage)
|
||||
}
|
||||
} else if subscriptions.isEmpty {
|
||||
ContentUnavailableView {
|
||||
Label(
|
||||
String(localized: "settings.notifications.noSubscriptions.title"),
|
||||
systemImage: "person.2.slash"
|
||||
)
|
||||
} description: {
|
||||
Text(String(localized: "settings.notifications.noSubscriptions.description"))
|
||||
}
|
||||
} else {
|
||||
ForEach(subscriptions, id: \.channelID) { subscription in
|
||||
ChannelNotificationToggle(subscription: subscription)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(String(localized: "settings.notifications.manageChannels.title"))
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.task {
|
||||
await loadSubscriptionsAsync()
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads subscriptions from the current subscription account provider.
|
||||
/// For local accounts, loads from DataManager.
|
||||
/// For Invidious/Piped accounts, fetches from the respective API.
|
||||
private func loadSubscriptionsAsync() async {
|
||||
guard let appEnvironment, let subscriptionService else { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
defer { isLoading = false }
|
||||
|
||||
// For local account, load from DataManager
|
||||
if appEnvironment.settingsManager.subscriptionAccount.type == .local {
|
||||
subscriptions = appEnvironment.dataManager.subscriptions()
|
||||
return
|
||||
}
|
||||
|
||||
// For Invidious/Piped, fetch from service
|
||||
do {
|
||||
let channels = try await subscriptionService.fetchSubscriptions()
|
||||
subscriptions = channels.map { Subscription.from(channel: $0) }
|
||||
} catch {
|
||||
LoggingService.shared.error(
|
||||
"Failed to load subscriptions for notifications: \(error.localizedDescription)",
|
||||
category: .general
|
||||
)
|
||||
errorMessage = error.localizedDescription
|
||||
subscriptions = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Channel Notification Toggle
|
||||
|
||||
private struct ChannelNotificationToggle: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
let subscription: Subscription
|
||||
@State private var notificationsEnabled: Bool = false
|
||||
|
||||
private var yatteeServer: Instance? {
|
||||
appEnvironment?.instancesManager.enabledYatteeServerInstances.first
|
||||
}
|
||||
|
||||
private var effectiveAvatarURL: URL? {
|
||||
AvatarURLBuilder.avatarURL(
|
||||
channelID: subscription.channelID,
|
||||
directURL: subscription.avatarURL,
|
||||
serverURL: yatteeServer?.url,
|
||||
size: 28
|
||||
)
|
||||
}
|
||||
|
||||
private var authHeader: String? {
|
||||
guard let server = yatteeServer else { return nil }
|
||||
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
|
||||
}
|
||||
|
||||
private var toggleBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { notificationsEnabled },
|
||||
set: { newValue in
|
||||
notificationsEnabled = newValue
|
||||
appEnvironment?.dataManager.setNotificationsEnabled(newValue, for: subscription.channelID)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: toggleBinding) {
|
||||
HStack(spacing: 10) {
|
||||
// Channel avatar
|
||||
LazyImage(request: AvatarURLBuilder.imageRequest(url: effectiveAvatarURL, authHeader: authHeader)) { state in
|
||||
if let image = state.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
} else {
|
||||
Circle()
|
||||
.fill(.quaternary)
|
||||
}
|
||||
}
|
||||
.frame(width: 28, height: 28)
|
||||
.clipShape(Circle())
|
||||
|
||||
// Channel name
|
||||
Text(subscription.name)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Load initial value from ChannelNotificationSettings
|
||||
notificationsEnabled = appEnvironment?.dataManager.notificationsEnabled(for: subscription.channelID) ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
NotificationSettingsView()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user