@@ -8,19 +8,15 @@ let pinyinPath = pinyinLocalDir.localPath()
88let customphrase = pinyinLocalDir. appendingPathComponent ( " customphrase " )
99let 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
4339private 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
4945class 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
6353struct 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