Playlists and channels in the sidebar

This commit is contained in:
Arkadiusz Fal 2021-08-29 23:36:18 +02:00
parent 1196a2a5e2
commit 1651110a5d
13 changed files with 300 additions and 48 deletions

View File

@ -3,7 +3,7 @@ import Defaults
import Foundation import Foundation
import SwiftyJSON import SwiftyJSON
struct Channel: Codable, Defaults.Serializable { struct Channel: Codable, Identifiable, Defaults.Serializable {
var id: String var id: String
var name: String var name: String
var subscriptionsCount: String var subscriptionsCount: String

View File

@ -54,6 +54,10 @@ final class InvidiousAPI: Service {
content.json.arrayValue.map(Playlist.init) content.json.arrayValue.map(Playlist.init)
} }
configureTransformer("/auth/playlists/*", requestMethods: [.get]) { (content: Entity<JSON>) -> Playlist in
Playlist(content.json)
}
configureTransformer("/auth/playlists", requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in configureTransformer("/auth/playlists", requestMethods: [.post, .patch]) { (content: Entity<Data>) -> Playlist in
// hacky, to verify if possible to get it in easier way // hacky, to verify if possible to get it in easier way
Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!)) Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!))

View File

@ -2,6 +2,10 @@ import Foundation
import SwiftUI import SwiftUI
final class NavigationState: ObservableObject { final class NavigationState: ObservableObject {
enum TabSelection: Hashable {
case subscriptions, popular, trending, playlists, channel(String), playlist(String), search
}
@Published var tabSelection: TabSelection = .subscriptions @Published var tabSelection: TabSelection = .subscriptions
@Published var showingChannel = false @Published var showingChannel = false
@ -13,6 +17,12 @@ final class NavigationState: ObservableObject {
@Published var returnToDetails = false @Published var returnToDetails = false
@Published var presentingPlaylistForm = false
@Published var editedPlaylist: Playlist!
@Published var presentingUnsubscribeAlert = false
@Published var channelToUnsubscribe: Channel!
func openChannel(_ channel: Channel) { func openChannel(_ channel: Channel) {
returnToDetails = false returnToDetails = false
self.channel = channel self.channel = channel
@ -54,4 +64,21 @@ final class NavigationState: ObservableObject {
} }
) )
} }
func presentEditPlaylistForm(_ playlist: Playlist?) {
editedPlaylist = playlist
presentingPlaylistForm = editedPlaylist != nil
}
func presentNewPlaylistForm() {
editedPlaylist = nil
presentingPlaylistForm = true
}
func presentUnsubscribeAlert(_ channel: Channel?) {
channelToUnsubscribe = channel
presentingUnsubscribeAlert = channelToUnsubscribe != nil
}
} }
typealias TabSelection = NavigationState.TabSelection

35
Model/Playlists.swift Normal file
View File

@ -0,0 +1,35 @@
import Foundation
import Siesta
import SwiftUI
final class Playlists: ObservableObject {
@Published var playlists = [Playlist]()
var resource: Resource {
InvidiousAPI.shared.playlists
}
init() {
load()
}
var all: [Playlist] {
playlists.sorted { $0.title.lowercased() < $1.title.lowercased() }
}
func find(id: Playlist.ID) -> Playlist? {
all.first { $0.id == id }
}
func reload() {
load()
}
fileprivate func load() {
resource.load().onSuccess { resource in
if let playlists: [Playlist] = resource.typedContent() {
self.playlists = playlists
}
}
}
}

View File

@ -13,6 +13,10 @@ final class Subscriptions: ObservableObject {
load() load()
} }
var all: [Channel] {
channels.sorted { $0.name.lowercased() < $1.name.lowercased() }
}
func subscribe(_ channelID: String) { func subscribe(_ channelID: String) {
performChannelSubscriptionRequest(channelID, method: .post) performChannelSubscriptionRequest(channelID, method: .post)
} }

View File

