mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Debouncing and form validation improvements
This commit is contained in:
parent
f9396985c9
commit
a0f74a5899
11
Extensions/String+Format.swift
Normal file
11
Extensions/String+Format.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
var serializationSafe: String {
|
||||
let serializationUnsafe = ":;"
|
||||
let forbidden = CharacterSet(charactersIn: serializationUnsafe)
|
||||
let result = unicodeScalars.filter { !forbidden.contains($0) }
|
||||
|
||||
return String(String.UnicodeScalarView(result))
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import Foundation
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
final class InstanceAccountValidator: Service {
|
||||
final class AccountValidator: Service {
|
||||
let url: String
|
||||
let account: Instance.Account?
|
||||
|
||||
@ -14,14 +14,14 @@ final class InstanceAccountValidator: Service {
|
||||
init(
|
||||
url: String,
|
||||
account: Instance.Account? = nil,
|
||||
formObjectID: Binding<String>,
|
||||
id: Binding<String>,
|
||||
valid: Binding<Bool>,
|
||||
validated: Binding<Bool>,
|
||||
error: Binding<String?>? = nil
|
||||
) {
|
||||
self.url = url
|
||||
self.account = account
|
||||
self.formObjectID = formObjectID
|
||||
formObjectID = id
|
||||
self.valid = valid
|
||||
self.validated = validated
|
||||
self.error = error
|
@ -74,9 +74,15 @@
|
||||
37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||
37484C2E26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||
37484C2F26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */; };
|
||||
37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||
37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||
37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */; };
|
||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||
375168DB27010806008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; };
|
||||
375168DC27010807008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; };
|
||||
375168DD27010808008F96A6 /* String+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D92701070E008F96A6 /* String+Format.swift */; };
|
||||
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
375DFB5A26F9DA010013F468 /* InstancesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375DFB5726F9DA010013F468 /* InstancesModel.swift */; };
|
||||
@ -324,7 +330,9 @@
|
||||
37484C2426FC83E000287258 /* InstanceFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceFormView.swift; sourceTree = "<group>"; };
|
||||
37484C2826FC83FF00287258 /* AccountFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFormView.swift; sourceTree = "<group>"; };
|
||||
37484C2C26FC844700287258 /* InstanceDetailsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceDetailsSettingsView.swift; sourceTree = "<group>"; };
|
||||
37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceAccountValidator.swift; sourceTree = "<group>"; };
|
||||
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
||||
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
||||
375168D92701070E008F96A6 /* String+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Format.swift"; sourceTree = "<group>"; };
|
||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = "<group>"; };
|
||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
|
||||
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
|
||||
@ -640,6 +648,7 @@
|
||||
379775922689365600DD52A8 /* Array+Next.swift */,
|
||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||
375168D92701070E008F96A6 /* String+Format.swift */,
|
||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
@ -676,6 +685,7 @@
|
||||
371AAE2726CEBF4700901972 /* Videos */,
|
||||
371AAE2826CEC7D900901972 /* Views */,
|
||||
3788AC2126F683AB00F6BAA9 /* Watch Now */,
|
||||
375168D52700FAFF008F96A6 /* Debounce.swift */,
|
||||
372915E52687E3B900F5A35B /* Defaults.swift */,
|
||||
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
|
||||
37D4B0C22671614700C925CA /* PearvidiousApp.swift */,
|
||||
@ -741,7 +751,7 @@
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37141672267A8E10006CA35D /* Country.swift */,
|
||||
378E50FA26FE8B9F00F49626 /* Instance.swift */,
|
||||
37484C3026FCB8F900287258 /* InstanceAccountValidator.swift */,
|
||||
37484C3026FCB8F900287258 /* AccountValidator.swift */,
|
||||
375DFB5726F9DA010013F468 /* InstancesModel.swift */,
|
||||
37977582268922F600DD52A8 /* InvidiousAPI.swift */,
|
||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */,
|
||||
@ -1069,6 +1079,7 @@
|
||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||
375168DD27010808008F96A6 /* String+Format.swift in Sources */,
|
||||
37484C1926FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||
37F64FE426FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */,
|
||||
@ -1084,6 +1095,7 @@
|
||||
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
|
||||
3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */,
|
||||
37E64DD126D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
|
||||
376578892685471400D4EA09 /* Playlist.swift in Sources */,
|
||||
37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */,
|
||||
@ -1103,7 +1115,7 @@
|
||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
37484C2D26FC844700287258 /* InstanceDetailsSettingsView.swift in Sources */,
|
||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
|
||||
37484C3126FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
@ -1161,7 +1173,7 @@
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
37484C1A26FC837400287258 /* PlaybackSettingsView.swift in Sources */,
|
||||
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */,
|
||||
37484C3226FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
|
||||
37EAD870267B9ED100D9E01B /* Segment.swift in Sources */,
|
||||
3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||
@ -1202,9 +1214,11 @@
|
||||
37754C9E26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||
375168DC27010807008F96A6 /* String+Format.swift in Sources */,
|
||||
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
|
||||
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
|
||||
37D4B19826717E1500C925CA /* Video.swift in Sources */,
|
||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */,
|
||||
37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||
37BD07C12698AD3B003EBB87 /* TrendingCountry.swift in Sources */,
|
||||
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
@ -1263,6 +1277,7 @@
|
||||
376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||
@ -1280,6 +1295,7 @@
|
||||
37AAF27E26737323007FC770 /* PopularView.swift in Sources */,
|
||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||
37484C1F26FC83A400287258 /* InstancesSettingsView.swift in Sources */,
|
||||
375168DB27010806008F96A6 /* String+Format.swift in Sources */,
|
||||
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,
|
||||
37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
|
||||
376578932685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||
@ -1291,7 +1307,7 @@
|
||||
37BA794526DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||
37484C3326FCB8F900287258 /* InstanceAccountValidator.swift in Sources */,
|
||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||
37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */,
|
||||
37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||
|
15
Shared/Debounce.swift
Normal file
15
Shared/Debounce.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
struct Debounce {
|
||||
private var timer: Timer?
|
||||
|
||||
mutating func debouncing(_ interval: TimeInterval, action: @escaping () -> Void) {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
timer?.invalidate()
|
||||
}
|
||||
}
|
@ -8,12 +8,12 @@ struct AccountsMenuView: View {
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
ForEach(instances, id: \.self) { instance in
|
||||
ForEach(instances) { instance in
|
||||
Button(accountButtonTitle(instance: instance, account: instance.anonymousAccount)) {
|
||||
api.setAccount(instance.anonymousAccount)
|
||||
}
|
||||
|
||||
ForEach(instance.accounts, id: \.self) { account in
|
||||
ForEach(instance.accounts) { account in
|
||||
Button(accountButtonTitle(instance: instance, account: account)) {
|
||||
api.setAccount(account)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ struct AccountFormView: View {
|
||||
|
||||
@State private var valid = false
|
||||
@State private var validated = false
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
@ -19,55 +20,10 @@ struct AccountFormView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack(alignment: .center) {
|
||||
Text("Add Account")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Form {
|
||||
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||
.focused($focused)
|
||||
|
||||
TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie"))
|
||||
}
|
||||
.onAppear(perform: initializeForm)
|
||||
.onChange(of: sid) { _ in validate() }
|
||||
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
|
||||
HStack {
|
||||
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)
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
.padding(.horizontal)
|
||||
header
|
||||
form
|
||||
footer
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
.padding(.vertical)
|
||||
#else
|
||||
@ -75,35 +31,97 @@ struct AccountFormView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
func initializeForm() {
|
||||
var header: some View {
|
||||
HStack(alignment: .center) {
|
||||
Text("Add Account")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var form: some View {
|
||||
Form {
|
||||
TextField("Name", text: $name, prompt: Text("Account Name (optional)"))
|
||||
.focused($focused)
|
||||
|
||||
TextField("SID", text: $sid, prompt: Text("Invidious SID Cookie"))
|
||||
}
|
||||
.onAppear(perform: initializeForm)
|
||||
.onChange(of: sid) { _ in validate() }
|
||||
#if os(macOS)
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
}
|
||||
|
||||
var footer: some View {
|
||||
HStack {
|
||||
validationStatus
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Save", action: submitForm)
|
||||
.disabled(!valid)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
.frame(minHeight: 35)
|
||||
.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
|
||||
}
|
||||
|
||||
func validate() {
|
||||
private func validate() {
|
||||
validationDebounce.invalidate()
|
||||
|
||||
guard !sid.isEmpty else {
|
||||
validator.reset()
|
||||
return
|
||||
}
|
||||
|
||||
validator.validateAccount()
|
||||
validationDebounce.debouncing(2) {
|
||||
validator.validateAccount()
|
||||
}
|
||||
}
|
||||
|
||||
func submitForm() {
|
||||
private func submitForm() {
|
||||
guard valid else {
|
||||
return
|
||||
}
|
||||
|
||||
let account = instances.addAccount(instance: instance, name: name, sid: sid)
|
||||
let account = instances.addAccount(instance: instance, name: name.serializationSafe, sid: sid)
|
||||
selectedAccount?.wrappedValue = account
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private var validator: InstanceAccountValidator {
|
||||
InstanceAccountValidator(
|
||||
private var validator: AccountValidator {
|
||||
AccountValidator(
|
||||
url: instance.url,
|
||||
account: Instance.Account(url: instance.url, sid: sid),
|
||||
formObjectID: $sid,
|
||||
id: $sid,
|
||||
valid: $valid,
|
||||
validated: $validated
|
||||
)
|
||||
|
@ -15,7 +15,7 @@ struct InstanceDetailsSettingsView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("Accounts")) {
|
||||
ForEach(instance.accounts, id: \.self) { account in
|
||||
ForEach(instance.accounts) { account in
|
||||
Text(account.description)
|
||||
#if !os(tvOS)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
|
@ -9,6 +9,7 @@ struct InstanceFormView: View {
|
||||
@State private var valid = false
|
||||
@State private var validated = false
|
||||
@State private var validationError: String?
|
||||
@State private var validationDebounce = Debounce()
|
||||
|
||||
@FocusState private var nameFieldFocused: Bool
|
||||
|
||||
@ -78,14 +79,13 @@ struct InstanceFormView: View {
|
||||
.padding(.vertical)
|
||||
#else
|
||||
.frame(width: 400, height: 150)
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
var validator: InstanceAccountValidator {
|
||||
InstanceAccountValidator(
|
||||
var validator: AccountValidator {
|
||||
AccountValidator(
|
||||
url: url,
|
||||
formObjectID: $url,
|
||||
id: $url,
|
||||
valid: $valid,
|
||||
validated: $validated,
|
||||
error: $validationError
|
||||
@ -93,15 +93,16 @@ struct InstanceFormView: View {
|
||||
}
|
||||
|
||||
func validate() {
|
||||
valid = false
|
||||
validated = false
|
||||
validationError = nil
|
||||
validationDebounce.invalidate()
|
||||
|
||||
guard !url.isEmpty else {
|
||||
validator.reset()
|
||||
return
|
||||
}
|
||||
|
||||
validator.validateInstance()
|
||||
validationDebounce.debouncing(2) {
|
||||
validator.validateInstance()
|
||||
}
|
||||
}
|
||||
|
||||
func initializeForm() {
|
||||
|
@ -27,7 +27,7 @@ struct InstancesSettingsView: View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Section(header: instancesHeader) {
|
||||
ForEach(instances, id: \.self) { instance in
|
||||
ForEach(instances) { instance in
|
||||
Button(action: {
|
||||
self.selectedInstanceID = instance.id
|
||||
self.presentingInstanceDetails = true
|
||||
@ -62,7 +62,7 @@ struct InstancesSettingsView: View {
|
||||
|
||||
if !instances.isEmpty {
|
||||
Picker("Instance", selection: $selectedInstanceID) {
|
||||
ForEach(instances, id: \.url) { instance in
|
||||
ForEach(instances) { instance in
|
||||
Text(instance.description).tag(Optional(instance.id))
|
||||
}
|
||||
}
|
||||
@ -81,7 +81,7 @@ struct InstancesSettingsView: View {
|
||||
} else {
|
||||
Text("Accounts")
|
||||
List(selection: $selectedAccount) {
|
||||
ForEach(instance.accounts, id: \.self) { account in
|
||||
ForEach(instance.accounts) { account in
|
||||
AccountSettingsView(instance: instance, account: account,
|
||||
selectedAccount: $selectedAccount)
|
||||
}
|
||||
|
@ -12,21 +12,21 @@ struct SearchView: View {
|
||||
@State private var presentingClearConfirmation = false
|
||||
@State private var recentsChanged = false
|
||||
|
||||
@State private var searchDebounce = Debounce()
|
||||
@State private var recentsDebounce = Debounce()
|
||||
|
||||
@Environment(\.navigationStyle) private var navigationStyle
|
||||
|
||||
@EnvironmentObject<RecentsModel> private var recents
|
||||
@EnvironmentObject<SearchModel> private var state
|
||||
|
||||
@State private var searchDebounceTimer: Timer?
|
||||
@State private var recentSearchDebounceTimer: Timer?
|
||||
|
||||
init(_ query: SearchQuery? = nil) {
|
||||
self.query = query
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if navigationStyle == .tab && state.queryText.isEmpty {
|
||||
if showRecentQueries {
|
||||
recentQueries
|
||||
} else {
|
||||
#if os(tvOS)
|
||||
@ -40,15 +40,11 @@ struct SearchView: View {
|
||||
VideosView(videos: state.store.collection)
|
||||
#endif
|
||||
|
||||
if state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty {
|
||||
if noResults {
|
||||
Text("No results")
|
||||
|
||||
if searchFiltersActive {
|
||||
Button("Reset search filters") {
|
||||
self.searchSortOrder = .relevance
|
||||
self.searchDate = .any
|
||||
self.searchDuration = .any
|
||||
}
|
||||
Button("Reset search filters", action: resetFilters)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@ -101,14 +97,14 @@ struct SearchView: View {
|
||||
state.loadSuggestions(newQuery)
|
||||
|
||||
#if os(tvOS)
|
||||
searchDebounceTimer?.invalidate()
|
||||
recentSearchDebounceTimer?.invalidate()
|
||||
searchDebounce.invalidate()
|
||||
recentsDebounce.invalidate()
|
||||
|
||||
searchDebounceTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
searchDebounce.debouncing(2) {
|
||||
state.changeQuery { query in query.query = newQuery }
|
||||
}
|
||||
|
||||
recentSearchDebounceTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in
|
||||
recentsDebounce.debouncing(10) {
|
||||
recents.addQuery(newQuery)
|
||||
}
|
||||
#endif
|
||||
@ -147,10 +143,24 @@ struct SearchView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
var filtersActive: Bool {
|
||||
fileprivate var showRecentQueries: Bool {
|
||||
navigationStyle == .tab && state.queryText.isEmpty
|
||||
}
|
||||
|
||||
fileprivate var filtersActive: Bool {
|
||||
searchDuration != .any || searchDate != .any
|
||||
}
|
||||
|
||||
fileprivate func resetFilters() {
|
||||
searchSortOrder = .relevance
|
||||
searchDate = .any
|
||||
searchDuration = .any
|
||||
}
|
||||
|
||||
fileprivate var noResults: Bool {
|
||||
state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty
|
||||
}
|
||||
|
||||
var recentQueries: some View {
|
||||
VStack {
|
||||
List {
|
||||
@ -282,6 +292,7 @@ struct SearchView: View {
|
||||
searchSortOrderButton
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
Text("Duration")
|
||||
.foregroundColor(.secondary)
|
||||
|
Loading…
Reference in New Issue
Block a user