.../Source/App/Services/History/HistoryRepo.swift
Line | Count | Source (jump to first uncovered line) |
1 | | // |
2 | | // HistoryRepo.swift |
3 | | // CutBox |
4 | | // |
5 | | // Created by Jason Milkins on 29/4/18. |
6 | | // Copyright © 2018-2023 ocodo. All rights reserved. |
7 | | // |
8 | | |
9 | | import Foundation |
10 | | |
11 | | class HistoryRepo { |
12 | | |
13 | | /// Clip items store (cached from/to defaults by history service) |
14 | 103 | private var store: [[String: String]] = [] |
15 | | |
16 | | /// dict keys of clip items |
17 | 103 | private var historyStoreKey = Constants.kHistoryStoreKey |
18 | 103 | private var stringKey = Constants.kStringKey |
19 | 103 | private var favoriteKey = Constants.kFavoriteKey |
20 | 103 | private var timestampKey = Constants.kTimestampKey |
21 | | |
22 | | /// Optional time filter |
23 | | var timeFilter: Double? |
24 | | |
25 | | /// preferences defaults key for protect favorites flag |
26 | | private var kProtectFavorites = "protectFavorites" |
27 | | |
28 | | /// Standalone predicate, false for item older than an earliest timestamp |
29 | 12 | func timeFilterPredicate(item: [String: String], earliest: String) -> Bool { |
30 | 12 | if let timestamp = item[self.timestampKey] { |
31 | 12 | return earliest < timestamp |
32 | 12 | } else { |
33 | 0 | return false |
34 | 0 | } |
35 | 0 | } |
36 | | |
37 | 516 | var items: [String] { |
38 | 516 | if let seconds = self.timeFilter { |
39 | 4 | let latest = secondsBeforeTimeNowAsISO8601(seconds: seconds) |
40 | 4 | return self.dict |
41 | 10 | .filter { timeFilterPredicate(item: $0, earliest: latest) } |
42 | 4 | .map { $0[self.stringKey]! } |
43 | 512 | } |
44 | 512 | |
45 | 512 | return self.dict |
46 | 3.71k | .map { $0[self.stringKey]! } |
47 | 516 | } |
48 | | |
49 | 342 | var favorites: [String] { |
50 | 342 | return self.dict |
51 | 2.86k | .filter { $0[favoriteKey] == self.favoriteKey } |
52 | 342 | .map { $0[self.stringKey]! } |
53 | 342 | } |
54 | | |
55 | 3 | var favoritesDict: [[String: String]] { |
56 | 3 | return self.dict |
57 | 40 | .filter { $0[favoriteKey] == self.favoriteKey } |
58 | 3 | } |
59 | | |
60 | 1.22k | var dict: [[String: String]] { |
61 | 1.22k | return self.store |
62 | 1.22k | } |
63 | | |
64 | | private var defaults: UserDefaults |
65 | | private var prefs: CutBoxPreferencesService |
66 | | |
67 | | init(defaults: UserDefaults = UserDefaults.standard, |
68 | 103 | prefs: CutBoxPreferencesService = CutBoxPreferencesService.shared) { |
69 | 103 | self.defaults = defaults |
70 | 103 | self.prefs = prefs |
71 | 103 | } |
72 | | |
73 | 4 | func clear() { |
74 | 4 | self.store.removeAll(protectFavorites: prefs.protectFavorites) |
75 | 4 | self.saveToDefaults() |
76 | 4 | } |
77 | | |
78 | 50 | func index(of string: String) -> Int? { |
79 | 50 | return items.firstIndex(of: string) |
80 | 50 | } |
81 | | |
82 | 1.45k | func insert(_ newElement: String, at index: Int = 0, isFavorite: Bool = false, date: Date? = Date()) { |
83 | 1.45k | var item = [stringKey: newElement] |
84 | 1.45k | |
85 | 1.45k | if let unwrappedDate = date { |
86 | 1.44k | let timestamp = iso8601Timestamp(fromDate: unwrappedDate) |
87 | 1.44k | item[self.timestampKey] = timestamp |
88 | 1.45k | } |
89 | 1.45k | |
90 | 1.45k | if isFavorite { |
91 | 6 | item[self.favoriteKey] = self.favoriteKey |
92 | 1.45k | } |
93 | 1.45k | |
94 | 1.45k | self.store.insert(item, at: index) |
95 | 1.45k | } |
96 | | |
97 | 4 | func remove(at: Int) { |
98 | 4 | if at >= 0 && self.store.count > at { |
99 | 4 | self.store.remove(at: at) |
100 | 4 | self.saveToDefaults() |
101 | 4 | } |
102 | 4 | } |
103 | | |
104 | 3 | func removeAtIndexes(indexes: IndexSet) { |
105 | 3 | self.store.removeAtIndexes(indexes: indexes) |
106 | 3 | self.saveToDefaults() |
107 | 3 | } |
108 | | |
109 | 335 | func findIndexSetOf(string: String) -> IndexSet { |
110 | 335 | var indexes = IndexSet() |
111 | 2.84k | for item in dict where item[stringKey] == string { |
112 | 2.84k | if let index = dict.firstIndex(of: item) { |
113 | 1 | indexes.insert(index) |
114 | 2.84k | } |
115 | 2.84k | } |
116 | 335 | return indexes |
117 | 335 | } |
118 | | |
119 | 5 | func toggleFavorite(indexes: IndexSet) { |
120 | 11 | for i in indexes { |
121 | 11 | toggleFavorite(at: i) |
122 | 11 | } |
123 | 5 | self.saveToDefaults() |
124 | 5 | } |
125 | | |
126 | 14 | func toggleFavorite(at i: Int) { |
127 | 14 | if self.store[i].keys.contains(favoriteKey) { |
128 | 6 | self.store[i].removeValue(forKey: favoriteKey) |
129 | 14 | } else { |
130 | 8 | self.store[i][favoriteKey] = favoriteKey |
131 | 14 | } |
132 | 14 | } |
133 | | |
134 | 4 | func removeSubrange(_ bounds: Range<Int>) { |
135 | 4 | self.store.removeSubrange(bounds) |
136 | 4 | self.saveToDefaults() |
137 | 4 | } |
138 | | |
139 | 13 | func migrateLegacyPasteStore(_ newElements: [String], at: Int = 0) { |
140 | 50 | newElements.reversed().forEach { |
141 | 50 | if index(of: $0) == nil { |
142 | 36 | self.insert($0) |
143 | 50 | } |
144 | 50 | } |
145 | 13 | } |
146 | | |
147 | | /// Used to read user defaults to in memory history store |
148 | 93 | func loadFromDefaults() { |
149 | 93 | let key = self.historyStoreKey |
150 | 93 | if let historyStore = self.defaults.array(forKey: key) as? [[String: String]] { |
151 | 55 | self.store = historyStore |
152 | 93 | } else { |
153 | 38 | self.store = [] |
154 | 93 | } |
155 | 93 | } |
156 | | |
157 | | /// Used to persist in memory history store to user defaults |
158 | 372 | func saveToDefaults() { |
159 | 372 | self.defaults.set(self.store, forKey: self.historyStoreKey) |
160 | 372 | } |
161 | | |
162 | | /// removes the historyStore completely |
163 | 4 | func clearHistory() { |
164 | 4 | self.clear() |
165 | 4 | } |
166 | | |
167 | | /// Clear the history using a timestampPredicate |
168 | | /// |
169 | | /// Favorites are protected when `prefs.protectFavorites` is enabled |
170 | | /// |
171 | | /// Items without a timestamp will be ignored. |
172 | | /// |
173 | | /// see also: `historyOffsetPredicateFactory(offset: TimeInterval?)` |
174 | 5 | func clearHistory(timestampPredicate: (String) -> Bool) { |
175 | 13 | let keep = self.store.filter { |
176 | 13 | if $0[favoriteKey] != nil { |
177 | 3 | if prefs.protectFavorites { |
178 | 2 | return true |
179 | 2 | } |
180 | 11 | } |
181 | 11 | |
182 | 11 | if let timestamp: String = $0[timestampKey] { |
183 | 9 | return !timestampPredicate(timestamp) |
184 | 9 | } |
185 | 2 | |
186 | 2 | return true |
187 | 11 | } |
188 | 5 | |
189 | 5 | self.store = keep |
190 | 5 | saveToDefaults() |
191 | 5 | } |
192 | | |
193 | | /// Size of history repo in bytes |
194 | 51 | func bytes() throws -> Int { |
195 | 51 | let data = try PropertyListSerialization.data( |
196 | 51 | fromPropertyList: store, |
197 | 51 | format: .binary, |
198 | 51 | options: .bitWidth) |
199 | 51 | |
200 | 51 | return data.count |
201 | 51 | } |
202 | | |
203 | | /// Bytes formatter |
204 | 51 | func bytesFormatted() -> String { |
205 | 51 | do { |
206 | 51 | let i: Int = try bytes() |
207 | 51 | let byteFormatter = ByteCountFormatter() |
208 | 51 | byteFormatter.allowedUnits = [.useGB, .useMB, .useKB] |
209 | 51 | return byteFormatter.string(fromByteCount: Int64(i)) |
210 | 51 | } catch { |
211 | 0 | return "" |
212 | 0 | } |
213 | 0 | } |
214 | | } |