@ -120,6 +120,19 @@
37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; }; 37B81B0526D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; }; 37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; }; 37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81B0426D2CEDA00675966 /* PlaybackState.swift */; };
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */; };
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
37BA794026DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */; };
37BA794326DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
37BA794426DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
37BA794526DBA973002A0235 /* Playlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794226DBA973002A0235 /* Playlists.swift */; };
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */; };
37BA794B26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */; };
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; }; 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */; };
37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; }; 37BADCA52699FB72009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA42699FB72009BE4FB /* Alamofire */; };
37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; }; 37BADCA7269A552E009BE4FB /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 37BADCA6269A552E009BE4FB /* Alamofire */; };
@ -136,7 +149,6 @@
37BD07C72698B27B003EBB87 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07C62698B27B003EBB87 /* Introspect */; }; 37BD07C72698B27B003EBB87 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 37BD07C62698B27B003EBB87 /* Introspect */; };
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C32671614700C925CA /* AppTabNavigation.swift */; }; 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C32671614700C925CA /* AppTabNavigation.swift */; };
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07B42698AA4D003EBB87 /* ContentView.swift */; }; 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07B42698AA4D003EBB87 /* ContentView.swift */; };
37BD07CA2698FBE5003EBB87 /* AppSidebarNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */; };
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; }; 37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; };
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; }; 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; };
37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; }; 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; };
@ -260,6 +272,11 @@
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; }; 37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; }; 37B81B0126D2CAE700675966 /* PlaybackBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBar.swift; sourceTree = "<group>"; };
37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; }; 37B81B0426D2CEDA00675966 /* PlaybackState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackState.swift; sourceTree = "<group>"; };
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVideosView.swift; sourceTree = "<group>"; };
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelVideosView.swift; sourceTree = "<group>"; };
37BA794226DBA973002A0235 /* Playlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlists.swift; sourceTree = "<group>"; };
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarSubscriptions.swift; sourceTree = "<group>"; };
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarPlaylists.swift; sourceTree = "<group>"; };
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; }; 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVNavigationView.swift; sourceTree = "<group>"; };
37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 37BD07B42698AA4D003EBB87 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; }; 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarNavigation.swift; sourceTree = "<group>"; };
@ -362,6 +379,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */, 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */,
37D4B0C32671614700C925CA /* AppTabNavigation.swift */, 37D4B0C32671614700C925CA /* AppTabNavigation.swift */,
37BD07B42698AA4D003EBB87 /* ContentView.swift */, 37BD07B42698AA4D003EBB87 /* ContentView.swift */,
); );
@ -417,6 +436,8 @@
371AAE2826CEC7D900901972 /* Views */ = { 371AAE2826CEC7D900901972 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37BA793E26DB8F97002A0235 /* ChannelVideosView.swift */,
37BA793A26DB8EE4002A0235 /* PlaylistVideosView.swift */,
37AAF27D26737323007FC770 /* PopularView.swift */, 37AAF27D26737323007FC770 /* PopularView.swift */,
37AAF27F26737550007FC770 /* SearchView.swift */, 37AAF27F26737550007FC770 /* SearchView.swift */,
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */, 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */,
@ -574,6 +595,7 @@
37B81B0426D2CEDA00675966 /* PlaybackState.swift */, 37B81B0426D2CEDA00675966 /* PlaybackState.swift */,
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */, 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */,
376578882685471400D4EA09 /* Playlist.swift */, 376578882685471400D4EA09 /* Playlist.swift */,
37BA794226DBA973002A0235 /* Playlists.swift */,
37C7A1DB267CE9D90010EAD6 /* Profile.swift */, 37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
373CFACA26966264003CB2C6 /* SearchQuery.swift */, 373CFACA26966264003CB2C6 /* SearchQuery.swift */,
3711403E26B206A6005B3555 /* SearchState.swift */, 3711403E26B206A6005B3555 /* SearchState.swift */,
@ -842,10 +864,12 @@
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */,
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */, 37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
3711403F26B206A6005B3555 /* SearchState.swift in Sources */, 3711403F26B206A6005B3555 /* SearchState.swift in Sources */,
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
@ -854,6 +878,7 @@
37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */, 37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */,
376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */,
37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
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 */,
@ -866,6 +891,7 @@
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoView.swift in Sources */,
37BA794326DBA973002A0235 /* Playlists.swift in Sources */,
37AAF29026740715007FC770 /* Channel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */,
3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */, 37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */,
@ -878,6 +904,7 @@
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */,
37BA794B26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */, 37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */, 373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */,
@ -903,6 +930,7 @@
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
373CFABF26966149003CB2C6 /* CoverSectionView.swift in Sources */, 373CFABF26966149003CB2C6 /* CoverSectionView.swift in Sources */,
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 37EAD86C267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
37CEE4C22677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C22677B697005A1EFE /* Stream.swift in Sources */,
371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */, 371F2F1B269B43D300E4A7AB /* NavigationState.swift in Sources */,
@ -912,6 +940,7 @@
37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */, 37BD07C32698AD4F003EBB87 /* ContentView.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 */,
37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */,
377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */, 377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */,
37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */, 37B81B0626D2CEDA00675966 /* PlaybackState.swift in Sources */,
@ -942,9 +971,11 @@
3797758C2689345500DD52A8 /* Store.swift in Sources */, 3797758C2689345500DD52A8 /* Store.swift in Sources */,
37141674267A8E10006CA35D /* Country.swift in Sources */, 37141674267A8E10006CA35D /* Country.swift in Sources */,
37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */, 37AAF2A126741C97007FC770 /* SubscriptionsView.swift in Sources */,
37BA794C26DC30EC002A0235 /* AppSidebarPlaylists.swift in Sources */,
37D4B19826717E1500C925CA /* Video.swift in Sources */, 37D4B19826717E1500C925CA /* Video.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 */,
3711404026B206A6005B3555 /* SearchState.swift in Sources */, 3711404026B206A6005B3555 /* SearchState.swift in Sources */,
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
@ -953,6 +984,7 @@
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -987,11 +1019,11 @@
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */, 37D4B1802671650A00C925CA /* PearvidiousApp.swift in Sources */,
3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
37BD07CA2698FBE5003EBB87 /* AppSidebarNavigation.swift in Sources */,
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */, 373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
37B17DA6268A285E006AEE9B /* VideoDetailsView.swift in Sources */, 37B17DA6268A285E006AEE9B /* VideoDetailsView.swift in Sources */,
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.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 */,
37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */, 37B81B0726D2D6CF00675966 /* PlaybackState.swift in Sources */,
@ -1012,10 +1044,12 @@
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */, 377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */, 3748186826A7627F0084E870 /* Video+Fixtures.swift in Sources */,
371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */, 371F2F1C269B43D300E4A7AB /* NavigationState.swift in Sources */,
37BA794526DBA973002A0235 /* Playlists.swift in Sources */,
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */, 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
3711404126B206A6005B3555 /* SearchState.swift in Sources */, 3711404126B206A6005B3555 /* SearchState.swift in Sources */,
379775952689365600DD52A8 /* Array+Next.swift in Sources */, 379775952689365600DD52A8 /* Array+Next.swift in Sources */,
37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */, 37AAF28A2673AB89007FC770 /* ChannelView.swift in Sources */,

View File

@ -3,17 +3,17 @@ import SwiftUI
import Introspect import Introspect
#endif #endif
typealias TabSelection = AppSidebarNavigation.TabSelection
struct AppSidebarNavigation: View { struct AppSidebarNavigation: View {
enum TabSelection: String {
case subscriptions, popular, trending, playlists, channel, search
}
@EnvironmentObject<NavigationState> private var navigationState @EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Playlists> private var playlists
@EnvironmentObject<Subscriptions> private var subscriptions
@State private var didApplyPrimaryViewWorkAround = false @State private var didApplyPrimaryViewWorkAround = false
var selection: Binding<TabSelection?> {
navigationState.tabSelectionOptionalBinding
}
var body: some View { var body: some View {
#if os(iOS) #if os(iOS)
content.introspectViewController { viewController in content.introspectViewController { viewController in
@ -38,53 +38,18 @@ struct AppSidebarNavigation: View {
NavigationView { NavigationView {
sidebar sidebar
.frame(minWidth: 180) .frame(minWidth: 180)
Text("Select section") Text("Select section")
} }
} }
var sidebar: some View { var sidebar: some View {
List { List {
NavigationLink(tag: TabSelection.subscriptions, selection: navigationState.tabSelectionOptionalBinding) { mainNavigationLinks
SubscriptionsView()
}
label: {
Label("Subscriptions", systemImage: "star.circle.fill")
.accessibility(label: Text("Subscriptions"))
}
NavigationLink(tag: TabSelection.popular, selection: navigationState.tabSelectionOptionalBinding) { AppSidebarSubscriptions(selection: selection)
PopularView() AppSidebarPlaylists(selection: selection)
}
label: {
Label("Popular", systemImage: "chart.bar")
.accessibility(label: Text("Popular"))
}
NavigationLink(tag: TabSelection.trending, selection: navigationState.tabSelectionOptionalBinding) {
TrendingView()
}
label: {
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
.accessibility(label: Text("Trending"))
}
NavigationLink(tag: TabSelection.playlists, selection: navigationState.tabSelectionOptionalBinding) {
PlaylistsView()
}
label: {
Label("Playlists", systemImage: "list.and.film")
.accessibility(label: Text("Playlists"))
}
NavigationLink(tag: TabSelection.search, selection: navigationState.tabSelectionOptionalBinding) {
SearchView()
}
label: {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
} }
#if os(macOS) #if os(macOS)
.toolbar { .toolbar {
Button(action: toggleSidebar) { Button(action: toggleSidebar) {
@ -94,6 +59,59 @@ struct AppSidebarNavigation: View {
#endif #endif
} }
var mainNavigationLinks: some View {
Group {
NavigationLink(tag: TabSelection.subscriptions, selection: selection) {
SubscriptionsView()
}
label: {
Label("Subscriptions", systemImage: "star.circle.fill")
.accessibility(label: Text("Subscriptions"))
}
NavigationLink(tag: TabSelection.popular, selection: selection) {
PopularView()
}
label: {
Label("Popular", systemImage: "chart.bar")
.accessibility(label: Text("Popular"))
}
NavigationLink(tag: TabSelection.trending, selection: selection) {
TrendingView()
}
label: {
Label("Trending", systemImage: "chart.line.uptrend.xyaxis")
.accessibility(label: Text("Trending"))
}
NavigationLink(tag: TabSelection.playlists, selection: selection) {
PlaylistsView()
}
label: {
Label("Playlists", systemImage: "list.and.film")
.accessibility(label: Text("Playlists"))
}
NavigationLink(tag: TabSelection.search, selection: selection) {
SearchView()
}
label: {
Label("Search", systemImage: "magnifyingglass")
.accessibility(label: Text("Search"))
}
}
}
static func symbolSystemImage(_ name: String) -> String {
let firstLetter = name.first?.lowercased()
let regex = #"^[a-z0-9]$"#
let symbolName = firstLetter?.range(of: regex, options: .regularExpression) != nil ? firstLetter! : "questionmark"
return "\(symbolName).square"
}
#if os(macOS) #if os(macOS)
private func toggleSidebar() { private func toggleSidebar() {
NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)

View File

@ -0,0 +1,37 @@
import SwiftUI
struct AppSidebarPlaylists: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Playlists> private var playlists
@Binding var selection: TabSelection?
var body: some View {
Section(header: Text("Playlists")) {
ForEach(playlists.all) { playlist in
NavigationLink(tag: TabSelection.playlist(playlist.id), selection: $selection) {
PlaylistVideosView(playlist)
} label: {
Label(playlist.title, systemImage: AppSidebarNavigation.symbolSystemImage(playlist.title))
.badge(Text("\(playlist.videos.count)"))
}
.contextMenu {
Button("Edit") {
navigationState.presentEditPlaylistForm(playlists.find(id: playlist.id))
}
}
}
newPlaylistButton
.padding(.top, 8)
}
}
var newPlaylistButton: some View {
Button(action: { navigationState.presentNewPlaylistForm() }) {
Label("New Playlist", systemImage: "plus.square")
}
.foregroundColor(.secondary)
.buttonStyle(.plain)
}
}

View File

@ -0,0 +1,40 @@
import SwiftUI
struct AppSidebarSubscriptions: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Subscriptions> private var subscriptions
@Binding var selection: TabSelection?
var body: some View {
Section(header: Text("Subscriptions")) {
ForEach(subscriptions.all) { channel in
NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) {
ChannelVideosView(channel)
} label: {
Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name))
}
.contextMenu {
Button("Unsubscribe") {
navigationState.presentUnsubscribeAlert(channel)
}
}
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
if let channel = navigationState.channelToUnsubscribe {
Button("Unsubscribe", role: .destructive) {
subscriptions.unsubscribe(channel.id)
}
}
}
}
}
}
var unsubscribeAlertTitle: String {
if let channel = navigationState.channelToUnsubscribe {
return "Unsubscribe from \(channel.name)"
}
return "Unknown channel"
}
}

View File

@ -5,6 +5,7 @@ struct ContentView: View {
@StateObject private var playbackState = PlaybackState() @StateObject private var playbackState = PlaybackState()
@StateObject private var searchState = SearchState() @StateObject private var searchState = SearchState()
@StateObject private var subscriptions = Subscriptions() @StateObject private var subscriptions = Subscriptions()
@StateObject private var playlists = Playlists()
#if os(iOS) #if os(iOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.horizontalSizeClass) private var horizontalSizeClass
@ -37,11 +38,15 @@ struct ContentView: View {
#endif #endif
} }
} }
.sheet(isPresented: $navigationState.presentingPlaylistForm) {
PlaylistFormView(playlist: $navigationState.editedPlaylist)
}
#endif #endif
.environmentObject(navigationState) .environmentObject(navigationState)
.environmentObject(playbackState) .environmentObject(playbackState)
.environmentObject(searchState) .environmentObject(searchState)
.environmentObject(subscriptions) .environmentObject(subscriptions)
.environmentObject(playlists)
} }
} }

View File

@ -14,6 +14,8 @@ struct PlaylistFormView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@EnvironmentObject<Playlists> private var playlists
var editing: Bool { var editing: Bool {
playlist != nil playlist != nil
} }
@ -139,6 +141,8 @@ struct PlaylistFormView: View {
playlist = modifiedPlaylist playlist = modifiedPlaylist
} }
playlists.reload()
dismiss() dismiss()
} }
} }

View File

@ -0,0 +1,27 @@
import Siesta
import SwiftUI
struct ChannelVideosView: View {
@ObservedObject private var store = Store<[Video]>()
let channel: Channel
var resource: Resource {
InvidiousAPI.shared.channelVideos(channel.id)
}
init(_ channel: Channel) {
self.channel = channel
resource.addObserver(store)
}
var body: some View {
VideosView(videos: store.collection)
#if !os(tvOS)
.navigationTitle("\(channel.name) Channel")
#endif
.onAppear {
resource.loadIfNeeded()
}
}
}

View File

@ -0,0 +1,17 @@
import Siesta
import SwiftUI
struct PlaylistVideosView: View {
let playlist: Playlist
init(_ playlist: Playlist) {
self.playlist = playlist
}
var body: some View {
VideosView(videos: playlist.videos)
#if !os(tvOS)
.navigationTitle("\(playlist.title) Playlist")
#endif
}
}