Commit 7805888980
Unsigned
Layout: unified · split
DomainDig/DomainViewModel+Monitoring.swift added +174
| @@ -0,0 +1,174 @@ | ||
| 1 | import Foundation | |
| 2 | import SwiftUI | |
| 3 | import UserNotifications | |
| 4 | ||
| 5 | /// Monitoring configuration surface of `DomainViewModel`: notification | |
| 6 | /// authorization, the monitoring settings mutators (scope, interval, adaptivity, | |
| 7 | /// sensitivity, quiet hours, alert filter/toggle, per-domain selection), the | |
| 8 | /// manual run, and the per-domain status labels. | |
| 9 | /// | |
| 10 | /// Settings mutations persist through `persistMonitoringSettings(...)` and | |
| 11 | /// `sanitizeMonitoringSelection()`, which remain on the main type because the | |
| 12 | /// tracked-domain lifecycle (add/delete/clear) calls them too. | |
| 13 | extension DomainViewModel { | |
| 14 | func refreshMonitoringAuthorizationStatus() async { | |
| 15 | let settings = await UNUserNotificationCenter.current().notificationSettings() | |
| 16 | monitoringNotificationStatus = settings.authorizationStatus | |
| 17 | } | |
| 18 | ||
| 19 | func setMonitoringEnabled(_ isEnabled: Bool) { | |
| 20 | guard !isEnabled || FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 21 | monitoringSettings.isEnabled = false | |
| 22 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 23 | return | |
| 24 | } | |
| 25 | ||
| 26 | monitoringSettings.isEnabled = isEnabled | |
| 27 | persistMonitoringSettings(localActivationConfirmed: isEnabled) | |
| 28 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 29 | } | |
| 30 | ||
| 31 | func setMonitoringScope(_ scope: MonitoringScope) { | |
| 32 | monitoringSettings.scope = scope | |
| 33 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 34 | } | |
| 35 | ||
| 36 | func setMonitoringBaseInterval(_ baseInterval: MonitoringBaseInterval) { | |
| 37 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 38 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 39 | return | |
| 40 | } | |
| 41 | monitoringSettings.baseInterval = baseInterval.interval | |
| 42 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 43 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 44 | } | |
| 45 | ||
| 46 | func setMonitoringAdaptiveEnabled(_ isEnabled: Bool) { | |
| 47 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 48 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 49 | return | |
| 50 | } | |
| 51 | monitoringSettings.adaptiveEnabled = isEnabled | |
| 52 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 53 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 54 | } | |
| 55 | ||
| 56 | func setMonitoringSensitivity(_ sensitivity: MonitoringSensitivity) { | |
| 57 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 58 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 59 | return | |
| 60 | } | |
| 61 | monitoringSettings.sensitivity = sensitivity | |
| 62 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 63 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 64 | } | |
| 65 | ||
| 66 | func setMonitoringQuietHours(startHour: Int, endHour: Int, isEnabled: Bool) { | |
| 67 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 68 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 69 | return | |
| 70 | } | |
| 71 | monitoringSettings.quietHours = isEnabled ? QuietHours(startHour: startHour, endHour: endHour) : nil | |
| 72 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 73 | } | |
| 74 | ||
| 75 | func setMonitoringAlertFilter(_ filter: MonitoringAlertFilter) { | |
| 76 | guard FeatureAccessService.hasAccess(to: .localAlerts) else { | |
| 77 | monitoringSettings.alertsEnabled = false | |
| 78 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts) | |
| 79 | return | |
| 80 | } | |
| 81 | monitoringSettings.alertFilter = filter | |
| 82 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 83 | } | |
| 84 | ||
| 85 | func setMonitoringAlertsEnabled(_ isEnabled: Bool) { | |
| 86 | guard !isEnabled || FeatureAccessService.hasAccess(to: .localAlerts) else { | |
| 87 | monitoringSettings.alertsEnabled = false | |
| 88 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts) | |
| 89 | return | |
| 90 | } | |
| 91 | monitoringSettings.alertsEnabled = isEnabled | |
| 92 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 93 | } | |
| 94 | ||
| 95 | func setMonitoringSelection(for trackedDomain: TrackedDomain, isSelected: Bool) { | |
| 96 | if isSelected { | |
| 97 | if !monitoringSettings.selectedDomainIDs.contains(trackedDomain.id) { | |
| 98 | monitoringSettings.selectedDomainIDs.append(trackedDomain.id) | |
| 99 | } | |
| 100 | } else { | |
| 101 | monitoringSettings.selectedDomainIDs.removeAll { $0 == trackedDomain.id } | |
| 102 | } | |
| 103 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 104 | } | |
| 105 | ||
| 106 | func toggleMonitoring(for trackedDomain: TrackedDomain) { | |
| 107 | guard canEdit(trackedDomain) else { return } | |
| 108 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 109 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 110 | return | |
| 111 | } | |
| 112 | guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return } | |
| 113 | trackedDomains[index].monitoringEnabled.toggle() | |
| 114 | trackedDomains[index].updatedAt = Date() | |
| 115 | MonitoringStorage.saveTrackedDomains(trackedDomains) | |
| 116 | CloudSyncService.shared.scheduleSyncIfNeeded() | |
| 117 | sanitizeMonitoringSelection() | |
| 118 | } | |
| 119 | ||
| 120 | func requestMonitoringNotificationAuthorization() async { | |
| 121 | let granted = await LocalNotificationService.shared.requestAuthorizationIfNeeded() | |
| 122 | monitoringSettings.alertsEnabled = granted | |
| 123 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 124 | await refreshMonitoringAuthorizationStatus() | |
| 125 | } | |
| 126 | ||
| 127 | func runMonitoringNow() { | |
| 128 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 129 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 130 | return | |
| 131 | } | |
| 132 | guard !monitoringRunInProgress else { return } | |
| 133 | ||
| 134 | monitoringRunInProgress = true | |
| 135 | monitoringStatusMessage = nil | |
| 136 | ||
| 137 | Task { [weak self] in | |
| 138 | guard let self else { return } | |
| 139 | let outcome = await DomainMonitoringService.shared.performMonitoring( | |
| 140 | trigger: .manual, | |
| 141 | requireEnabledSetting: false | |
| 142 | ) | |
| 143 | await MainActor.run { | |
| 144 | self.refreshMonitoringState() | |
| 145 | self.monitoringRunInProgress = false | |
| 146 | self.monitoringStatusMessage = outcome.message | |
| 147 | } | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | func monitoringIntervalLabel(for trackedDomain: TrackedDomain) -> String { | |
| 152 | let state = DomainMonitoringScheduler.normalizedMonitoringState(for: trackedDomain, settings: monitoringSettings) | |
| 153 | return Self.intervalLabel(for: state.currentInterval) | |
| 154 | } | |
| 155 | ||
| 156 | func monitoringStatusLabel(for trackedDomain: TrackedDomain) -> String { | |
| 157 | let state = trackedDomain.monitoringState | |
| 158 | if let lastChangeDate = state.lastChangeDate, | |
| 159 | Date().timeIntervalSince(lastChangeDate) <= 6 * 60 * 60 { | |
| 160 | return "Recently Changed" | |
| 161 | } | |
| 162 | if state.consecutiveStableChecks >= 2 { | |
| 163 | return "Stable" | |
| 164 | } | |
| 165 | return trackedDomain.monitoringEnabled ? "Active" : "Paused" | |
| 166 | } | |
| 167 | ||
| 168 | private static func intervalLabel(for interval: TimeInterval) -> String { | |
| 169 | let formatter = DateComponentsFormatter() | |
| 170 | formatter.allowedUnits = interval < 3600 ? [.minute] : [.hour, .minute] | |
| 171 | formatter.unitsStyle = .full | |
| 172 | return formatter.string(from: interval) ?? "Unknown" | |
| 173 | } | |
| 174 | } | |
DomainDig/DomainViewModel.swift +2 −163
| @@ -1100,167 +1100,6 @@ final class DomainViewModel { | ||
| 1100 | 1100 | await refreshMonitoringAuthorizationStatus() |
| 1101 | 1101 | } |
| 1102 | 1102 | |
| 1103 | func refreshMonitoringAuthorizationStatus() async { | |
| 1104 | let settings = await UNUserNotificationCenter.current().notificationSettings() | |
| 1105 | monitoringNotificationStatus = settings.authorizationStatus | |
| 1106 | } | |
| 1107 | ||
| 1108 | func setMonitoringEnabled(_ isEnabled: Bool) { | |
| 1109 | guard !isEnabled || FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1110 | monitoringSettings.isEnabled = false | |
| 1111 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1112 | return | |
| 1113 | } | |
| 1114 | ||
| 1115 | monitoringSettings.isEnabled = isEnabled | |
| 1116 | persistMonitoringSettings(localActivationConfirmed: isEnabled) | |
| 1117 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 1118 | } | |
| 1119 | ||
| 1120 | func setMonitoringScope(_ scope: MonitoringScope) { | |
| 1121 | monitoringSettings.scope = scope | |
| 1122 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1123 | } | |
| 1124 | ||
| 1125 | func setMonitoringBaseInterval(_ baseInterval: MonitoringBaseInterval) { | |
| 1126 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1127 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1128 | return | |
| 1129 | } | |
| 1130 | monitoringSettings.baseInterval = baseInterval.interval | |
| 1131 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1132 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 1133 | } | |
| 1134 | ||
| 1135 | func setMonitoringAdaptiveEnabled(_ isEnabled: Bool) { | |
| 1136 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1137 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1138 | return | |
| 1139 | } | |
| 1140 | monitoringSettings.adaptiveEnabled = isEnabled | |
| 1141 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1142 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 1143 | } | |
| 1144 | ||
| 1145 | func setMonitoringSensitivity(_ sensitivity: MonitoringSensitivity) { | |
| 1146 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1147 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1148 | return | |
| 1149 | } | |
| 1150 | monitoringSettings.sensitivity = sensitivity | |
| 1151 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1152 | monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | |
| 1153 | } | |
| 1154 | ||
| 1155 | func setMonitoringQuietHours(startHour: Int, endHour: Int, isEnabled: Bool) { | |
| 1156 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1157 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1158 | return | |
| 1159 | } | |
| 1160 | monitoringSettings.quietHours = isEnabled ? QuietHours(startHour: startHour, endHour: endHour) : nil | |
| 1161 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1162 | } | |
| 1163 | ||
| 1164 | func setMonitoringAlertFilter(_ filter: MonitoringAlertFilter) { | |
| 1165 | guard FeatureAccessService.hasAccess(to: .localAlerts) else { | |
| 1166 | monitoringSettings.alertsEnabled = false | |
| 1167 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts) | |
| 1168 | return | |
| 1169 | } | |
| 1170 | monitoringSettings.alertFilter = filter | |
| 1171 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1172 | } | |
| 1173 | ||
| 1174 | func setMonitoringAlertsEnabled(_ isEnabled: Bool) { | |
| 1175 | guard !isEnabled || FeatureAccessService.hasAccess(to: .localAlerts) else { | |
| 1176 | monitoringSettings.alertsEnabled = false | |
| 1177 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .localAlerts) | |
| 1178 | return | |
| 1179 | } | |
| 1180 | monitoringSettings.alertsEnabled = isEnabled | |
| 1181 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1182 | } | |
| 1183 | ||
| 1184 | func setMonitoringSelection(for trackedDomain: TrackedDomain, isSelected: Bool) { | |
| 1185 | if isSelected { | |
| 1186 | if !monitoringSettings.selectedDomainIDs.contains(trackedDomain.id) { | |
| 1187 | monitoringSettings.selectedDomainIDs.append(trackedDomain.id) | |
| 1188 | } | |
| 1189 | } else { | |
| 1190 | monitoringSettings.selectedDomainIDs.removeAll { $0 == trackedDomain.id } | |
| 1191 | } | |
| 1192 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1193 | } | |
| 1194 | ||
| 1195 | func toggleMonitoring(for trackedDomain: TrackedDomain) { | |
| 1196 | guard canEdit(trackedDomain) else { return } | |
| 1197 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1198 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1199 | return | |
| 1200 | } | |
| 1201 | guard let index = trackedDomains.firstIndex(where: { $0.id == trackedDomain.id }) else { return } | |
| 1202 | trackedDomains[index].monitoringEnabled.toggle() | |
| 1203 | trackedDomains[index].updatedAt = Date() | |
| 1204 | MonitoringStorage.saveTrackedDomains(trackedDomains) | |
| 1205 | CloudSyncService.shared.scheduleSyncIfNeeded() | |
| 1206 | sanitizeMonitoringSelection() | |
| 1207 | } | |
| 1208 | ||
| 1209 | func requestMonitoringNotificationAuthorization() async { | |
| 1210 | let granted = await LocalNotificationService.shared.requestAuthorizationIfNeeded() | |
| 1211 | monitoringSettings.alertsEnabled = granted | |
| 1212 | persistMonitoringSettings(localActivationConfirmed: monitoringSettings.isEnabled) | |
| 1213 | await refreshMonitoringAuthorizationStatus() | |
| 1214 | } | |
| 1215 | ||
| 1216 | func runMonitoringNow() { | |
| 1217 | guard FeatureAccessService.hasAccess(to: .automatedMonitoring) else { | |
| 1218 | upgradePrompt = FeatureAccessService.upgradePrompt(for: .automatedMonitoring) | |
| 1219 | return | |
| 1220 | } | |
| 1221 | guard !monitoringRunInProgress else { return } | |
| 1222 | ||
| 1223 | monitoringRunInProgress = true | |
| 1224 | monitoringStatusMessage = nil | |
| 1225 | ||
| 1226 | Task { [weak self] in | |
| 1227 | guard let self else { return } | |
| 1228 | let outcome = await DomainMonitoringService.shared.performMonitoring( | |
| 1229 | trigger: .manual, | |
| 1230 | requireEnabledSetting: false | |
| 1231 | ) | |
| 1232 | await MainActor.run { | |
| 1233 | self.refreshMonitoringState() | |
| 1234 | self.monitoringRunInProgress = false | |
| 1235 | self.monitoringStatusMessage = outcome.message | |
| 1236 | } | |
| 1237 | } | |
| 1238 | } | |
| 1239 | ||
| 1240 | func monitoringIntervalLabel(for trackedDomain: TrackedDomain) -> String { | |
| 1241 | let state = DomainMonitoringScheduler.normalizedMonitoringState(for: trackedDomain, settings: monitoringSettings) | |
| 1242 | return Self.intervalLabel(for: state.currentInterval) | |
| 1243 | } | |
| 1244 | ||
| 1245 | func monitoringStatusLabel(for trackedDomain: TrackedDomain) -> String { | |
| 1246 | let state = trackedDomain.monitoringState | |
| 1247 | if let lastChangeDate = state.lastChangeDate, | |
| 1248 | Date().timeIntervalSince(lastChangeDate) <= 6 * 60 * 60 { | |
| 1249 | return "Recently Changed" | |
| 1250 | } | |
| 1251 | if state.consecutiveStableChecks >= 2 { | |
| 1252 | return "Stable" | |
| 1253 | } | |
| 1254 | return trackedDomain.monitoringEnabled ? "Active" : "Paused" | |
| 1255 | } | |
| 1256 | ||
| 1257 | private static func intervalLabel(for interval: TimeInterval) -> String { | |
| 1258 | let formatter = DateComponentsFormatter() | |
| 1259 | formatter.allowedUnits = interval < 3600 ? [.minute] : [.hour, .minute] | |
| 1260 | formatter.unitsStyle = .full | |
| 1261 | return formatter.string(from: interval) ?? "Unknown" | |
| 1262 | } | |
| 1263 | ||
| 1264 | 1103 | func rerunLookup(from entry: HistoryEntry, useSnapshotResolver: Bool) { |
| 1265 | 1104 | if useSnapshotResolver { |
| 1266 | 1105 | UserDefaults.standard.set(entry.resolverURLString, forKey: DNSResolverOption.userDefaultsKey) |
| @@ -2590,13 +2429,13 @@ final class DomainViewModel { | ||
| 2590 | 2429 | return existingMax + 1 |
| 2591 | 2430 | } |
| 2592 | 2431 | |
| 2593 | private func persistMonitoringSettings(localActivationConfirmed: Bool = false) { | |
| 2432 | func persistMonitoringSettings(localActivationConfirmed: Bool = false) { | |
| 2594 | 2433 | monitoringSettings = MonitoringStorage.sanitizeSettings(monitoringSettings, trackedDomains: trackedDomains) |
| 2595 | 2434 | MonitoringStorage.saveSettings(monitoringSettings) |
| 2596 | 2435 | CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: localActivationConfirmed) |
| 2597 | 2436 | } |
| 2598 | 2437 | |
| 2599 | private func sanitizeMonitoringSelection() { | |
| 2438 | func sanitizeMonitoringSelection() { | |
| 2600 | 2439 | monitoringSettings = MonitoringStorage.sanitizeSettings(monitoringSettings, trackedDomains: trackedDomains) |
| 2601 | 2440 | MonitoringStorage.saveSettings(monitoringSettings) |
| 2602 | 2441 | CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: false) |