Skip to content

Commit 83805c2

Browse files
authored
fix custom phrase edit interrupted caused by id change (#302)
1 parent 00e738b commit 83805c2

File tree

1 file changed

+21
-40
lines changed

1 file changed

+21
-40
lines changed

src/config/customphrase.swift

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,15 @@ let pinyinPath = pinyinLocalDir.localPath()
88
let customphrase = pinyinLocalDir.appendingPathComponent("customphrase")
99
let nativeCustomPhrase = cacheDir.appendingPathComponent("customphrase.plist")
1010

11-
struct CustomPhrase: Identifiable, Hashable {
12-
var id: Int {
13-
var hasher = Hasher()
14-
hasher.combine(keyword)
15-
hasher.combine(phrase)
16-
return hasher.finalize()
17-
}
11+
struct CustomPhrase: Identifiable {
12+
let id = UUID() // To support uninterrupted in-place edit, id can't be hash of content.
1813
var keyword: String
1914
var phrase: String
2015
var order: Int
16+
var enabled: Bool
2117
}
2218

23-
private func parseLine(_ s: String) -> (CustomPhrase, Bool)? {
19+
private func parseLine(_ s: String) -> CustomPhrase? {
2420
let regex = try! NSRegularExpression(pattern: "(\\S+),(-?\\d+)=(.+)", options: [])
2521
let matches = regex.matches(
2622
in: s, options: [], range: NSRange(location: 0, length: s.utf16.count))
@@ -29,41 +25,35 @@ private func parseLine(_ s: String) -> (CustomPhrase, Bool)? {
2925
let keyword = String(s[Range(match.range(at: 1), in: s)!])
3026
let order = Int(String(s[Range(match.range(at: 2), in: s)!])) ?? 0
3127
let phrase = String(s[Range(match.range(at: 3), in: s)!])
32-
return (CustomPhrase(keyword: keyword, phrase: phrase, order: abs(order)), order > 0)
28+
return CustomPhrase(keyword: keyword, phrase: phrase, order: abs(order), enabled: order > 0)
3329
}
3430
return nil
3531
}
3632

37-
private func stringToCustomPhrases(_ s: String) -> [(CustomPhrase, Bool)] {
33+
private func stringToCustomPhrases(_ s: String) -> [CustomPhrase] {
3834
return s.split(separator: "\n").compactMap { line in
3935
parseLine(String(line))
4036
}
4137
}
4238

4339
private func customPhrasesToString(_ customphraseVM: CustomPhraseVM) -> String {
4440
return customphraseVM.customPhrases.map { customPhrase in
45-
"\(customPhrase.keyword),\(customphraseVM.isEnabled[customPhrase.id] ?? true ? "" : "-")\(customPhrase.order)=\(customPhrase.phrase)"
41+
"\(customPhrase.keyword),\(customPhrase.enabled ? "" : "-")\(customPhrase.order)=\(customPhrase.phrase)"
4642
}.joined(separator: "\n")
4743
}
4844

4945
class CustomPhraseVM: ObservableObject {
50-
@Published var customPhrases: [CustomPhrase] = []
51-
@Published var isEnabled: [Int: Bool] = [:]
46+
@Published var customPhrases = [CustomPhrase]()
5247

5348
func refreshItems() {
54-
customPhrases = []
55-
isEnabled = [:]
56-
for (customPhrase, enabled) in stringToCustomPhrases(readUTF8(customphrase) ?? "") {
57-
customPhrases.append(customPhrase)
58-
isEnabled[customPhrase.id] = enabled
59-
}
49+
customPhrases = stringToCustomPhrases(readUTF8(customphrase) ?? "")
6050
}
6151
}
6252

6353
struct CustomPhraseView: View {
6454
@Environment(\.presentationMode) var presentationMode
6555

66-
@State private var selectedRows = Set<Int>()
56+
@State private var selectedRows = Set<UUID>()
6757
@ObservedObject private var customphraseVM = CustomPhraseVM()
6858
@State private var showReloaded = false
6959
@State private var importedPhrases = 0
@@ -108,15 +98,7 @@ struct CustomPhraseView: View {
10898
.font(.headline)
10999
ForEach($customphraseVM.customPhrases) { $customPhrase in
110100
HStack(alignment: .center) {
111-
Toggle(
112-
"",
113-
isOn: Binding(
114-
get: { customphraseVM.isEnabled[customPhrase.id] ?? true },
115-
set: {
116-
customphraseVM.isEnabled[customPhrase.id] = $0
117-
}
118-
)
119-
).frame(width: checkboxColumnWidth)
101+
Toggle("", isOn: $customPhrase.enabled).frame(width: checkboxColumnWidth)
120102
TextField("Keyword", text: $customPhrase.keyword).frame(
121103
minWidth: minKeywordColumnWidth, maxWidth: .infinity, alignment: .leading)
122104
TextField("Phrase", text: $customPhrase.phrase).frame(
@@ -141,15 +123,18 @@ struct CustomPhraseView: View {
141123
"/bin/zsh",
142124
["-c", "/usr/bin/defaults export -g - > \(quote(nativeCustomPhrase.localPath()))"])
143125
{
144-
let phrases = Set(customphraseVM.customPhrases)
126+
let phrasesMap = customphraseVM.customPhrases.reduce(into: [String: [CustomPhrase]]()) {
127+
result, customPhrase in
128+
result[customPhrase.keyword, default: []].append(customPhrase)
129+
}
145130
importedPhrases = 0
146131
for (shortcut, phrase) in parseCustomPhraseXML(nativeCustomPhrase) {
147-
let newItem = CustomPhrase(keyword: shortcut, phrase: phrase, order: 1)
148-
if !phrases.contains(newItem) {
149-
customphraseVM.isEnabled[newItem.id] = true
150-
customphraseVM.customPhrases.append(newItem)
151-
importedPhrases += 1
132+
if let array = phrasesMap[shortcut], array.contains(where: { $0.phrase == phrase }) {
133+
continue
152134
}
135+
let newItem = CustomPhrase(keyword: shortcut, phrase: phrase, order: 1, enabled: true)
136+
customphraseVM.customPhrases.append(newItem)
137+
importedPhrases += 1
153138
}
154139
if save() {
155140
showImportedPhrases = true
@@ -163,8 +148,7 @@ struct CustomPhraseView: View {
163148
}
164149

165150
Button {
166-
let newItem = CustomPhrase(keyword: "", phrase: "", order: 1)
167-
customphraseVM.isEnabled[newItem.id] = true
151+
let newItem = CustomPhrase(keyword: "", phrase: "", order: 1, enabled: true)
168152
customphraseVM.customPhrases.append(newItem)
169153
selectedRows = [newItem.id]
170154
} label: {
@@ -175,9 +159,6 @@ struct CustomPhraseView: View {
175159
customphraseVM.customPhrases.removeAll {
176160
selectedRows.contains($0.id)
177161
}
178-
customphraseVM.isEnabled = customphraseVM.isEnabled.filter { id, _ in
179-
!selectedRows.contains(id)
180-
}
181162
selectedRows.removeAll()
182163
} label: {
183164
Text("Remove items")

0 commit comments

Comments
 (0)