From 78a0291e5d3f2c6091cfda216509d11523d31b6d Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 28 Sep 2021 22:33:12 +0200 Subject: [PATCH] Extract instance/account validation status view --- Model/AccountValidator.swift | 40 +++++++++------- Model/InvidiousAPI.swift | 9 ---- Pearvidious.xcodeproj/project.pbxproj | 8 ++++ Shared/Playlists/AddToPlaylistView.swift | 2 +- Shared/Settings/AccountFormView.swift | 31 +++++------- .../InstanceDetailsSettingsView.swift | 9 ++-- Shared/Settings/InstanceFormView.swift | 42 ++++++---------- Shared/Settings/ValidationStatusView.swift | 48 +++++++++++++++++++ 8 files changed, 112 insertions(+), 77 deletions(-) create mode 100644 Shared/Settings/ValidationStatusView.swift diff --git a/Model/AccountValidator.swift b/Model/AccountValidator.swift index 98aa1f3a..00deda06 100644 --- a/Model/AccountValidator.swift +++ b/Model/AccountValidator.swift @@ -7,23 +7,26 @@ final class AccountValidator: Service { let account: Instance.Account? var formObjectID: Binding - var valid: Binding - var validated: Binding + var isValid: Binding + var isValidated: Binding + var isValidating: Binding var error: Binding? init( url: String, account: Instance.Account? = nil, id: Binding, - valid: Binding, - validated: Binding, + isValid: Binding, + isValidated: Binding, + isValidating: Binding, error: Binding? = nil ) { self.url = url self.account = account formObjectID = id - self.valid = valid - self.validated = validated + self.isValid = isValid + self.isValidated = isValidated + self.isValidating = isValidating self.error = error super.init(baseURL: url) @@ -50,18 +53,20 @@ final class AccountValidator: Service { return } - self.valid.wrappedValue = true + self.isValid.wrappedValue = true self.error?.wrappedValue = nil - self.validated.wrappedValue = true } .onFailure { error in guard self.url == self.formObjectID.wrappedValue else { return } - self.valid.wrappedValue = false + self.isValid.wrappedValue = false self.error?.wrappedValue = error.userMessage - self.validated.wrappedValue = true + } + .onCompletion { _ in + self.isValidated.wrappedValue = true + self.isValidating.wrappedValue = false } } @@ -75,22 +80,25 @@ final class AccountValidator: Service { return } - self.valid.wrappedValue = true - self.validated.wrappedValue = true + self.isValid.wrappedValue = true } .onFailure { _ in guard self.account!.sid == self.formObjectID.wrappedValue else { return } - self.valid.wrappedValue = false - self.validated.wrappedValue = true + self.isValid.wrappedValue = false + } + .onCompletion { _ in + self.isValidated.wrappedValue = true + self.isValidating.wrappedValue = false } } func reset() { - valid.wrappedValue = false - validated.wrappedValue = false + isValid.wrappedValue = false + isValidated.wrappedValue = false + isValidating.wrappedValue = false error?.wrappedValue = nil } diff --git a/Model/InvidiousAPI.swift b/Model/InvidiousAPI.swift index 756e921e..2d461ff1 100644 --- a/Model/InvidiousAPI.swift +++ b/Model/InvidiousAPI.swift @@ -11,15 +11,6 @@ final class InvidiousAPI: Service, ObservableObject { @Published var validInstance = true @Published var signedIn = true - init() { - super.init() - - #if os(tvOS) - // TODO: remove - setAccount(.init(id: UUID(), name: "", url: "https://invidious.home.arekf.net", sid: "RpoS7YPPK2-QS81jJF9z4KSQAjmzsOnMpn84c73-GQ8=")) - #endif - } - func setAccount(_ account: Instance.Account) { self.account = account diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index fce7ccd4..642585cf 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -101,6 +101,9 @@ 376CD21626FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */; }; + 37732FF02703A26300F04329 /* ValidationStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* ValidationStatusView.swift */; }; + 37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* ValidationStatusView.swift */; }; + 37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* ValidationStatusView.swift */; }; 377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; }; 377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; }; 377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; }; @@ -327,6 +330,7 @@ 37666BA927023AF000F869E5 /* AccountSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSelectionView.swift; sourceTree = ""; }; 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = ""; }; 376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = ""; }; + 37732FEF2703A26300F04329 /* ValidationStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationStatusView.swift; sourceTree = ""; }; 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = ""; }; 3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = ""; }; 3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = ""; }; @@ -558,6 +562,7 @@ 37484C1C26FC83A400287258 /* InstancesSettingsView.swift */, 37484C1826FC837400287258 /* PlaybackSettingsView.swift */, 37B044B626F7AB9000E1419D /* SettingsView.swift */, + 37732FEF2703A26300F04329 /* ValidationStatusView.swift */, ); path = Settings; sourceTree = ""; @@ -1119,6 +1124,7 @@ 37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, 37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */, 3797758B2689345500DD52A8 /* Store.swift in Sources */, + 37732FF02703A26300F04329 /* ValidationStatusView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1185,6 +1191,7 @@ 3797758C2689345500DD52A8 /* Store.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */, 37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */, + 37732FF12703A26300F04329 /* ValidationStatusView.swift in Sources */, 37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, @@ -1250,6 +1257,7 @@ 37AAF29226740715007FC770 /* Channel.swift in Sources */, 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */, + 37732FF22703A26300F04329 /* ValidationStatusView.swift in Sources */, 37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, 3765788B2685471400D4EA09 /* Playlist.swift in Sources */, 373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */, diff --git a/Shared/Playlists/AddToPlaylistView.swift b/Shared/Playlists/AddToPlaylistView.swift index 14888872..69498611 100644 --- a/Shared/Playlists/AddToPlaylistView.swift +++ b/Shared/Playlists/AddToPlaylistView.swift @@ -5,7 +5,7 @@ import SwiftUI struct AddToPlaylistView: View { @EnvironmentObject private var model - @State var video: Video + let video: Video @Environment(\.dismiss) private var dismiss diff --git a/Shared/Settings/AccountFormView.swift b/Shared/Settings/AccountFormView.swift index 9f4fb030..74c8b6bd 100644 --- a/Shared/Settings/AccountFormView.swift +++ b/Shared/Settings/AccountFormView.swift @@ -8,8 +8,9 @@ struct AccountFormView: View { @State private var name = "" @State private var sid = "" - @State private var valid = false - @State private var validated = false + @State private var isValid = false + @State private var isValidated = false + @State private var isValidating = false @State private var validationDebounce = Debounce() @FocusState private var focused: Bool @@ -82,12 +83,12 @@ struct AccountFormView: View { var footer: some View { HStack { - validationStatus + ValidationStatusView(isValid: $isValid, isValidated: $isValidated, isValidating: $isValidating, error: .constant(nil)) Spacer() Button("Save", action: submitForm) - .disabled(!valid) + .disabled(!isValid) #if !os(tvOS) .keyboardShortcut(.defaultAction) #endif @@ -99,17 +100,6 @@ struct AccountFormView: View { .padding(.horizontal) } - var validationStatus: some View { - HStack(spacing: 4) { - Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill") - .foregroundColor(valid ? .green : .red) - VStack(alignment: .leading) { - Text(valid ? "Account found" : "Invalid account details") - } - } - .opacity(validated ? 1 : 0) - } - private func initializeForm() { focused = true } @@ -122,13 +112,15 @@ struct AccountFormView: View { return } - validationDebounce.debouncing(2) { + isValidating = true + + validationDebounce.debouncing(1) { validator.validateAccount() } } private func submitForm() { - guard valid else { + guard isValid else { return } @@ -143,8 +135,9 @@ struct AccountFormView: View { url: instance.url, account: Instance.Account(instanceID: instance.id, url: instance.url, sid: sid), id: $sid, - valid: $valid, - validated: $validated + isValid: $isValid, + isValidated: $isValidated, + isValidating: $isValidating ) } } diff --git a/Shared/Settings/InstanceDetailsSettingsView.swift b/Shared/Settings/InstanceDetailsSettingsView.swift index 0bc14bcc..4db66716 100644 --- a/Shared/Settings/InstanceDetailsSettingsView.swift +++ b/Shared/Settings/InstanceDetailsSettingsView.swift @@ -16,7 +16,6 @@ struct InstanceDetailsSettingsView: View { List { Section(header: Text("Accounts")) { ForEach(instances.accounts(instanceID), id: \.self) { account in - #if !os(tvOS) HStack(spacing: 2) { Text(account.description) @@ -27,13 +26,13 @@ struct InstanceDetailsSettingsView: View { } .swipeActions(edge: .leading, allowsFullSwipe: true) { if instances.defaultAccount != account { - Button("Make Default", action: { makeDefault(account) }) + Button("Make Default") { makeDefault(account) } } else { Button("Reset Default", action: resetDefaultAccount) } } .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button("Remove", role: .destructive, action: { removeAccount(account) }) + Button("Remove", role: .destructive) { removeAccount(account) } } #else @@ -47,8 +46,8 @@ struct InstanceDetailsSettingsView: View { } } .contextMenu { - Button("Toggle Default", action: { toggleDefault(account) }) - Button("Remove", role: .destructive, action: { removeAccount(account) }) + Button("Toggle Default") { toggleDefault(account) } + Button("Remove", role: .destructive) { removeAccount(account) } } #endif } diff --git a/Shared/Settings/InstanceFormView.swift b/Shared/Settings/InstanceFormView.swift index 62c363e4..9bac8515 100644 --- a/Shared/Settings/InstanceFormView.swift +++ b/Shared/Settings/InstanceFormView.swift @@ -6,8 +6,9 @@ struct InstanceFormView: View { @State private var name = "" @State private var url = "" - @State private var valid = false - @State private var validated = false + @State private var isValid = false + @State private var isValidated = false + @State private var isValidating = false @State private var validationError: String? @State private var validationDebounce = Debounce() @@ -73,21 +74,24 @@ struct InstanceFormView: View { private var formFields: some View { Group { TextField("Name", text: $name, prompt: Text("Instance Name (optional)")) - .focused($nameFieldFocused) TextField("URL", text: $url, prompt: Text("https://invidious.home.net")) + #if !os(macOS) + .autocapitalization(.none) + .keyboardType(.URL) + #endif } } private var footer: some View { HStack(alignment: .center) { - validationStatus + ValidationStatusView(isValid: $isValid, isValidated: $isValidated, isValidating: $isValidating, error: $validationError) Spacer() Button("Save", action: submitForm) - .disabled(!valid) + .disabled(!isValid) #if !os(tvOS) .keyboardShortcut(.defaultAction) #endif @@ -98,31 +102,13 @@ struct InstanceFormView: View { .padding(.horizontal) } - private var validationStatus: some View { - HStack(spacing: 4) { - Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill") - .foregroundColor(valid ? .green : .red) - VStack(alignment: .leading) { - Text(valid ? "Connected successfully" : "Connection failed") - if !valid { - Text(validationError ?? "Unknown Error") - .font(.caption2) - .foregroundColor(.secondary) - .truncationMode(.tail) - .lineLimit(1) - } - } - .frame(minHeight: 35) - } - .opacity(validated ? 1 : 0) - } - var validator: AccountValidator { AccountValidator( url: url, id: $url, - valid: $valid, - validated: $validated, + isValid: $isValid, + isValidated: $isValidated, + isValidating: $isValidating, error: $validationError ) } @@ -135,6 +121,8 @@ struct InstanceFormView: View { return } + isValidating = true + validationDebounce.debouncing(2) { validator.validateInstance() } @@ -145,7 +133,7 @@ struct InstanceFormView: View { } func submitForm() { - guard valid else { + guard isValid else { return } diff --git a/Shared/Settings/ValidationStatusView.swift b/Shared/Settings/ValidationStatusView.swift new file mode 100644 index 00000000..22d68023 --- /dev/null +++ b/Shared/Settings/ValidationStatusView.swift @@ -0,0 +1,48 @@ +import Foundation +import SwiftUI + +struct ValidationStatusView: View { + @Binding var isValid: Bool + @Binding var isValidated: Bool + @Binding var isValidating: Bool + @Binding var error: String? + + var body: some View { + HStack(spacing: 4) { + Image(systemName: validationStatusSystemImage) + .foregroundColor(validationStatusColor) + + .frame(minWidth: 35, minHeight: 35) + .opacity(isValidating ? 1 : (isValidated ? 1 : 0)) + + VStack(alignment: .leading) { + Text(isValid ? "Connected successfully" : "Connection failed") + if !isValid && !error.isNil { + Text(error!) + .font(.caption2) + .foregroundColor(.secondary) + .truncationMode(.tail) + .lineLimit(1) + } + } + .frame(minHeight: 35) + .opacity(isValidating ? 0 : (isValidated ? 1 : 0)) + } + } + + var validationStatusSystemImage: String { + if isValidating { + return "bolt.horizontal.fill" + } else { + return isValid ? "checkmark.circle.fill" : "xmark.circle.fill" + } + } + + var validationStatusColor: Color { + if isValidating { + return .accentColor + } else { + return isValid ? .green : .red + } + } +}