123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- import Foundation
- import SwiftUI
- import StoreKit
- struct SettingsView: View {
- @EnvironmentObject private var store: Store
-
- var body: some View {
- NavigationView {
- Form {
- Section(
- header: Text("General"),
- footer: Text("When subscribing to new topics, this server will be used as a default.")
- ) {
- DefaultServerView()
- }
- Section(
- header: Text("Users"),
- 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.")
- ) {
- UserTableView()
- }
- Section(header: Text("About")) {
- AboutView()
- }
- }
- .navigationTitle("Settings")
- }
- .navigationViewStyle(StackNavigationViewStyle())
- }
- }
- struct DefaultServerView: View {
- @EnvironmentObject private var store: Store
- @FetchRequest(sortDescriptors: []) var prefs: FetchedResults<Preference>
- @State private var showDialog = false
- @State private var newDefaultBaseUrl: String = ""
-
- private var defaultBaseUrl: String {
- prefs
- .filter { $0.key == Store.prefKeyDefaultBaseUrl }
- .first?
- .value ?? Config.appBaseUrl
- }
-
- var body: some View {
- Button(action: {
- if defaultBaseUrl == Config.appBaseUrl {
- newDefaultBaseUrl = ""
- } else {
- newDefaultBaseUrl = defaultBaseUrl
- }
- showDialog = true
- }) {
- HStack {
- let _ = newDefaultBaseUrl
- Text("Default server")
- .foregroundColor(.primary)
- Spacer()
- Text(shortUrl(url: defaultBaseUrl))
- .foregroundColor(.gray)
- }
- .contentShape(Rectangle())
- }
- .sheet(isPresented: $showDialog) {
- NavigationView {
- Form {
- Section(
- footer: Text("When subscribing to new topics, this server will be used as a default. Note that if you pick your own ntfy server, you must configure upstream-base-url to receive instant push notifications.")
- ) {
- HStack {
- TextField(Config.appBaseUrl, text: $newDefaultBaseUrl)
- .disableAutocapitalization()
- .disableAutocorrection(true)
- if !newDefaultBaseUrl.isEmpty {
- Button {
- newDefaultBaseUrl = ""
- } label: {
- Image(systemName: "clear.fill")
- }
- }
- }
- }
- }
- .navigationTitle("Default server")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .navigationBarLeading) {
- Button(action: cancelAction) {
- Text("Cancel")
- }
- }
- ToolbarItem(placement: .navigationBarTrailing) {
- Button(action: saveAction) {
- Text("Save")
- }
- .disabled(!isValid())
- }
- }
- }
- }
- }
-
- private func saveAction() {
- if newDefaultBaseUrl == "" {
- store.saveDefaultBaseUrl(baseUrl: nil)
- } else {
- store.saveDefaultBaseUrl(baseUrl: newDefaultBaseUrl)
- }
- resetAndHide()
- }
-
- private func cancelAction() {
- resetAndHide()
- }
-
- private func isValid() -> Bool {
- if !newDefaultBaseUrl.isEmpty && newDefaultBaseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil {
- return false
- }
- return true
- }
-
- private func resetAndHide() {
- showDialog = false
- }
- }
- struct UserTableView: View {
- @EnvironmentObject private var store: Store
- @FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \User.baseUrl, ascending: true)]) var users: FetchedResults<User>
-
- @State private var selectedUser: User?
- @State private var showDialog = false
-
- @State private var baseUrl: String = ""
- @State private var username: String = ""
- @State private var password: String = ""
-
- var body: some View {
- let _ = selectedUser?.username // Workaround for FB7823148, see https://developer.apple.com/forums/thread/652080
- List {
- ForEach(users) { user in
- Button(action: {
- selectedUser = user
- baseUrl = user.baseUrl ?? "?"
- username = user.username ?? "?"
- showDialog = true
- }) {
- UserRowView(user: user)
- .foregroundColor(.primary)
- }
- }
- Button(action: {
- showDialog = true
- }) {
- HStack {
- Image(systemName: "plus")
- Text("Add user")
- }
- .foregroundColor(.primary)
- }
- .padding(.all, 4)
- }
- .sheet(isPresented: $showDialog) {
- NavigationView {
- Form {
- Section(
- footer: (selectedUser == nil)
- ? Text("You can add a user here. All topics for the given server will use this user.")
- : 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.")
- ) {
- if selectedUser == nil {
- TextField("Service URL, e.g. https://ntfy.home.io", text: $baseUrl)
- .disableAutocapitalization()
- .disableAutocorrection(true)
- }
- TextField("Username", text: $username)
- .disableAutocapitalization()
- .disableAutocorrection(true)
- SecureField("Password", text: $password)
- }
- }
- .navigationTitle(selectedUser == nil ? "Add user" : "Edit user")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .navigationBarLeading) {
- if selectedUser == nil {
- Button("Cancel") {
- cancelAction()
- }
- } else {
- Menu {
- Button("Cancel") {
- cancelAction()
- }
- if #available(iOS 15.0, *) {
- Button(role: .destructive) {
- deleteAction()
- } label: {
- Text("Delete")
- }
- } else {
- Button("Delete") {
- deleteAction()
- }
- }
- } label: {
- Image(systemName: "ellipsis.circle")
- .padding([.leading], 40)
- }
- }
- }
- ToolbarItem(placement: .navigationBarTrailing) {
- Button(action: saveAction) {
- Text("Save")
- }
- .disabled(!isValid())
- }
- }
- }
- }
- }
-
- private func saveAction() {
- var password = password
- if let user = selectedUser, password == "" {
- password = user.password ?? "?" // If password is blank, leave unchanged
- }
- store.saveUser(baseUrl: baseUrl, username: username, password: password)
- resetAndHide()
- }
-
- private func cancelAction() {
- resetAndHide()
- }
-
- private func deleteAction() {
- store.delete(user: selectedUser!)
- resetAndHide()
- }
-
- private func isValid() -> Bool {
- if selectedUser == nil { // New user
- if baseUrl.range(of: "^https?://.+", options: .regularExpression, range: nil, locale: nil) == nil {
- return false
- } else if username.isEmpty || password.isEmpty {
- return false
- } else if store.getUser(baseUrl: baseUrl) != nil {
- return false
- }
- } else { // Existing user
- if username.isEmpty {
- return false
- }
- }
- return true
- }
-
- private func resetAndHide() {
- showDialog = false
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
- // Hide first and then reset, otherwise we'll see the text fields change
- selectedUser = nil
- baseUrl = ""
- username = ""
- password = ""
- }
- }
- }
- struct UserRowView: View {
- @EnvironmentObject private var store: Store
- @ObservedObject var user: User
-
- var body: some View {
- // I tried to add a swipe action here to delete, but for some strange reason it doesn't work,
- // even though in the subscription list it does.
-
- HStack {
- Image(systemName: "person.fill")
- VStack(alignment: .leading, spacing: 0) {
- VStack(alignment: .leading, spacing: 0) {
- Text(user.username ?? "?")
- Text(user.baseUrl ?? "?")
- .font(.subheadline)
- .foregroundColor(.gray)
- }
- }
- Spacer()
- Image(systemName: "chevron.forward")
- .font(.system(size: 12.0))
- .foregroundColor(.gray)
- }
- .padding(.all, 4)
- }
- }
- struct AboutView: View {
- var body: some View {
- Group {
- Button(action: {
- open(url: "https://ntfy.sh/docs")
- }) {
- HStack {
- Text("Read the docs")
- Spacer()
- Text("ntfy.sh/docs")
- .foregroundColor(.gray)
- Image(systemName: "link")
- }
- }
- Button(action: {
- open(url: "https://github.com/binwiederhier/ntfy/issues")
- }) {
- HStack {
- Text("Report a bug")
- Spacer()
- Text("github.com")
- .foregroundColor(.gray)
- Image(systemName: "link")
- }
- }
- Button(action: {
- open(url: "itms-apps://itunes.apple.com/app/id1625396347")
- }) {
- HStack {
- Text("Rate the app")
- Spacer()
- Text("App Store")
- .foregroundColor(.gray)
- Image(systemName: "star.fill")
- }
- }
- HStack {
- Text("Version")
- Spacer()
- Text("ntfy \(Config.version) (\(Config.build))")
- .foregroundColor(.gray)
- }
- }
- .foregroundColor(.primary)
- }
-
- private func open(url: String) {
- guard let url = URL(string: url) else { return }
- UIApplication.shared.open(url, options: [:], completionHandler: nil)
- }
- }
- struct SettingsView_Previews: PreviewProvider {
- static var previews: some View {
- let store = Store.preview // Store.previewEmpty
- SettingsView()
- .environment(\.managedObjectContext, store.context)
- .environmentObject(store)
- .environmentObject(AppDelegate())
- }
- }
|