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