SettingsView.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import Foundation
  2. import SwiftUI
  3. struct SettingsView: View {
  4. @EnvironmentObject private var store: Store
  5. var body: some View {
  6. NavigationView {
  7. Form {
  8. /*Section(header: Text("General")) {
  9. NavigationLink(destination: UsersView()) {
  10. Text("Manage users")
  11. }
  12. }*/
  13. Section(
  14. header: Text("Users"),
  15. footer: Text("To access read-protected topics, you may add or edit users here. All topics for a given server will use the same user.")
  16. ) {
  17. UsersView()
  18. }
  19. Section(header: Text("About")) {
  20. HStack {
  21. Text("Version")
  22. .foregroundColor(.gray)
  23. Spacer()
  24. Text("ntfy \(Config.version) (\(Config.build))")
  25. }
  26. }
  27. }
  28. .navigationTitle("Settings")
  29. }
  30. .navigationViewStyle(StackNavigationViewStyle())
  31. }
  32. }
  33. struct UsersView: View {
  34. @EnvironmentObject private var store: Store
  35. @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \User.baseUrl, ascending: true)]) var users: FetchedResults<User>
  36. @State private var selectedUser: User?
  37. @State private var showDialog = false
  38. @State private var baseUrl: String = ""
  39. @State private var username: String = ""
  40. @State private var password: String = ""
  41. var body: some View {
  42. let _ = selectedUser?.username // Workaround for FB7823148, see https://developer.apple.com/forums/thread/652080
  43. List {
  44. ForEach(users) { user in
  45. UserRowView(user: user)
  46. .onTapGesture {
  47. selectedUser = user
  48. baseUrl = user.baseUrl ?? "?"
  49. username = user.username ?? "?"
  50. showDialog = true
  51. }
  52. }
  53. HStack {
  54. Image(systemName: "plus")
  55. Text("Add user")
  56. }
  57. .padding(.all, 4)
  58. .onTapGesture {
  59. showDialog = true
  60. }
  61. }
  62. .sheet(isPresented: $showDialog) {
  63. NavigationView {
  64. Form {
  65. Section(
  66. footer: (selectedUser == nil)
  67. ? Text("You can add a user here. All topics for the given server will use this user.")
  68. : Text("Edit the username or password for \(shortUrl(url: baseUrl)) here. This user is used for all topics of this server. Leave the password blank to leave it unchanged.")
  69. ) {
  70. if selectedUser == nil {
  71. TextField("Service URL, e.g. https://ntfy.home.io", text: $baseUrl)
  72. .disableAutocapitalization()
  73. .disableAutocorrection(true)
  74. }
  75. TextField("Username", text: $username)
  76. .disableAutocapitalization()
  77. .disableAutocorrection(true)
  78. SecureField("Password", text: $password)
  79. }
  80. }
  81. .navigationTitle(selectedUser == nil ? "Add user" : "Edit user")
  82. .navigationBarTitleDisplayMode(.inline)
  83. .toolbar {
  84. ToolbarItem(placement: .navigationBarLeading) {
  85. // Sigh, for iOS 14 we need to add a "Delete" menu item, because it doesn't support
  86. // swipe actions. Quite annoying.
  87. if #available(iOS 15.0, *) {
  88. Button(action: cancelAction) {
  89. Text("Cancel")
  90. }
  91. } else {
  92. if selectedUser == nil {
  93. Button("Cancel") {
  94. cancelAction()
  95. }
  96. } else {
  97. Menu {
  98. Button("Cancel") {
  99. cancelAction()
  100. }
  101. Button("Delete") {
  102. deleteAction()
  103. }
  104. } label: {
  105. Image(systemName: "ellipsis.circle")
  106. .padding([.leading], 40)
  107. }
  108. }
  109. }
  110. }
  111. ToolbarItem(placement: .navigationBarTrailing) {
  112. Button(action: saveAction) {
  113. Text("Save")
  114. }
  115. .disabled(!isValid())
  116. }
  117. }
  118. }
  119. }
  120. }
  121. private func saveAction() {
  122. var password = password
  123. if let user = selectedUser, password == "" {
  124. password = user.password ?? "?" // If password is blank, leave unchanged
  125. }
  126. store.saveUser(baseUrl: baseUrl, username: username, password: password)
  127. resetAndHide()
  128. }
  129. private func cancelAction() {
  130. resetAndHide()
  131. }
  132. private func deleteAction() {
  133. store.delete(user: selectedUser!)
  134. resetAndHide()
  135. }
  136. private func isValid() -> Bool {
  137. if selectedUser == nil { // New user
  138. if baseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil {
  139. return false
  140. } else if username.isEmpty || password.isEmpty {
  141. return false
  142. } else if store.getUser(baseUrl: baseUrl) != nil {
  143. return false
  144. }
  145. } else { // Existing user
  146. if username.isEmpty {
  147. return false
  148. }
  149. }
  150. return true
  151. }
  152. private func resetAndHide() {
  153. showDialog = false
  154. DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
  155. // Hide first and then reset, otherwise we'll see the text fields change
  156. selectedUser = nil
  157. baseUrl = ""
  158. username = ""
  159. password = ""
  160. }
  161. }
  162. }
  163. struct UserRowView: View {
  164. @EnvironmentObject private var store: Store
  165. @ObservedObject var user: User
  166. var body: some View {
  167. if #available(iOS 15.0, *) {
  168. userRow
  169. .swipeActions(edge: .trailing) {
  170. Button(role: .destructive) {
  171. store.delete(user: user)
  172. } label: {
  173. Label("Delete", systemImage: "trash.circle")
  174. }
  175. }
  176. } else {
  177. userRow
  178. }
  179. }
  180. private var userRow: some View {
  181. HStack {
  182. Image(systemName: "person.fill")
  183. VStack(alignment: .leading, spacing: 0) {
  184. VStack(alignment: .leading, spacing: 0) {
  185. Text(user.username ?? "?")
  186. Text(user.baseUrl ?? "?")
  187. .font(.subheadline)
  188. .foregroundColor(.gray)
  189. }
  190. }
  191. Spacer()
  192. Image(systemName: "chevron.forward")
  193. .font(.system(size: 12.0))
  194. .foregroundColor(.gray)
  195. }
  196. .padding(.all, 4)
  197. }
  198. }
  199. struct SettingsView_Previews: PreviewProvider {
  200. static var previews: some View {
  201. let store = Store.preview // Store.previewEmpty
  202. SettingsView()
  203. .environment(\.managedObjectContext, store.context)
  204. .environmentObject(store)
  205. .environmentObject(AppDelegate())
  206. }
  207. }