@@ -7,78 +7,191 @@ final class FilterListStore {
77 private( set) var isLoading = false
88 private( set) var lastUpdated : Date ?
99 private( set) var error : String ?
10-
11- private let listURL = URL ( string: " https://raw.githubusercontent.com/DandelionSprout/adfilt/master/LegitimateURLShortener.txt " ) !
10+
11+ private( set) var sources : [ FilterSource ] = [ ]
12+ var customRulesText : String = " "
13+
1214 private let parser = FilterListParser ( )
1315 private let logger = Logger ( subsystem: " dev.sweet.diva.nutcracker " , category: " FilterListStore " )
14-
16+
17+ // MARK: - Cache paths
18+
1519 private var cacheDirectory : URL {
1620 let appSupport = FileManager . default. urls ( for: . applicationSupportDirectory, in: . userDomainMask) . first!
1721 return appSupport. appendingPathComponent ( " nutcracker " , isDirectory: true )
1822 }
19-
20- private var cacheFileURL : URL {
21- cacheDirectory. appendingPathComponent ( " LegitimateURLShortener.txt " )
23+
24+ private var sourcesFileURL : URL {
25+ cacheDirectory. appendingPathComponent ( " sources.json " )
2226 }
23-
27+
28+ private var customRulesFileURL : URL {
29+ cacheDirectory. appendingPathComponent ( " customRules.txt " )
30+ }
31+
2432 private var metadataURL : URL {
2533 cacheDirectory. appendingPathComponent ( " metadata.json " )
2634 }
27-
35+
36+ private func cacheFileURL( for source: FilterSource ) -> URL {
37+ cacheDirectory. appendingPathComponent ( " cache_ \( source. id. uuidString) .txt " )
38+ }
39+
40+ // MARK: - Public API
41+
2842 func loadOrFetch( ) async {
29- // Try loading from cache first
30- if let cached = loadFromCache ( ) {
31- rules = parser. parse ( cached)
32- logger. info ( " Loaded \( self . rules. count) rules from cache " )
33-
34- // Check if refresh needed (older than 24h)
35- if let lastUpdated, Date ( ) . timeIntervalSince ( lastUpdated) < 86400 {
36- return
37- }
43+ loadSavedState ( )
44+
45+ if sources. isEmpty {
46+ sources. append ( . defaultSource)
47+ saveSources ( )
3848 }
39-
40- await fetchFromRemote ( )
49+
50+ reparseRules ( )
51+ logger. info ( " Loaded \( self . rules. count) rules from cache " )
52+
53+ if let lastUpdated, Date ( ) . timeIntervalSince ( lastUpdated) < 86400 {
54+ return
55+ }
56+
57+ await refreshAllSources ( )
4158 }
42-
43- func fetchFromRemote ( ) async {
59+
60+ func refreshAllSources ( ) async {
4461 isLoading = true
4562 error = nil
4663 defer { isLoading = false }
47-
48- do {
49- let ( data, _) = try await URLSession . shared. data ( from: listURL)
50- guard let text = String ( data: data, encoding: . utf8) else {
51- error = " Invalid encoding "
52- return
64+
65+ for source in sources where source. isEnabled {
66+ do {
67+ try await fetchSource ( source)
68+ } catch {
69+ self . error = " Some lists failed to update "
70+ logger. error ( " Failed to fetch \( source. name) : \( error. localizedDescription) " )
5371 }
54-
55- // Save to cache
56- try saveToCache ( text)
57-
58- // Parse rules
59- let newRules = parser. parse ( text)
60- rules = newRules
61- lastUpdated = Date ( )
62- saveMetadata ( )
63-
64- logger. info ( " Fetched and parsed \( newRules. count) rules from remote " )
65- } catch {
66- self . error = error. localizedDescription
67- logger. error ( " Failed to fetch filter list: \( error. localizedDescription) " )
6872 }
73+
74+ lastUpdated = Date ( )
75+ saveMetadata ( )
76+ reparseRules ( )
77+ logger. info ( " Refreshed all sources, \( self . rules. count) total rules " )
6978 }
70-
71- private func loadFromCache( ) -> String ? {
72- loadMetadata ( )
73- guard FileManager . default. fileExists ( atPath: cacheFileURL. path) else { return nil }
74- return try ? String ( contentsOf: cacheFileURL, encoding: . utf8)
79+
80+ func addSource( name: String , url: String ) {
81+ let source = FilterSource ( name: name, url: url)
82+ sources. append ( source)
83+ saveSources ( )
84+
85+ Task {
86+ isLoading = true
87+ defer { isLoading = false }
88+ do {
89+ try await fetchSource ( source)
90+ reparseRules ( )
91+ } catch {
92+ logger. error ( " Failed to fetch new source \( name) : \( error. localizedDescription) " )
93+ }
94+ }
95+ }
96+
97+ func removeSource( id: UUID ) {
98+ guard let index = sources. firstIndex ( where: { $0. id == id } ) else { return }
99+ let source = sources [ index]
100+ try ? FileManager . default. removeItem ( at: cacheFileURL ( for: source) )
101+ sources. remove ( at: index)
102+ saveSources ( )
103+ reparseRules ( )
104+ }
105+
106+ func toggleSource( id: UUID ) {
107+ guard let index = sources. firstIndex ( where: { $0. id == id } ) else { return }
108+ sources [ index] . isEnabled. toggle ( )
109+ saveSources ( )
110+ reparseRules ( )
111+ }
112+
113+ func applyCustomRules( ) {
114+ saveCustomRules ( )
115+ reparseRules ( )
116+ }
117+
118+ // MARK: - Parsing
119+
120+ private func reparseRules( ) {
121+ var allRules : [ RemoveParamRule ] = [ ]
122+
123+ for source in sources where source. isEnabled {
124+ if let cached = try ? String ( contentsOf: cacheFileURL ( for: source) , encoding: . utf8) {
125+ allRules. append ( contentsOf: parser. parse ( cached) )
126+ }
127+ }
128+
129+ let trimmed = customRulesText. trimmingCharacters ( in: . whitespacesAndNewlines)
130+ if !trimmed. isEmpty {
131+ allRules. append ( contentsOf: parser. parse ( customRulesText) )
132+ }
133+
134+ rules = allRules
135+ }
136+
137+ // MARK: - Networking
138+
139+ private func fetchSource( _ source: FilterSource ) async throws {
140+ guard let url = URL ( string: source. url) else {
141+ throw URLError ( . badURL)
142+ }
143+
144+ let ( data, _) = try await URLSession . shared. data ( from: url)
145+ guard let text = String ( data: data, encoding: . utf8) else {
146+ throw URLError ( . cannotDecodeContentData)
147+ }
148+
149+ try ensureCacheDirectory ( )
150+ try text. write ( to: cacheFileURL ( for: source) , atomically: true , encoding: . utf8)
75151 }
76-
77- private func saveToCache( _ text: String ) throws {
152+
153+ // MARK: - Persistence
154+
155+ private func ensureCacheDirectory( ) throws {
78156 try FileManager . default. createDirectory ( at: cacheDirectory, withIntermediateDirectories: true )
79- try text. write ( to: cacheFileURL, atomically: true , encoding: . utf8)
80157 }
81-
158+
159+ private func loadSavedState( ) {
160+ loadMetadata ( )
161+ loadSources ( )
162+ loadCustomRules ( )
163+ }
164+
165+ private func loadSources( ) {
166+ guard let data = try ? Data ( contentsOf: sourcesFileURL) ,
167+ let decoded = try ? JSONDecoder ( ) . decode ( [ FilterSource ] . self, from: data)
168+ else { return }
169+ sources = decoded
170+ }
171+
172+ private func saveSources( ) {
173+ do {
174+ try ensureCacheDirectory ( )
175+ let data = try JSONEncoder ( ) . encode ( sources)
176+ try data. write ( to: sourcesFileURL, options: . atomic)
177+ } catch {
178+ logger. error ( " Failed to save sources: \( error. localizedDescription) " )
179+ }
180+ }
181+
182+ private func loadCustomRules( ) {
183+ customRulesText = ( try ? String ( contentsOf: customRulesFileURL, encoding: . utf8) ) ?? " "
184+ }
185+
186+ private func saveCustomRules( ) {
187+ do {
188+ try ensureCacheDirectory ( )
189+ try customRulesText. write ( to: customRulesFileURL, atomically: true , encoding: . utf8)
190+ } catch {
191+ logger. error ( " Failed to save custom rules: \( error. localizedDescription) " )
192+ }
193+ }
194+
82195 private func saveMetadata( ) {
83196 let meta : [ String : String ] = [
84197 " lastUpdated " : ISO8601DateFormatter ( ) . string ( from: lastUpdated ?? Date ( ) )
@@ -87,7 +200,7 @@ final class FilterListStore {
87200 try ? data. write ( to: metadataURL)
88201 }
89202 }
90-
203+
91204 private func loadMetadata( ) {
92205 guard let data = try ? Data ( contentsOf: metadataURL) ,
93206 let meta = try ? JSONDecoder ( ) . decode ( [ String : String ] . self, from: data) ,
0 commit comments