mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 04:31:54 +00:00 
			
		
		
		
	Unify forms, add to/remove from playlist on all platforms, UI improvements
This commit is contained in:
		| @@ -2,6 +2,6 @@ import Foundation | ||||
|  | ||||
| extension Playlist { | ||||
|     static var fixture: Playlist { | ||||
|         Playlist(id: "ABC", title: "The Playlist", visibility: .public, updated: 1) | ||||
|         Playlist(id: UUID().uuidString, title: "Relaxing music", visibility: .public, updated: 1) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,9 @@ final class NavigationModel: ObservableObject { | ||||
|  | ||||
|     @Published var returnToDetails = false | ||||
|  | ||||
|     @Published var presentingAddToPlaylist = false | ||||
|     @Published var videoToAddToPlaylist: Video! | ||||
|  | ||||
|     @Published var presentingPlaylistForm = false | ||||
|     @Published var editedPlaylist: Playlist! | ||||
|  | ||||
| @@ -42,6 +45,11 @@ final class NavigationModel: ObservableObject { | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     func presentAddToPlaylist(_ video: Video) { | ||||
|         videoToAddToPlaylist = video | ||||
|         presentingAddToPlaylist = true | ||||
|     } | ||||
|  | ||||
|     func presentEditPlaylistForm(_ playlist: Playlist?) { | ||||
|         editedPlaylist = playlist | ||||
|         presentingPlaylistForm = editedPlaylist != nil | ||||
|   | ||||
| @@ -4,11 +4,12 @@ import SwiftUI | ||||
|  | ||||
| final class PlaylistsModel: ObservableObject { | ||||
|     @Published var playlists = [Playlist]() | ||||
|     @Published var api = InvidiousAPI() | ||||
|  | ||||
|     @Published var api: InvidiousAPI! | ||||
|     @Published var selectedPlaylistID: Playlist.ID = "" | ||||
|  | ||||
|     var resource: Resource { | ||||
|         api.playlists | ||||
|     init(_ playlists: [Playlist] = [Playlist]()) { | ||||
|         self.playlists = playlists | ||||
|     } | ||||
|  | ||||
|     var all: [Playlist] { | ||||
| @@ -16,20 +17,67 @@ final class PlaylistsModel: ObservableObject { | ||||
|     } | ||||
|  | ||||
|     func find(id: Playlist.ID) -> Playlist? { | ||||
|         all.first { $0.id == id } | ||||
|         playlists.first { $0.id == id } | ||||
|     } | ||||
|  | ||||
|     func load(force: Bool = false) { | ||||
|     var isEmpty: Bool { | ||||
|         playlists.isEmpty | ||||
|     } | ||||
|  | ||||
|     func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) { | ||||
|         let request = force ? resource.load() : resource.loadIfNeeded() | ||||
|  | ||||
|         request? | ||||
|             .onSuccess { resource in | ||||
|                 if let playlists: [Playlist] = resource.typedContent() { | ||||
|                     self.playlists = playlists | ||||
|                     if self.selectedPlaylistID.isEmpty { | ||||
|                         self.selectPlaylist(self.all.first?.id) | ||||
|                     } | ||||
|                     onSuccess() | ||||
|                 } | ||||
|             } | ||||
|             .onFailure { _ in | ||||
|                 self.playlists = [] | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     func addVideoToCurrentPlaylist(videoID: Video.ID, onSuccess: @escaping () -> Void = {}) { | ||||
|         let resource = api.playlistVideos(currentPlaylist!.id) | ||||
|         let body = ["videoId": videoID] | ||||
|  | ||||
|         resource.request(.post, json: body).onSuccess { _ in | ||||
|             self.load(force: true) | ||||
|             onSuccess() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func removeVideoFromPlaylist(videoIndexID: String, playlistID: Playlist.ID, onSuccess: @escaping () -> Void = {}) { | ||||
|         let resource = api.playlistVideo(playlistID, videoIndexID) | ||||
|  | ||||
|         resource.request(.delete).onSuccess { _ in | ||||
|             self.load(force: true) | ||||
|             onSuccess() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     func selectPlaylist(_ id: String?) { | ||||
|         selectedPlaylistID = id ?? "" | ||||
|     } | ||||
|  | ||||
|     private var resource: Resource { | ||||
|         api.playlists | ||||
|     } | ||||
|  | ||||
|     private var selectedPlaylist: Playlist? { | ||||
|         guard !selectedPlaylistID.isEmpty else { | ||||
|             return nil | ||||
|         } | ||||
|  | ||||
|         return find(id: selectedPlaylistID) | ||||
|     } | ||||
|  | ||||
|     var currentPlaylist: Playlist? { | ||||
|         selectedPlaylist ?? all.first | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ final class RecentsModel: ObservableObject { | ||||
|         items.removeAll { $0.type == .query } | ||||
|     } | ||||
|  | ||||
|     func open(_ item: RecentItem) { | ||||
|     func add(_ item: RecentItem) { | ||||
|         if !items.contains(where: { $0.id == item.id }) { | ||||
|             items.append(item) | ||||
|         } | ||||
| @@ -30,7 +30,7 @@ final class RecentsModel: ObservableObject { | ||||
|  | ||||
|     func addQuery(_ query: String) { | ||||
|         if !query.isEmpty { | ||||
|             open(.init(from: query)) | ||||
|             add(.init(from: query)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import SwiftUI | ||||
|  | ||||
| final class SubscriptionsModel: ObservableObject { | ||||
|     @Published var channels = [Channel]() | ||||
|     @Published var api: InvidiousAPI! | ||||
|     @Published var api: InvidiousAPI! = InvidiousAPI() | ||||
|  | ||||
|     var resource: Resource { | ||||
|         api.subscriptions | ||||
|   | ||||
| @@ -30,12 +30,6 @@ | ||||
| 		372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; | ||||
| 		372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; | ||||
| 		372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; | ||||
| 		373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFABD26966115003CB2C6 /* CoverSectionView.swift */; }; | ||||
| 		373CFABF26966149003CB2C6 /* CoverSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFABD26966115003CB2C6 /* CoverSectionView.swift */; }; | ||||
| 		373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFABD26966115003CB2C6 /* CoverSectionView.swift */; }; | ||||
| 		373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */; }; | ||||
| 		373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */; }; | ||||
| 		373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */; }; | ||||
| 		373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; | ||||
| 		373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; | ||||
| 		373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFACA26966264003CB2C6 /* SearchQuery.swift */; }; | ||||
| @@ -100,6 +94,7 @@ | ||||
| 		376578912685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; }; | ||||
| 		376578922685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; }; | ||||
| 		376578932685490700D4EA09 /* PlaylistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578902685490700D4EA09 /* PlaylistsView.swift */; }; | ||||
| 		37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37666BA927023AF000F869E5 /* AccountSelectionView.swift */; }; | ||||
| 		376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; }; | ||||
| 		376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; }; | ||||
| 		376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B2E0626F920D600B1D64D /* SignInRequiredView.swift */; }; | ||||
| @@ -218,7 +213,6 @@ | ||||
| 		37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */; }; | ||||
| 		37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; | ||||
| 		37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; | ||||
| 		37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B76E95268747C900CE5671 /* OptionsView.swift */; }; | ||||
| 		37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; | ||||
| 		37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; | ||||
| 		37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; | ||||
| @@ -308,8 +302,6 @@ | ||||
| 		37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; }; | ||||
| 		371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; }; | ||||
| 		372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; }; | ||||
| 		373CFABD26966115003CB2C6 /* CoverSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionView.swift; sourceTree = "<group>"; }; | ||||
| 		373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoverSectionRowView.swift; sourceTree = "<group>"; }; | ||||
| 		373CFACA26966264003CB2C6 /* SearchQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchQuery.swift; sourceTree = "<group>"; }; | ||||
| 		373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; }; | ||||
| 		373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; }; | ||||
| @@ -332,6 +324,7 @@ | ||||
| 		376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; }; | ||||
| 		376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; }; | ||||
| 		376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; }; | ||||
| 		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>"; }; | ||||
| 		377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; }; | ||||
| @@ -352,7 +345,6 @@ | ||||
| 		37B044B626F7AB9000E1419D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; | ||||
| 		37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; }; | ||||
| 		37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; }; | ||||
| 		37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = "<group>"; }; | ||||
| 		37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; }; | ||||
| 		37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; }; | ||||
| 		37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; }; | ||||
| @@ -512,8 +504,7 @@ | ||||
| 		371AAE2626CEBF1600901972 /* Playlists */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */, | ||||
| 				373CFABD26966115003CB2C6 /* CoverSectionView.swift */, | ||||
| 				373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */, | ||||
| 				373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, | ||||
| 				376578902685490700D4EA09 /* PlaylistsView.swift */, | ||||
| 			); | ||||
| @@ -715,8 +706,7 @@ | ||||
| 		37D4B159267164AE00C925CA /* tvOS */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */, | ||||
| 				37B76E95268747C900CE5671 /* OptionsView.swift */, | ||||
| 				37666BA927023AF000F869E5 /* AccountSelectionView.swift */, | ||||
| 				37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, | ||||
| 				37D4B15E267164AF00C925CA /* Assets.xcassets */, | ||||
| 				37D4B1AE26729DEB00C925CA /* Info.plist */, | ||||
| @@ -1086,7 +1076,6 @@ | ||||
| 				37B81B0526D2CEDA00675966 /* PlaybackModel.swift in Sources */, | ||||
| 				373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, | ||||
| 				3788AC2B26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, | ||||
| 				373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */, | ||||
| 				3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, | ||||
| 				373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, | ||||
| 				37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, | ||||
| @@ -1115,7 +1104,6 @@ | ||||
| 				3788AC2726F6840700F6BAA9 /* WatchNowSection.swift in Sources */, | ||||
| 				375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */, | ||||
| 				373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, | ||||
| 				373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */, | ||||
| 				37141673267A8E10006CA35D /* Country.swift in Sources */, | ||||
| 				3748186E26A769D60084E870 /* DetailBadge.swift in Sources */, | ||||
| 				37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, | ||||
| @@ -1142,7 +1130,6 @@ | ||||
| 				37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, | ||||
| 				3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, | ||||
| 				37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, | ||||
| 				373CFABF26966149003CB2C6 /* CoverSectionView.swift in Sources */, | ||||
| 				37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, | ||||
| 				37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, | ||||
| 				37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, | ||||
| @@ -1173,7 +1160,6 @@ | ||||
| 				37B044B826F7AB9000E1419D /* SettingsView.swift in Sources */, | ||||
| 				3765788A2685471400D4EA09 /* Playlist.swift in Sources */, | ||||
| 				373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */, | ||||
| 				373CFAC32696616C003CB2C6 /* CoverSectionRowView.swift in Sources */, | ||||
| 				37AAF29126740715007FC770 /* Channel.swift in Sources */, | ||||
| 				37BD07BC2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */, | ||||
| 				3748186F26A769D60084E870 /* DetailBadge.swift in Sources */, | ||||
| @@ -1264,11 +1250,11 @@ | ||||
| 				37AAF29226740715007FC770 /* Channel.swift in Sources */, | ||||
| 				37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, | ||||
| 				37B81B0726D2D6CF00675966 /* PlaybackModel.swift in Sources */, | ||||
| 				37666BAA27023AF000F869E5 /* AccountSelectionView.swift in Sources */, | ||||
| 				3765788B2685471400D4EA09 /* Playlist.swift in Sources */, | ||||
| 				373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */, | ||||
| 				37E64DD326D597EB00C71877 /* SubscriptionsModel.swift in Sources */, | ||||
| 				37B044B926F7AB9000E1419D /* SettingsView.swift in Sources */, | ||||
| 				373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */, | ||||
| 				37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, | ||||
| 				373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, | ||||
| 				3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */, | ||||
| @@ -1292,7 +1278,6 @@ | ||||
| 				37484C2326FC83C400287258 /* AccountSettingsView.swift in Sources */, | ||||
| 				37C194C926F6A9C8005D3B96 /* RecentsModel.swift in Sources */, | ||||
| 				37F64FE626FE70A60081B69E /* RedrawOnViewModifier.swift in Sources */, | ||||
| 				37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */, | ||||
| 				37484C2B26FC83FF00287258 /* AccountFormView.swift in Sources */, | ||||
| 				37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, | ||||
| 				3711404126B206A6005B3555 /* SearchModel.swift in Sources */, | ||||
| @@ -1303,7 +1288,6 @@ | ||||
| 				37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, | ||||
| 				37484C2726FC83E000287258 /* InstanceFormView.swift in Sources */, | ||||
| 				37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, | ||||
| 				373CFAC42696616C003CB2C6 /* CoverSectionRowView.swift in Sources */, | ||||
| 				37D4B19926717E1500C925CA /* Video.swift in Sources */, | ||||
| 				378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */, | ||||
| 				3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, | ||||
|   | ||||
| @@ -5,13 +5,10 @@ extension Defaults.Keys { | ||||
|     static let accounts = Key<[Instance.Account]>("accounts", default: []) | ||||
|     static let defaultAccountID = Key<String?>("defaultAccountID") | ||||
|  | ||||
|     static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default) | ||||
|     static let trendingCountry = Key<Country>("trendingCountry", default: .us) | ||||
|  | ||||
|     static let selectedPlaylistID = Key<String?>("selectedPlaylistID") | ||||
|     static let showingAddToPlaylist = Key<Bool>("showingAddToPlaylist", default: false) | ||||
|     static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist") | ||||
|     static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest) | ||||
|  | ||||
|     static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) | ||||
|     static let quality = Key<Stream.ResolutionSetting>("quality", default: .hd720pFirstThenBest) | ||||
|  | ||||
|     static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default) | ||||
|     static let trendingCountry = Key<Country>("trendingCountry", default: .us) | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ struct ContentView: View { | ||||
|  | ||||
|     @EnvironmentObject<InvidiousAPI> private var api | ||||
|     @EnvironmentObject<InstancesModel> private var instances | ||||
|     @EnvironmentObject<PlaylistsModel> private var playlists | ||||
|  | ||||
|     #if os(iOS) | ||||
|         @Environment(\.horizontalSizeClass) private var horizontalSizeClass | ||||
| @@ -44,6 +45,9 @@ struct ContentView: View { | ||||
|                     #endif | ||||
|                 } | ||||
|             } | ||||
|             .sheet(isPresented: $navigation.presentingAddToPlaylist) { | ||||
|                 AddToPlaylistView(video: navigation.videoToAddToPlaylist) | ||||
|             } | ||||
|             .sheet(isPresented: $navigation.presentingPlaylistForm) { | ||||
|                 PlaylistFormView(playlist: $navigation.editedPlaylist) | ||||
|             } | ||||
|   | ||||
| @@ -40,7 +40,7 @@ struct PearvidiousApp: App { | ||||
|         search.api = api | ||||
|         subscriptions.api = api | ||||
|  | ||||
|         if let account = instances.defaultAccount { | ||||
|         if let account = instances.defaultAccount, api.account.isNil { | ||||
|             api.setAccount(account) | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										178
									
								
								Shared/Playlists/AddToPlaylistView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								Shared/Playlists/AddToPlaylistView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| import Defaults | ||||
| import Siesta | ||||
| import SwiftUI | ||||
|  | ||||
| struct AddToPlaylistView: View { | ||||
|     @EnvironmentObject<PlaylistsModel> private var model | ||||
|  | ||||
|     @State var video: Video | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|  | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             VStack { | ||||
|                 if model.isEmpty { | ||||
|                     emptyPlaylistsMessage | ||||
|                 } else { | ||||
|                     header | ||||
|                     Spacer() | ||||
|                     form | ||||
|                     Spacer() | ||||
|                     footer | ||||
|                 } | ||||
|             } | ||||
|             .frame(maxWidth: 1000, maxHeight: height) | ||||
|         } | ||||
|         .onAppear { | ||||
|             model.load { | ||||
|                 if let playlist = model.all.first { | ||||
|                     model.selectedPlaylistID = playlist.id | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         #if os(macOS) | ||||
|             .frame(width: 500, height: 270) | ||||
|             .padding(.vertical) | ||||
|         #elseif os(tvOS) | ||||
|             .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||||
|             .background(.thickMaterial) | ||||
|         #else | ||||
|             .padding(.vertical) | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     var height: Double { | ||||
|         #if os(tvOS) | ||||
|             600 | ||||
|         #else | ||||
|             .infinity | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     private var emptyPlaylistsMessage: some View { | ||||
|         VStack(spacing: 20) { | ||||
|             Text("You have no Playlists") | ||||
|                 .font(.title2.bold()) | ||||
|             Text("Open \"Playlists\" tab to create new one") | ||||
|                 .foregroundColor(.secondary) | ||||
|                 .multilineTextAlignment(.center) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var header: some View { | ||||
|         HStack(alignment: .center) { | ||||
|             Text("Add to Playlist") | ||||
|                 .font(.title2.bold()) | ||||
|  | ||||
|             Spacer() | ||||
|  | ||||
|             #if !os(tvOS) | ||||
|                 Button("Cancel") { | ||||
|                     dismiss() | ||||
|                 } | ||||
|                 .keyboardShortcut(.cancelAction) | ||||
|             #endif | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     private var form: some View { | ||||
|         VStack(alignment: formAlignment) { | ||||
|             VStack(alignment: .leading, spacing: 10) { | ||||
|                 Text(video.title) | ||||
|                     .font(.headline) | ||||
|                 Text(video.author) | ||||
|                     .foregroundColor(.secondary) | ||||
|             } | ||||
|             .frame(maxWidth: .infinity, alignment: .leading) | ||||
|             .padding(.vertical, 40) | ||||
|  | ||||
|             VStack(alignment: formAlignment) { | ||||
|                 #if os(tvOS) | ||||
|  | ||||
|                     selectPlaylistButton | ||||
|                 #else | ||||
|                     Picker("Playlist", selection: $model.selectedPlaylistID) { | ||||
|                         ForEach(model.all) { playlist in | ||||
|                             Text(playlist.title).tag(playlist.id) | ||||
|                         } | ||||
|                     } | ||||
|                     .frame(maxWidth: 500) | ||||
|                     #if os(iOS) | ||||
|                         .pickerStyle(.inline) | ||||
|                     #elseif os(macOS) | ||||
|                         .labelsHidden() | ||||
|  | ||||
|                     #endif | ||||
|                 #endif | ||||
|             } | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     private var formAlignment: HorizontalAlignment { | ||||
|         #if os(tvOS) | ||||
|             .trailing | ||||
|         #else | ||||
|             .center | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     private var footer: some View { | ||||
|         HStack { | ||||
|             Spacer() | ||||
|             Button("Add to Playlist", action: addToPlaylist) | ||||
|             #if !os(tvOS) | ||||
|                 .keyboardShortcut(.defaultAction) | ||||
|             #endif | ||||
|             .disabled(model.currentPlaylist.isNil) | ||||
|                 .padding(.top, 30) | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     private var footerAlignment: HorizontalAlignment { | ||||
|         #if os(tvOS) | ||||
|             .trailing | ||||
|         #else | ||||
|             .leading | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     private var selectPlaylistButton: some View { | ||||
|         Button(model.currentPlaylist?.title ?? "Select playlist") { | ||||
|             guard model.currentPlaylist != nil else { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             model.selectedPlaylistID = model.all.next(after: model.currentPlaylist!)!.id | ||||
|         } | ||||
|         .contextMenu { | ||||
|             ForEach(model.all) { playlist in | ||||
|                 Button(playlist.title) { | ||||
|                     model.selectedPlaylistID = playlist.id | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func addToPlaylist() { | ||||
|         guard model.currentPlaylist != nil else { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         model.addVideoToCurrentPlaylist(videoID: video.id) { | ||||
|             dismiss() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct AddToPlaylistView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         AddToPlaylistView(video: Video.fixture) | ||||
|             .environmentObject(PlaylistsModel([Playlist.fixture])) | ||||
|             .environmentObject(SubscriptionsModel()) | ||||
|             .environmentObject(NavigationModel()) | ||||
|     } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct CoverSectionRowView<ControlContent: View>: View { | ||||
|     let label: String? | ||||
|     let controlView: ControlContent | ||||
|  | ||||
|     init(_ label: String? = nil, @ViewBuilder controlView: @escaping () -> ControlContent) { | ||||
|         self.label = label | ||||
|         self.controlView = controlView() | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         HStack { | ||||
|             Text(label ?? "") | ||||
|  | ||||
|             Spacer() | ||||
|             controlView | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,52 +0,0 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct CoverSectionView<Content: View>: View { | ||||
|     let title: String? | ||||
|  | ||||
|     let actionsView: Content | ||||
|     let divider: Bool | ||||
|     let inline: Bool | ||||
|  | ||||
|     init(_ title: String? = nil, divider: Bool = true, inline: Bool = false, @ViewBuilder actionsView: @escaping () -> Content) { | ||||
|         self.title = title | ||||
|         self.divider = divider | ||||
|         self.inline = inline | ||||
|         self.actionsView = actionsView() | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading) { | ||||
|             if inline { | ||||
|                 HStack { | ||||
|                     if title != nil { | ||||
|                         sectionTitle | ||||
|                     } | ||||
|  | ||||
|                     Spacer() | ||||
|                     actionsView | ||||
|                 } | ||||
|             } else if title != nil { | ||||
|                 sectionTitle | ||||
|             } | ||||
|  | ||||
|             if !inline { | ||||
|                 actionsView | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if divider { | ||||
|             Divider() | ||||
|                 .padding(.vertical) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var sectionTitle: some View { | ||||
|         Text(title ?? "") | ||||
|  | ||||
|             .font(.title2) | ||||
|         #if os(macOS) | ||||
|             .bold() | ||||
|         #endif | ||||
|         .padding(.bottom) | ||||
|     } | ||||
| } | ||||
| @@ -73,37 +73,13 @@ struct PlaylistFormView: View { | ||||
|             #endif | ||||
|  | ||||
|         #else | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|  | ||||
|             VStack { | ||||
|                     Spacer() | ||||
|  | ||||
|                     CoverSectionView(editing ? "Edit Playlist" : "Create Playlist") { | ||||
|                         CoverSectionRowView("Name") { | ||||
|                             TextField("Playlist Name", text: $name, onCommit: validate) | ||||
|                                 .frame(maxWidth: 450) | ||||
|                 Group { | ||||
|                     header | ||||
|                     form | ||||
|                 } | ||||
|  | ||||
|                         CoverSectionRowView("Visibility") { visibilityButton } | ||||
|                 .frame(maxWidth: 1000) | ||||
|             } | ||||
|  | ||||
|                     CoverSectionRowView { | ||||
|                         Button("Save", action: submitForm).disabled(!valid) | ||||
|                     } | ||||
|  | ||||
|                     if editing { | ||||
|                         CoverSectionView("Delete Playlist", divider: false, inline: true) { deletePlaylistButton } | ||||
|                             .padding(.top, 50) | ||||
|                     } | ||||
|  | ||||
|                     Spacer() | ||||
|                 } | ||||
|                 .frame(maxWidth: 800) | ||||
|  | ||||
|                 Spacer() | ||||
|             } | ||||
|             .background(.thinMaterial) | ||||
|             .onAppear { | ||||
|                 guard editing else { | ||||
|                     return | ||||
| @@ -114,9 +90,66 @@ struct PlaylistFormView: View { | ||||
|  | ||||
|                 validate() | ||||
|             } | ||||
|  | ||||
|             .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||||
|             .background(.thickMaterial) | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     var header: some View { | ||||
|         HStack(alignment: .center) { | ||||
|             Text(editing ? "Edit Playlist" : "Create Playlist") | ||||
|                 .font(.title2.bold()) | ||||
|  | ||||
|             Spacer() | ||||
|  | ||||
|             Button("Cancel") { | ||||
|                 dismiss() | ||||
|             } | ||||
|             #if !os(tvOS) | ||||
|                 .keyboardShortcut(.cancelAction) | ||||
|             #endif | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     var form: some View { | ||||
|         VStack(alignment: .trailing) { | ||||
|             VStack { | ||||
|                 Text("Name") | ||||
|                     .frame(maxWidth: .infinity, alignment: .leading) | ||||
|  | ||||
|                 TextField("Playlist Name", text: $name, onCommit: validate) | ||||
|             } | ||||
|  | ||||
|             HStack { | ||||
|                 Text("Visibility") | ||||
|                     .frame(maxWidth: .infinity, alignment: .leading) | ||||
|  | ||||
|                 visibilityButton | ||||
|             } | ||||
|             .padding(.top, 10) | ||||
|  | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|  | ||||
|                 Button("Save", action: submitForm).disabled(!valid) | ||||
|             } | ||||
|             .padding(.top, 40) | ||||
|  | ||||
|             if editing { | ||||
|                 Divider() | ||||
|                 HStack { | ||||
|                     Text("Delete playlist") | ||||
|                         .font(.title2.bold()) | ||||
|                     Spacer() | ||||
|                     deletePlaylistButton | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     func initializeForm() { | ||||
|         focused = true | ||||
|  | ||||
|   | ||||
| @@ -3,9 +3,10 @@ import Siesta | ||||
| import SwiftUI | ||||
|  | ||||
| struct PlaylistsView: View { | ||||
|     @StateObject private var store = Store<[Playlist]>() | ||||
|     @EnvironmentObject<PlaylistsModel> private var model | ||||
|  | ||||
|     @EnvironmentObject<InvidiousAPI> private var api | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|  | ||||
|     @State private var showingNewPlaylist = false | ||||
|     @State private var createdPlaylist: Playlist? | ||||
| @@ -13,14 +14,11 @@ struct PlaylistsView: View { | ||||
|     @State private var showingEditPlaylist = false | ||||
|     @State private var editedPlaylist: Playlist? | ||||
|  | ||||
|     @Default(.selectedPlaylistID) private var selectedPlaylistID | ||||
|  | ||||
|     var resource: Resource { | ||||
|         api.playlists | ||||
|     } | ||||
|     @State private var showingAddToPlaylist = false | ||||
|     @State private var videoIDToAddToPlaylist = "" | ||||
|  | ||||
|     var videos: [Video] { | ||||
|         currentPlaylist?.videos ?? [] | ||||
|         model.currentPlaylist?.videos ?? [] | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
| @@ -31,9 +29,9 @@ struct PlaylistsView: View { | ||||
|                         .font(.system(size: 28)) | ||||
|  | ||||
|                 #endif | ||||
|                 if currentPlaylist != nil, videos.isEmpty { | ||||
|                 if model.currentPlaylist != nil, videos.isEmpty { | ||||
|                     hintText("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"") | ||||
|                 } else if store.collection.isEmpty { | ||||
|                 } else if model.all.isEmpty { | ||||
|                     hintText("You have no playlists\n\nTap on \"New Playlist\" to create one") | ||||
|                 } else { | ||||
|                     VideosCellsVertical(videos: videos) | ||||
| @@ -58,11 +56,11 @@ struct PlaylistsView: View { | ||||
|         .toolbar { | ||||
|             ToolbarItemGroup { | ||||
|                 #if !os(iOS) | ||||
|                     if !store.collection.isEmpty { | ||||
|                     if !model.isEmpty { | ||||
|                         selectPlaylistButton | ||||
|                     } | ||||
|  | ||||
|                     if currentPlaylist != nil { | ||||
|                     if model.currentPlaylist != nil { | ||||
|                         editPlaylistButton | ||||
|                     } | ||||
|                 #endif | ||||
| @@ -72,7 +70,7 @@ struct PlaylistsView: View { | ||||
|             #if os(iOS) | ||||
|                 ToolbarItemGroup(placement: .bottomBar) { | ||||
|                     Group { | ||||
|                         if store.collection.isEmpty { | ||||
|                         if model.isEmpty { | ||||
|                             Text("No Playlists") | ||||
|                                 .foregroundColor(.secondary) | ||||
|                         } else { | ||||
| @@ -84,7 +82,7 @@ struct PlaylistsView: View { | ||||
|  | ||||
|                         Spacer() | ||||
|  | ||||
|                         if currentPlaylist != nil { | ||||
|                         if model.currentPlaylist != nil { | ||||
|                             editPlaylistButton | ||||
|                         } | ||||
|                     } | ||||
| @@ -93,17 +91,13 @@ struct PlaylistsView: View { | ||||
|             #endif | ||||
|         } | ||||
|         .onAppear { | ||||
|             resource.addObserver(store) | ||||
|  | ||||
|             resource.loadIfNeeded()?.onSuccess { _ in | ||||
|                 selectPlaylist(selectedPlaylistID) | ||||
|             } | ||||
|             model.load() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var toolbar: some View { | ||||
|         HStack { | ||||
|             if store.collection.isEmpty { | ||||
|             if model.isEmpty { | ||||
|                 Text("No Playlists") | ||||
|                     .foregroundColor(.secondary) | ||||
|             } else { | ||||
| @@ -117,7 +111,7 @@ struct PlaylistsView: View { | ||||
|                 Spacer() | ||||
|             #endif | ||||
|  | ||||
|             if currentPlaylist != nil { | ||||
|             if model.currentPlaylist != nil { | ||||
|                 editPlaylistButton | ||||
|             } | ||||
|  | ||||
| @@ -142,17 +136,15 @@ struct PlaylistsView: View { | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     func selectPlaylist(_ id: String?) { | ||||
|         selectedPlaylistID = id | ||||
|     } | ||||
|  | ||||
|     func selectCreatedPlaylist() { | ||||
|         guard createdPlaylist != nil else { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         resource.load().onSuccess { _ in | ||||
|             self.selectPlaylist(createdPlaylist?.id) | ||||
|         model.load(force: true) { | ||||
|             if let id = createdPlaylist?.id { | ||||
|                 self.model.selectPlaylist(id) | ||||
|             } | ||||
|  | ||||
|             self.createdPlaylist = nil | ||||
|         } | ||||
| @@ -160,41 +152,37 @@ struct PlaylistsView: View { | ||||
|  | ||||
|     func selectEditedPlaylist() { | ||||
|         if editedPlaylist.isNil { | ||||
|             selectPlaylist(nil) | ||||
|             model.selectPlaylist(nil) | ||||
|         } | ||||
|  | ||||
|         resource.load().onSuccess { _ in | ||||
|             selectPlaylist(editedPlaylist?.id) | ||||
|         model.load(force: true) { | ||||
|             model.selectPlaylist(editedPlaylist?.id) | ||||
|  | ||||
|             self.editedPlaylist = nil | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var currentPlaylist: Playlist? { | ||||
|         store.collection.first { $0.id == selectedPlaylistID } ?? store.collection.first | ||||
|     } | ||||
|  | ||||
|     var selectPlaylistButton: some View { | ||||
|         #if os(tvOS) | ||||
|             Button(currentPlaylist?.title ?? "Select playlist") { | ||||
|                 guard currentPlaylist != nil else { | ||||
|             Button(model.currentPlaylist?.title ?? "Select playlist") { | ||||
|                 guard model.currentPlaylist != nil else { | ||||
|                     return | ||||
|                 } | ||||
|  | ||||
|                 selectPlaylist(store.collection.next(after: currentPlaylist!)?.id) | ||||
|                 model.selectPlaylist(model.all.next(after: model.currentPlaylist!)?.id) | ||||
|             } | ||||
|             .contextMenu { | ||||
|                 ForEach(store.collection) { playlist in | ||||
|                 ForEach(model.all) { playlist in | ||||
|                     Button(playlist.title) { | ||||
|                         selectPlaylist(playlist.id) | ||||
|                         model.selectPlaylist(playlist.id) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         #else | ||||
|             Menu(currentPlaylist?.title ?? "Select playlist") { | ||||
|                 ForEach(store.collection) { playlist in | ||||
|                     Button(action: { selectPlaylist(playlist.id) }) { | ||||
|                         if playlist == self.currentPlaylist { | ||||
|             Menu(model.currentPlaylist?.title ?? "Select playlist") { | ||||
|                 ForEach(model.all) { playlist in | ||||
|                     Button(action: { model.selectPlaylist(playlist.id) }) { | ||||
|                         if playlist == model.currentPlaylist { | ||||
|                             Label(playlist.title, systemImage: "checkmark") | ||||
|                         } else { | ||||
|                             Text(playlist.title) | ||||
| @@ -207,7 +195,7 @@ struct PlaylistsView: View { | ||||
|  | ||||
|     var editPlaylistButton: some View { | ||||
|         Button(action: { | ||||
|             self.editedPlaylist = self.currentPlaylist | ||||
|             self.editedPlaylist = self.model.currentPlaylist | ||||
|             self.showingEditPlaylist = true | ||||
|         }) { | ||||
|             HStack(spacing: 8) { | ||||
|   | ||||
| @@ -20,12 +20,18 @@ struct AccountFormView: View { | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack { | ||||
|             Group { | ||||
|                 header | ||||
|                 form | ||||
|                 footer | ||||
|             } | ||||
|             .frame(maxWidth: 1000) | ||||
|         } | ||||
|         #if os(iOS) | ||||
|             .padding(.vertical) | ||||
|         #elseif os(tvOS) | ||||
|             .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||||
|             .background(.thickMaterial) | ||||
|         #else | ||||
|             .frame(width: 400, height: 145) | ||||
|         #endif | ||||
| @@ -48,18 +54,30 @@ struct AccountFormView: View { | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     var form: some View { | ||||
|     private var form: some View { | ||||
|         Group { | ||||
|             #if !os(tvOS) | ||||
|                 Form { | ||||
|                     formFields | ||||
|                     #if os(macOS) | ||||
|                         .padding(.horizontal) | ||||
|                     #endif | ||||
|                 } | ||||
|             #else | ||||
|                 formFields | ||||
|             #endif | ||||
|         } | ||||
|         .onAppear(perform: initializeForm) | ||||
|         .onChange(of: sid) { _ in validate() } | ||||
|     } | ||||
|  | ||||
|     var formFields: some View { | ||||
|         Group { | ||||
|             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 { | ||||
| @@ -75,6 +93,9 @@ struct AccountFormView: View { | ||||
|             #endif | ||||
|         } | ||||
|         .frame(minHeight: 35) | ||||
|         #if os(tvOS) | ||||
|             .padding(.top, 30) | ||||
|         #endif | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,9 @@ struct InstanceDetailsSettingsView: View { | ||||
|     var body: some View { | ||||
|         List { | ||||
|             Section(header: Text("Accounts")) { | ||||
|                 ForEach(instances.accounts(instanceID)) { account in | ||||
|                 ForEach(instances.accounts(instanceID), id: \.self) { account in | ||||
|  | ||||
|                     #if !os(tvOS) | ||||
|                         HStack(spacing: 2) { | ||||
|                             Text(account.description) | ||||
|                             if instances.defaultAccount == account { | ||||
| @@ -23,7 +25,6 @@ struct InstanceDetailsSettingsView: View { | ||||
|                                     .foregroundColor(.secondary) | ||||
|                             } | ||||
|                         } | ||||
|                     #if !os(tvOS) | ||||
|                         .swipeActions(edge: .leading, allowsFullSwipe: true) { | ||||
|                             if instances.defaultAccount != account { | ||||
|                                 Button("Make Default", action: { makeDefault(account) }) | ||||
| @@ -34,6 +35,21 @@ struct InstanceDetailsSettingsView: View { | ||||
|                         .swipeActions(edge: .trailing, allowsFullSwipe: false) { | ||||
|                             Button("Remove", role: .destructive, action: { removeAccount(account) }) | ||||
|                         } | ||||
|  | ||||
|                     #else | ||||
|                         Button(action: { toggleDefault(account) }) { | ||||
|                             HStack(spacing: 2) { | ||||
|                                 Text(account.description) | ||||
|                                 if instances.defaultAccount == account { | ||||
|                                     Text("— default") | ||||
|                                         .foregroundColor(.secondary) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         .contextMenu { | ||||
|                             Button("Toggle Default", action: { toggleDefault(account) }) | ||||
|                             Button("Remove", role: .destructive, action: { removeAccount(account) }) | ||||
|                         } | ||||
|                     #endif | ||||
|                 } | ||||
|                 .redrawOn(change: accountsChanged) | ||||
| @@ -45,6 +61,8 @@ struct InstanceDetailsSettingsView: View { | ||||
|         } | ||||
|         #if os(iOS) | ||||
|             .listStyle(.insetGrouped) | ||||
|         #elseif os(tvOS) | ||||
|             .frame(maxWidth: 1000) | ||||
|         #endif | ||||
|  | ||||
|         .navigationTitle(instance.shortDescription) | ||||
| @@ -58,6 +76,14 @@ struct InstanceDetailsSettingsView: View { | ||||
|         accountsChanged.toggle() | ||||
|     } | ||||
|  | ||||
|     private func toggleDefault(_ account: Instance.Account) { | ||||
|         if account == instances.defaultAccount { | ||||
|             resetDefaultAccount() | ||||
|         } else { | ||||
|             makeDefault(account) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private func resetDefaultAccount() { | ||||
|         instances.resetDefaultAccount() | ||||
|         accountsChanged.toggle() | ||||
|   | ||||
| @@ -19,6 +19,28 @@ struct InstanceFormView: View { | ||||
|  | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading) { | ||||
|             Group { | ||||
|                 header | ||||
|  | ||||
|                 form | ||||
|  | ||||
|                 footer | ||||
|             } | ||||
|             .frame(maxWidth: 1000) | ||||
|         } | ||||
|         .onChange(of: url) { _ in validate() } | ||||
|         .onAppear(perform: initializeForm) | ||||
|         #if os(iOS) | ||||
|             .padding(.vertical) | ||||
|         #elseif os(tvOS) | ||||
|             .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||||
|             .background(.thickMaterial) | ||||
|         #else | ||||
|             .frame(width: 400, height: 150) | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     private var header: some View { | ||||
|         HStack(alignment: .center) { | ||||
|             Text("Add Instance") | ||||
|                 .font(.title2.bold()) | ||||
| @@ -33,20 +55,50 @@ struct InstanceFormView: View { | ||||
|             #endif | ||||
|         } | ||||
|         .padding(.horizontal) | ||||
|  | ||||
|             Form { | ||||
|                 TextField("Name", text: $name, prompt: Text("Instance Name (optional)")) | ||||
|                     .frame(maxWidth: 450) | ||||
|                     .focused($nameFieldFocused) | ||||
|  | ||||
|                 TextField("URL", text: $url, prompt: Text("https://invidious.home.net")) | ||||
|                     .frame(maxWidth: 450) | ||||
|     } | ||||
|  | ||||
|     private var form: some View { | ||||
|         #if !os(tvOS) | ||||
|             Form { | ||||
|                 formFields | ||||
|                 #if os(macOS) | ||||
|                     .padding(.horizontal) | ||||
|                 #endif | ||||
|             } | ||||
|         #else | ||||
|             formFields | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|             HStack { | ||||
|     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")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var footer: some View { | ||||
|         HStack(alignment: .center) { | ||||
|             validationStatus | ||||
|  | ||||
|             Spacer() | ||||
|  | ||||
|             Button("Save", action: submitForm) | ||||
|                 .disabled(!valid) | ||||
|             #if !os(tvOS) | ||||
|                 .keyboardShortcut(.defaultAction) | ||||
|             #endif | ||||
|         } | ||||
|         #if os(tvOS) | ||||
|             .padding(.top, 30) | ||||
|         #endif | ||||
|         .padding(.horizontal) | ||||
|     } | ||||
|  | ||||
|     private var validationStatus: some View { | ||||
|         HStack(spacing: 4) { | ||||
|             Image(systemName: valid ? "checkmark.circle.fill" : "xmark.circle.fill") | ||||
|                 .foregroundColor(valid ? .green : .red) | ||||
| @@ -60,26 +112,9 @@ struct InstanceFormView: View { | ||||
|                         .lineLimit(1) | ||||
|                 } | ||||
|             } | ||||
|                     .frame(minHeight: 40) | ||||
|             .frame(minHeight: 35) | ||||
|         } | ||||
|         .opacity(validated ? 1 : 0) | ||||
|                 Spacer() | ||||
|  | ||||
|                 Button("Save", action: submitForm) | ||||
|                     .disabled(!valid) | ||||
|                 #if !os(tvOS) | ||||
|                     .keyboardShortcut(.defaultAction) | ||||
|                 #endif | ||||
|             } | ||||
|             .padding(.horizontal) | ||||
|         } | ||||
|         .onChange(of: url) { _ in validate() } | ||||
|         .onAppear(perform: initializeForm) | ||||
|         #if os(iOS) | ||||
|             .padding(.vertical) | ||||
|         #else | ||||
|             .frame(width: 400, height: 150) | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     var validator: AccountValidator { | ||||
|   | ||||
| @@ -34,7 +34,7 @@ struct InstancesSettingsView: View { | ||||
|     var body: some View { | ||||
|         Group { | ||||
|             #if os(iOS) | ||||
|                 Section(header: instancesHeader, footer: instancesFooter) { | ||||
|                 Section(header: instancesHeader, footer: defaultAccountSection) { | ||||
|                     ForEach(instances) { instance in | ||||
|                         Button(action: { | ||||
|                             self.selectedInstanceID = instance.id | ||||
| @@ -59,11 +59,33 @@ struct InstancesSettingsView: View { | ||||
|                         .buttonStyle(.plain) | ||||
|                     } | ||||
|  | ||||
|                     Button("Add Instance...") { | ||||
|                         presentingInstanceForm = true | ||||
|                     } | ||||
|                     addInstanceButton | ||||
|                 } | ||||
|                 .listStyle(.insetGrouped) | ||||
|             #elseif os(tvOS) | ||||
|                 Section(header: instancesHeader) { | ||||
|                     ForEach(instances) { instance in | ||||
|                         Button(action: { | ||||
|                             self.selectedInstanceID = instance.id | ||||
|                             self.presentingInstanceDetails = true | ||||
|                         }) { | ||||
|                             Text(instance.description) | ||||
|                         } | ||||
|                         .contextMenu { | ||||
|                             Button("Remove", role: .destructive) { | ||||
|                                 instancesModel.remove(instance) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     addInstanceButton | ||||
|  | ||||
|                     defaultAccountSection | ||||
|                 } | ||||
|                 .frame(maxWidth: 1000, alignment: .leading) | ||||
|                 .sheet(isPresented: $presentingAccountForm) { | ||||
|                     AccountFormView(instance: selectedInstance, selectedAccount: $selectedAccount) | ||||
|                 } | ||||
|             #else | ||||
|                 Section { | ||||
|                     Text("Instance") | ||||
| @@ -157,22 +179,33 @@ struct InstancesSettingsView: View { | ||||
|         Text("Instances").background(instanceDetailsNavigationLink) | ||||
|     } | ||||
|  | ||||
|     var instancesFooter: some View { | ||||
|     var defaultAccountSection: some View { | ||||
|         Group { | ||||
|             if let account = instancesModel.defaultAccount { | ||||
|                 VStack { | ||||
|                     HStack(spacing: 2) { | ||||
|                         Text("**\(account.description)** account on instance **\(account.instance.shortDescription)** is your default.") | ||||
|                             .truncationMode(.middle) | ||||
|                             .lineLimit(1) | ||||
|  | ||||
|                         #if !os(tvOS) | ||||
|  | ||||
|                             Button("Reset", action: resetDefaultAccount) | ||||
|                                 .buttonStyle(.plain) | ||||
|                                 .foregroundColor(.red) | ||||
|                         #endif | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 Text("You have no default account set") | ||||
|             } | ||||
|         } | ||||
|         #if os(tvOS) | ||||
|             .foregroundColor(.gray) | ||||
|         #elseif os(macOS) | ||||
|             .font(.caption2) | ||||
|             .foregroundColor(.secondary) | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     var instanceDetailsNavigationLink: some View { | ||||
| @@ -181,25 +214,13 @@ struct InstancesSettingsView: View { | ||||
|             destination: { InstanceDetailsSettingsView(instanceID: selectedInstanceID) }, | ||||
|             label: { EmptyView() } | ||||
|         ) | ||||
|         .opacity(0) | ||||
|     } | ||||
|  | ||||
|     private var defaultAccountSection: some View { | ||||
|         Group { | ||||
|             if let account = instancesModel.defaultAccount { | ||||
|                 HStack(spacing: 2) { | ||||
|                     Text("**\(account.description)** account on instance **\(account.instance.shortDescription)** is your default.") | ||||
|                         .truncationMode(.middle) | ||||
|                         .lineLimit(1) | ||||
|                     Button("Reset", action: resetDefaultAccount) | ||||
|                         .buttonStyle(.plain) | ||||
|                         .foregroundColor(.red) | ||||
|     private var addInstanceButton: some View { | ||||
|         Button("Add Instance...") { | ||||
|             presentingInstanceForm = true | ||||
|         } | ||||
|             } else { | ||||
|                 Text("You have no default account set") | ||||
|             } | ||||
|         } | ||||
|         .font(.caption2) | ||||
|         .foregroundColor(.secondary) | ||||
|     } | ||||
|  | ||||
|     private func resetDefaultAccount() { | ||||
|   | ||||
| @@ -15,6 +15,8 @@ struct PlaybackSettingsView: View { | ||||
|  | ||||
|             #if os(iOS) | ||||
|                 .pickerStyle(.automatic) | ||||
|             #elseif os(tvOS) | ||||
|                 .pickerStyle(.inline) | ||||
|             #endif | ||||
|  | ||||
|             #if os(macOS) | ||||
|   | ||||
| @@ -33,24 +33,31 @@ struct SettingsView: View { | ||||
|         #else | ||||
|             NavigationView { | ||||
|                 List { | ||||
|                     #if os(tvOS) | ||||
|                         AccountSelectionView() | ||||
|                     #endif | ||||
|                     InstancesSettingsView() | ||||
|                     PlaybackSettingsView() | ||||
|                 } | ||||
|                 .navigationTitle("Settings") | ||||
|                 .toolbar { | ||||
|                     ToolbarItem(placement: .navigationBarTrailing) { | ||||
|                         #if !os(tvOS) | ||||
|                             Button("Done") { | ||||
|                                 dismiss() | ||||
|                             } | ||||
|                         #if !os(tvOS) | ||||
|                             .keyboardShortcut(.cancelAction) | ||||
|                         #endif | ||||
|                     } | ||||
|                 } | ||||
|                 .frame(maxWidth: 1000) | ||||
|                 #if os(iOS) | ||||
|                     .listStyle(.insetGrouped) | ||||
|                 #endif | ||||
|             } | ||||
|             #if os(tvOS) | ||||
|                 .background(.thickMaterial) | ||||
|             #endif | ||||
|         #endif | ||||
|     } | ||||
| } | ||||
| @@ -58,6 +65,11 @@ struct SettingsView: View { | ||||
| struct SettingsView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SettingsView() | ||||
|             .environmentObject(InstancesModel()) | ||||
|             .environmentObject(InvidiousAPI()) | ||||
|             .environmentObject(NavigationModel()) | ||||
|             .environmentObject(SearchModel()) | ||||
|             .environmentObject(SubscriptionsModel()) | ||||
|         #if os(macOS) | ||||
|             .frame(width: 600, height: 300) | ||||
|         #endif | ||||
|   | ||||
| @@ -69,6 +69,9 @@ struct ChannelVideosView: View { | ||||
|                 } | ||||
|             } | ||||
|         #endif | ||||
|         #if os(tvOS) | ||||
|             .background(.thickMaterial) | ||||
|         #endif | ||||
|         .modifier(UnsubscribeAlertModifier()) | ||||
|             .onAppear { | ||||
|                 if store.item.isNil { | ||||
|   | ||||
| @@ -47,6 +47,10 @@ struct SignInRequiredView<Content: View>: View { | ||||
|             if instances.isEmpty { | ||||
|                 openSettingsButton | ||||
|             } | ||||
|  | ||||
|             #if os(tvOS) | ||||
|                 openSettingsButton | ||||
|             #endif | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,22 +4,24 @@ import SwiftUI | ||||
| struct VideoContextMenuView: View { | ||||
|     @EnvironmentObject<InvidiousAPI> private var api | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|     @EnvironmentObject<PlaylistsModel> private var playlists | ||||
|     @EnvironmentObject<RecentsModel> private var recents | ||||
|     @EnvironmentObject<SubscriptionsModel> private var subscriptions | ||||
|  | ||||
|     let video: Video | ||||
|  | ||||
|     @Default(.showingAddToPlaylist) var showingAddToPlaylist | ||||
|     @Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist | ||||
|  | ||||
|     var body: some View { | ||||
|         Section { | ||||
|             openChannelButton | ||||
|  | ||||
|             subscriptionButton | ||||
|  | ||||
|             if case let .playlist(id) = navigation.tabSelection { | ||||
|                 removeFromPlaylistButton(playlistID: id) | ||||
|             } | ||||
|  | ||||
|             if navigation.tabSelection == .playlists { | ||||
|                 removeFromPlaylistButton | ||||
|                 removeFromPlaylistButton(playlistID: playlists.currentPlaylist!.id) | ||||
|             } else { | ||||
|                 addToPlaylistButton | ||||
|             } | ||||
| @@ -29,7 +31,7 @@ struct VideoContextMenuView: View { | ||||
|     var openChannelButton: some View { | ||||
|         Button("\(video.author) Channel") { | ||||
|             let recent = RecentItem(from: video.channel) | ||||
|             recents.open(recent) | ||||
|             recents.add(recent) | ||||
|             navigation.tabSelection = .recentlyOpened(recent.tag) | ||||
|             navigation.isChannelOpen = true | ||||
|             navigation.sidebarSectionChanged.toggle() | ||||
| @@ -58,17 +60,13 @@ struct VideoContextMenuView: View { | ||||
|  | ||||
|     var addToPlaylistButton: some View { | ||||
|         Button("Add to playlist...") { | ||||
|             videoIDToAddToPlaylist = video.id | ||||
|             showingAddToPlaylist = true | ||||
|             navigation.presentAddToPlaylist(video) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var removeFromPlaylistButton: some View { | ||||
|     func removeFromPlaylistButton(playlistID: String) -> some View { | ||||
|         Button("Remove from playlist", role: .destructive) { | ||||
|             let resource = api.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!) | ||||
|             resource.request(.delete).onSuccess { _ in | ||||
|                 api.playlists.load() | ||||
|             } | ||||
|             playlists.removeVideoFromPlaylist(videoIndexID: video.indexID!, playlistID: playlistID) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ struct WatchNowSection: View { | ||||
|         WatchNowSectionBody(label: label, videos: store.collection) | ||||
|             .onAppear { | ||||
|                 resource.addObserver(store) | ||||
|                 resource.load() | ||||
|                 resource.loadIfNeeded() | ||||
|             } | ||||
|             .onChange(of: api.account) { _ in | ||||
|                 resource.load() | ||||
|   | ||||
							
								
								
									
										47
									
								
								tvOS/AccountSelectionView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tvOS/AccountSelectionView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import Defaults | ||||
| import Foundation | ||||
| import SwiftUI | ||||
|  | ||||
| struct AccountSelectionView: View { | ||||
|     @EnvironmentObject<InstancesModel> private var instancesModel | ||||
|     @EnvironmentObject<InvidiousAPI> private var api | ||||
|  | ||||
|     @Default(.accounts) private var accounts | ||||
|     @Default(.instances) private var instances | ||||
|  | ||||
|     var body: some View { | ||||
|         Section(header: Text("Current Account")) { | ||||
|             Button(api.account?.name ?? "Not selected") { | ||||
|                 if let account = nextAccount { | ||||
|                     api.setAccount(account) | ||||
|                 } | ||||
|             } | ||||
|             .disabled(nextAccount == nil) | ||||
|             .contextMenu { | ||||
|                 ForEach(instances) { instance in | ||||
|                     Button(accountButtonTitle(instance: instance, account: instance.anonymousAccount)) { | ||||
|                         api.setAccount(instance.anonymousAccount) | ||||
|                     } | ||||
|  | ||||
|                     ForEach(instancesModel.accounts(instance.id)) { account in | ||||
|                         Button(accountButtonTitle(instance: instance, account: account)) { | ||||
|                             api.setAccount(account) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var nextAccount: Instance.Account? { | ||||
|         guard api.account != nil else { | ||||
|             return accounts.first | ||||
|         } | ||||
|  | ||||
|         return accounts.next(after: api.account!) | ||||
|     } | ||||
|  | ||||
|     func accountButtonTitle(instance: Instance, account: Instance.Account) -> String { | ||||
|         instances.count > 1 ? "\(account.description) — \(instance.shortDescription)" : account.description | ||||
|     } | ||||
| } | ||||
| @@ -1,99 +0,0 @@ | ||||
| import Defaults | ||||
| import Siesta | ||||
| import SwiftUI | ||||
|  | ||||
| struct AddToPlaylistView: View { | ||||
|     @StateObject private var store = Store<[Playlist]>() | ||||
|  | ||||
|     @State private var selectedPlaylist: Playlist? | ||||
|  | ||||
|     @Default(.videoIDToAddToPlaylist) private var videoID | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|  | ||||
|     @EnvironmentObject<InvidiousAPI> private var api | ||||
|  | ||||
|     var resource: Resource { | ||||
|         api.playlists | ||||
|     } | ||||
|  | ||||
|     init() { | ||||
|         resource.addObserver(store) | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         HStack { | ||||
|             Spacer() | ||||
|  | ||||
|             VStack { | ||||
|                 Spacer() | ||||
|  | ||||
|                 if !resource.isLoading && store.collection.isEmpty { | ||||
|                     CoverSectionView("You have no Playlists", inline: true) { | ||||
|                         Text("Open \"Playlists\" tab to create new one") | ||||
|                             .foregroundColor(.secondary) | ||||
|                             .multilineTextAlignment(.center) | ||||
|                     } | ||||
|                     Button("Go back") { | ||||
|                         dismiss() | ||||
|                     } | ||||
|                     .padding() | ||||
|                 } else if !store.collection.isEmpty { | ||||
|                     CoverSectionView("Add to Playlist", inline: true) { selectPlaylistButton } | ||||
|  | ||||
|                     CoverSectionRowView { | ||||
|                         Button("Add", action: addToPlaylist) | ||||
|                             .disabled(currentPlaylist.isNil) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 Spacer() | ||||
|             } | ||||
|             .frame(maxWidth: 1200) | ||||
|  | ||||
|             Spacer() | ||||
|         } | ||||
|         .background(.thinMaterial) | ||||
|         .onAppear { | ||||
|             resource.loadIfNeeded()?.onSuccess { _ in | ||||
|                 selectedPlaylist = store.collection.first | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var selectPlaylistButton: some View { | ||||
|         Button(currentPlaylist?.title ?? "Select playlist") { | ||||
|             guard currentPlaylist != nil else { | ||||
|                 return | ||||
|             } | ||||
|  | ||||
|             self.selectedPlaylist = store.collection.next(after: currentPlaylist!) | ||||
|         } | ||||
|         .contextMenu { | ||||
|             ForEach(store.collection) { playlist in | ||||
|                 Button(playlist.title) { | ||||
|                     self.selectedPlaylist = playlist | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var currentPlaylist: Playlist? { | ||||
|         selectedPlaylist ?? store.collection.first | ||||
|     } | ||||
|  | ||||
|     func addToPlaylist() { | ||||
|         guard currentPlaylist != nil else { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         let resource = api.playlistVideos(currentPlaylist!.id) | ||||
|         let body = ["videoId": videoID] | ||||
|  | ||||
|         resource.request(.post, json: body).onSuccess { _ in | ||||
|             Defaults.reset(.videoIDToAddToPlaylist) | ||||
|             api.playlists.load() | ||||
|             dismiss() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| import Defaults | ||||
| import SwiftUI | ||||
|  | ||||
| struct OptionsView: View { | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|  | ||||
|     var body: some View { | ||||
|         HStack { | ||||
|             VStack { | ||||
|                 HStack { | ||||
|                     Spacer() | ||||
|  | ||||
|                     VStack(alignment: .leading) { | ||||
|                         Spacer() | ||||
|  | ||||
|                         CoverSectionView("View Options") { | ||||
| //                            CoverSectionRowView("Show videos as") { nextLayoutButton } | ||||
|                         } | ||||
|  | ||||
|                         CoverSectionView(divider: false) { | ||||
|                             CoverSectionRowView("Close View Options") { Button("Close") { dismiss() } } | ||||
|                         } | ||||
|  | ||||
|                         Spacer() | ||||
|  | ||||
|                         SettingsView() | ||||
|                     } | ||||
|                     .frame(maxWidth: 800) | ||||
|  | ||||
|                     Spacer() | ||||
|                 } | ||||
|  | ||||
|                 Spacer() | ||||
|             } | ||||
|         } | ||||
|         .background(.thinMaterial) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct OptionsView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         OptionsView() | ||||
|     } | ||||
| } | ||||
| @@ -7,10 +7,6 @@ struct TVNavigationView: View { | ||||
|     @EnvironmentObject<RecentsModel> private var recents | ||||
|     @EnvironmentObject<SearchModel> private var search | ||||
|  | ||||
|     @State private var showingOptions = false | ||||
|  | ||||
|     @Default(.showingAddToPlaylist) var showingAddToPlaylist | ||||
|  | ||||
|     var body: some View { | ||||
|         TabView(selection: $navigation.tabSelection) { | ||||
|             WatchNowView() | ||||
| @@ -37,8 +33,12 @@ struct TVNavigationView: View { | ||||
|                 .tabItem { Image(systemName: "magnifyingglass") } | ||||
|                 .tag(TabSelection.search) | ||||
|         } | ||||
|         .fullScreenCover(isPresented: $showingOptions) { OptionsView() } | ||||
|         .fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() } | ||||
|         .fullScreenCover(isPresented: $navigation.presentingSettings) { SettingsView() } | ||||
|         .fullScreenCover(isPresented: $navigation.presentingAddToPlaylist) { | ||||
|             if let video = navigation.videoToAddToPlaylist { | ||||
|                 AddToPlaylistView(video: video) | ||||
|             } | ||||
|         } | ||||
|         .fullScreenCover(isPresented: $navigation.showingVideo) { | ||||
|             if let video = navigation.video { | ||||
|                 VideoPlayerView(video) | ||||
| @@ -48,15 +48,19 @@ struct TVNavigationView: View { | ||||
|         .fullScreenCover(isPresented: $navigation.isChannelOpen) { | ||||
|             if let channel = recents.presentedChannel { | ||||
|                 ChannelVideosView(channel: channel) | ||||
|                     .background(.thickMaterial) | ||||
|             } | ||||
|         } | ||||
|         .onPlayPauseCommand { showingOptions.toggle() } | ||||
|         .onPlayPauseCommand { navigation.presentingSettings.toggle() } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct TVNavigationView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         TVNavigationView() | ||||
|             .environmentObject(InvidiousAPI()) | ||||
|             .environmentObject(NavigationModel()) | ||||
|             .environmentObject(SearchModel()) | ||||
|             .environmentObject(InstancesModel()) | ||||
|             .environmentObject(SubscriptionsModel()) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal