SettingsItem.swift (9283B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 10 struct SettingsImage: View { 11 let imageName: String? 12 13 @ScaledMetric var imgSize: CGFloat = 32 // sys image relative to fontSize 14 15 var hasImage: Bool { 16 if let imageName { 17 return UIImage(named: imageName) != nil 18 } 19 return false 20 } 21 22 var hasSysImage: Bool { 23 if let imageName { 24 return UIImage(systemName: imageName) != nil 25 } 26 return false 27 } 28 29 var body: some View { 30 if hasImage { 31 let imageSize = imgSize * 5 / 4 // Logos have space around them, thus need to be bigger 32 Image(imageName!) 33 .resizable() 34 .scaledToFit() 35 .frame(width: imageSize, height: imageSize) 36 } else if hasSysImage { 37 Image(systemName: imageName!) 38 .resizable() 39 .scaledToFit() 40 .frame(width: imgSize, height: imgSize) 41 .padding(.horizontal, 2) 42 } else { 43 EmptyView() 44 } 45 } 46 } 47 48 struct SettingsDescription: View { 49 let id1: String? 50 let description: String? 51 52 @AppStorage("minimalistic") var minimalistic: Bool = false 53 54 var body: some View { 55 if !minimalistic { 56 if let desc = description { 57 Text(desc) 58 .id(id1 == nil ? nil : id1! + "_T") 59 .frame(maxWidth: .infinity, alignment: .leading) 60 .talerFont(.caption) 61 } 62 } 63 } 64 } 65 // MARK: - 66 struct SettingsBase<Content: View>: View { 67 let id1: String? 68 let description: String? 69 let content: () -> Content 70 71 init(id1: String?, description: String? = nil, @ViewBuilder content: @escaping () -> Content) { 72 self.id1 = id1 73 self.description = description 74 self.content = content 75 } 76 77 var body: some View { 78 let isWeb = id1?.hasPrefix("web") ?? false 79 let foreColor = isWeb ? Color.accentColor 80 : .primary 81 VStack { 82 content() 83 .id(id1) 84 .frame(maxWidth: .infinity, alignment: .leading) 85 .foregroundColor(foreColor) 86 .talerFont(.title3) 87 .padding([.bottom], 0.01) 88 SettingsDescription(id1: id1, description: description) 89 }.id(id1 == nil ? nil : id1! + "_V") 90 } 91 } 92 // MARK: - 93 struct SettingsItem<Content: View>: View { 94 let name: String 95 let id1: String? 96 let imageName: String? 97 let description: String? 98 let content: () -> Content 99 100 init(name: String, id1: String, imageName: String? = nil, description: String? = nil, 101 @ViewBuilder content: @escaping () -> Content 102 ) { 103 self.name = name 104 self.id1 = id1 105 self.imageName = imageName 106 self.description = description 107 self.content = content 108 } 109 110 var body: some View { 111 HStack { 112 SettingsImage(imageName: imageName) 113 114 SettingsBase(id1: id1, description: description) { 115 Text(name) 116 } 117 content() 118 .talerFont(.body) 119 }.id(id1 == nil ? nil : id1! + "_H") 120 .accessibilityElement(children: .combine) 121 .padding([.bottom], 4) 122 } 123 } 124 // MARK: - 125 struct SettingsToggle: View { 126 let name: String 127 @Binding var value: Bool 128 let id1: String? 129 let imageName: String? 130 let description: String? 131 let action: (_ newValue: Bool) -> Void 132 133 init(name: String, value: Binding<Bool>, id1: String? = nil, 134 imageName: String? = nil, 135 description: String? = nil, 136 action: @escaping (_ newValue: Bool) -> Void = {_ in} 137 ) { 138 self.name = name 139 self._value = value 140 self.id1 = id1 141 self.imageName = imageName 142 self.description = description 143 self.action = action 144 } 145 146 var body: some View { 147 let accLabel: String = if let description { 148 name + ", " + description 149 } else { 150 name 151 } 152 HStack { 153 SettingsImage(imageName: imageName) 154 155 SettingsBase(id1: id1, description: description) { 156 Toggle(name, isOn: $value.animation()) 157 .accessibility(sortPriority: 1) 158 .onChange(of: value) { value in 159 action(value) 160 } 161 } 162 }.id(id1 == nil ? nil : id1! + "_H") 163 .accessibilityElement(children: .combine) 164 .accessibilityLabel(accLabel) 165 .padding([.bottom], 4) 166 } 167 } 168 // MARK: - 169 struct SettingsFont: View { 170 let title: String 171 let value: Int 172 let action: (Int) -> Void 173 174 @State private var selectedFont = 0 175 let fonts = [String(localized: "Standard iOS Font"), "Atkinson-Hyperlegible", "Nunito"] 176 177 var body: some View { 178 Picker(title, selection: $selectedFont, content: { 179 ForEach(0..<fonts.count, id: \.self, content: { index in 180 Text(fonts[index]).tag(index) 181 }) 182 }) 183 .talerFont(.title2) 184 .pickerStyle(.menu) 185 .onAppear() { 186 withAnimation { selectedFont = value } 187 } 188 .onChange(of: selectedFont) { selectedF in 189 action(selectedF) 190 } 191 } 192 } 193 // MARK: - 194 struct SettingsStyle: View { 195 let title: String 196 @Binding var myListStyle: MyListStyle 197 198 var body: some View { 199 HStack { 200 Text(title) 201 .talerFont(.title2) 202 Spacer() 203 Picker(selection: $myListStyle) { 204 ForEach(MyListStyle.allCases, id: \.self) { 205 Text($0.displayName.capitalized).tag($0) 206 .talerFont(.title2) 207 } 208 } label: {} 209 .pickerStyle(.menu) 210 // .frame(alignment: .trailing) 211 // .background(WalletColors().buttonBackColor(pressed: false, disabled: false)) TODO: RoundRect 212 } 213 .accessibilityElement(children: .combine) 214 } 215 } 216 // MARK: - 217 struct SettingsTriState: View { 218 var name: String 219 @Binding var value: Int 220 let id1: String? 221 var description: String? 222 var action: (_ value: Int) -> Void = {value in } 223 224 func imageName1(_ value: Int) -> (String, String) { 225 return (value == 0) ? ("eye.slash", "Off") 226 : (value == 1) ? ("eye", "Type only") 227 : ("eye.fill", "Type and JSON") 228 } 229 func imageName2(_ value: Int) -> (String, String) { 230 return (value == 0) ? ("iphone", "System") // or apple.logo 231 : (value == 1) ? (LIGHT_OFF, "light") // 232 : (LIGHT_ON, "dark") // 233 } 234 235 var body: some View { 236 let observe = "observe" == id1 237 let image = observe ? imageName1(value) : imageName2(value) 238 239 VStack { 240 HStack { 241 Text(name) 242 .talerFont(.title2) 243 Spacer() 244 Text(verbatim: " ") 245 .talerFont(.largeTitle) 246 Button { 247 if value > 0 { 248 value = -1 249 action(value) 250 if observe { 251 Controller.shared.playSound(1) 252 } 253 } else { 254 value = value + 1 255 action(value) 256 if observe { 257 Controller.shared.playSound(value) 258 } 259 } 260 } label: { 261 Image(systemName: image.0) 262 .talerFont(.largeTitle) 263 } 264 } 265 266 SettingsDescription(id1: id1, description: description) 267 } 268 .accessibilityElement(children: .combine) 269 .accessibilityLabel(name) 270 .accessibility(value: Text(image.1)) 271 .accessibilityHint(description ?? EMPTYSTRING) 272 .padding([.bottom], 4) 273 } 274 } 275 // MARK: - 276 #if DEBUG 277 struct SettingsItemPreview : View { 278 @State var developerMode: Bool = false 279 @State var observe: Int = 0 280 281 var body: some View { 282 VStack { 283 SettingsToggle(name: "Developer Preview", value: $developerMode, id1: "dev1", 284 description: "More information intended for debugging") 285 SettingsTriState(name: "Observe walletCore", value: $observe, 286 id1: "observe", 287 description: "on LocalConsole") 288 } 289 } 290 } 291 292 struct SettingsItem_Previews: PreviewProvider { 293 static var previews: some View { 294 List { 295 SettingsItem(name: "Exchanges", id1: "list", 296 description: "Manage list of exchanges known to this wallet") {} 297 SettingsItemPreview() 298 SettingsItem(name: "Save Logfile", id1: "save", 299 description: "Help debugging wallet-core") { 300 Button("Save") { } 301 .buttonStyle(.bordered) 302 .disabled(true) 303 } 304 } 305 } 306 } 307 #endif