fancy new browser
1import Foundation
2import MereKit
3import Combine
4
5/// Manages ad blocking state across both engine contexts.
6/// Lives on WindowViewModel; drives both WebKitAdBlocker and ChromiumAdBlocker.
7@MainActor
8public final class AdBlockController: ObservableObject {
9
10 @Published public private(set) var isEnabled: Bool = true
11 @Published public private(set) var isLoading: Bool = false
12 @Published public private(set) var loadedLists: [String: Int] = [:]
13 @Published public private(set) var error: String?
14
15 private let engines: [any AdBlockEngine]
16
17 public init(engines: [any AdBlockEngine]) {
18 self.engines = engines
19 }
20
21 // MARK: - Control
22
23 public func setEnabled(_ enabled: Bool) {
24 isEnabled = enabled
25 engines.forEach { $0.isEnabled = enabled }
26 }
27
28 // MARK: - List management
29
30 /// Load the default lists (EasyList + EasyPrivacy).
31 public func loadDefaults() async {
32 await load(from: BlockListSource.easyList, name: "EasyList")
33 await load(from: BlockListSource.easyPrivacy, name: "EasyPrivacy")
34 }
35
36 /// Fetch a list from a URL and load it into all engines.
37 public func load(from url: URL, name: String) async {
38 isLoading = true
39 error = nil
40 do {
41 let (data, _) = try await URLSession.shared.data(from: url)
42 let text = String(decoding: data, as: UTF8.self)
43 let list = EasyListParser.parse(text, name: name)
44 for engine in engines {
45 try await engine.load(list)
46 }
47 loadedLists[name] = list.blockCount
48 } catch {
49 self.error = "\(name): \(error.localizedDescription)"
50 }
51 isLoading = false
52 }
53
54 public func remove(listNamed name: String) async {
55 for engine in engines { await engine.remove(listNamed: name) }
56 loadedLists.removeValue(forKey: name)
57 }
58
59 public var totalRuleCount: Int { loadedLists.values.reduce(0, +) }
60 public var totalBlockedCount: Int { engines.map(\.totalBlockedRequestCount).reduce(0, +) }
61}