Extract instance/account validation status view

This commit is contained in:
Arkadiusz Fal 2021-09-28 22:33:12 +02:00
parent 7446c945b5
commit 78a0291e5d
8 changed files with 112 additions and 77 deletions

View File

@ -7,23 +7,26 @@ final class AccountValidator: Service {
let account: Instance.Account?
var formObjectID: Binding<String>
var valid: Binding<Bool>
var validated: Binding<Bool>
var isValid: Binding<Bool>
var isValidated: Binding<Bool>
var isValidating: Binding<Bool>
var error: Binding<String?>?
init(
url: String,
account: Instance.Account? = nil,
id: Binding<String>,
valid: Binding<Bool>,
validated: Binding<Bool>,
isValid: Binding<Bool>,
isValidated: Binding<Bool>,
isValidating: Binding<Bool>,
error: Binding<String?>? = 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
}

View File

@ -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

View File

@ -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 = "<group>"; };
376B2E0626F920D600B1D64D /* SignInRequiredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInRequiredView.swift; sourceTree = "<group>"; };
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
37732FEF2703A26300F04329 /* ValidationStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationStatusView.swift; sourceTree = "<group>"; };
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
3788AC2626F6840700F6BAA9 /* WatchNowSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSection.swift; sourceTree = "<group>"; };
3788AC2A26F6842D00F6BAA9 /* WatchNowSectionBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNowSectionBody.swift; sourceTree = "<group>"; };
@ -558,6 +562,7 @@
37484C1C26FC83A400287258 /* InstancesSettingsView.swift */,
37484C1826FC837400287258 /* PlaybackSettingsView.swift */,
37B044B626F7AB9000E1419D /* SettingsView.swift */,
37732FEF2703A26300F04329 /* ValidationStatusView.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -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 */,

View File

@ -5,7 +5,7 @@ import SwiftUI
struct AddToPlaylistView: View {
@EnvironmentObject<PlaylistsModel> private var model
@State var video: Video
let video: Video
@Environment(\.dismiss) private var dismiss

View File

@ -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
)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}
}
}