mirror of
				https://github.com/yattee/yattee.git
				synced 2025-10-31 12:41:57 +00:00 
			
		
		
		
	Channel playlists support
This commit is contained in:
		| @@ -17,6 +17,12 @@ struct AppSidebarRecents: View { | ||||
|                                 RecentNavigationLink(recent: recent) { | ||||
|                                     LazyView(ChannelVideosView(channel: recent.channel!)) | ||||
|                                 } | ||||
|  | ||||
|                             case .playlist: | ||||
|                                 RecentNavigationLink(recent: recent, systemImage: "list.and.film") { | ||||
|                                     LazyView(ChannelPlaylistView(playlist: recent.playlist!)) | ||||
|                                 } | ||||
|  | ||||
|                             case .query: | ||||
|                                 RecentNavigationLink(recent: recent, systemImage: "magnifyingglass") { | ||||
|                                     LazyView(SearchView(recent.query!)) | ||||
| @@ -64,6 +70,7 @@ struct RecentNavigationLink<DestinationContent: View>: View { | ||||
|         } label: { | ||||
|             HStack { | ||||
|                 Label(recent.title, systemImage: labelSystemImage) | ||||
|                     .lineLimit(1) | ||||
|  | ||||
|                 Spacer() | ||||
|  | ||||
|   | ||||
| @@ -2,9 +2,11 @@ import Defaults | ||||
| import SwiftUI | ||||
|  | ||||
| struct AppTabNavigation: View { | ||||
|     @EnvironmentObject<AccountsModel> private var accounts | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|     @EnvironmentObject<SearchModel> private var search | ||||
|     @EnvironmentObject<PlayerModel> private var player | ||||
|     @EnvironmentObject<RecentsModel> private var recents | ||||
|     @EnvironmentObject<SearchModel> private var search | ||||
|  | ||||
|     var body: some View { | ||||
|         TabView(selection: navigation.tabSelectionBinding) { | ||||
| @@ -18,27 +20,30 @@ struct AppTabNavigation: View { | ||||
|             } | ||||
|             .tag(TabSelection.watchNow) | ||||
|  | ||||
|             NavigationView { | ||||
|                 LazyView(SubscriptionsView()) | ||||
|                     .toolbar { toolbarContent } | ||||
|             if accounts.app.supportsSubscriptions { | ||||
|                 NavigationView { | ||||
|                     LazyView(SubscriptionsView()) | ||||
|                         .toolbar { toolbarContent } | ||||
|                 } | ||||
|                 .tabItem { | ||||
|                     Label("Subscriptions", systemImage: "star.circle.fill") | ||||
|                         .accessibility(label: Text("Subscriptions")) | ||||
|                 } | ||||
|                 .tag(TabSelection.subscriptions) | ||||
|             } | ||||
|             .tabItem { | ||||
|                 Label("Subscriptions", systemImage: "star.circle.fill") | ||||
|                     .accessibility(label: Text("Subscriptions")) | ||||
|             } | ||||
|             .tag(TabSelection.subscriptions) | ||||
|  | ||||
| //            TODO: reenable with settings | ||||
| //            ============================ | ||||
| //            NavigationView { | ||||
| //                LazyView(PopularView()) | ||||
| //                    .toolbar { toolbarContent } | ||||
| //            } | ||||
| //            .tabItem { | ||||
| //                Label("Popular", systemImage: "chart.bar") | ||||
| //                    .accessibility(label: Text("Popular")) | ||||
| //            } | ||||
| //            .tag(TabSelection.popular) | ||||
|             // TODO: reenable with settings | ||||
|             if accounts.app.supportsPopular && false { | ||||
|                 NavigationView { | ||||
|                     LazyView(PopularView()) | ||||
|                         .toolbar { toolbarContent } | ||||
|                 } | ||||
|                 .tabItem { | ||||
|                     Label("Popular", systemImage: "chart.bar") | ||||
|                         .accessibility(label: Text("Popular")) | ||||
|                 } | ||||
|                 .tag(TabSelection.popular) | ||||
|             } | ||||
|  | ||||
|             NavigationView { | ||||
|                 LazyView(TrendingView()) | ||||
| @@ -50,15 +55,17 @@ struct AppTabNavigation: View { | ||||
|             } | ||||
|             .tag(TabSelection.trending) | ||||
|  | ||||
|             NavigationView { | ||||
|                 LazyView(PlaylistsView()) | ||||
|                     .toolbar { toolbarContent } | ||||
|             if accounts.app.supportsUserPlaylists { | ||||
|                 NavigationView { | ||||
|                     LazyView(PlaylistsView()) | ||||
|                         .toolbar { toolbarContent } | ||||
|                 } | ||||
|                 .tabItem { | ||||
|                     Label("Playlists", systemImage: "list.and.film") | ||||
|                         .accessibility(label: Text("Playlists")) | ||||
|                 } | ||||
|                 .tag(TabSelection.playlists) | ||||
|             } | ||||
|             .tabItem { | ||||
|                 Label("Playlists", systemImage: "list.and.film") | ||||
|                     .accessibility(label: Text("Playlists")) | ||||
|             } | ||||
|             .tag(TabSelection.playlists) | ||||
|  | ||||
|             NavigationView { | ||||
|                 LazyView( | ||||
| @@ -89,19 +96,41 @@ struct AppTabNavigation: View { | ||||
|             .tag(TabSelection.search) | ||||
|         } | ||||
|         .environment(\.navigationStyle, .tab) | ||||
|         .sheet(isPresented: $navigation.isChannelOpen, onDismiss: { | ||||
|         .sheet(isPresented: $navigation.presentingChannel, onDismiss: { | ||||
|             if let channel = recents.presentedChannel { | ||||
|                 let recent = RecentItem(from: channel) | ||||
|                 recents.close(recent) | ||||
|                 recents.close(RecentItem(from: channel)) | ||||
|             } | ||||
|         }) { | ||||
|             if recents.presentedChannel != nil { | ||||
|             if let channel = recents.presentedChannel { | ||||
|                 NavigationView { | ||||
|                     ChannelVideosView(channel: recents.presentedChannel!) | ||||
|                     ChannelVideosView(channel: channel) | ||||
|                         .environment(\.inNavigationView, true) | ||||
|                         .background(playerNavigationLink) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .sheet(isPresented: $navigation.presentingPlaylist, onDismiss: { | ||||
|             if let playlist = recents.presentedPlaylist { | ||||
|                 recents.close(RecentItem(from: playlist)) | ||||
|             } | ||||
|         }) { | ||||
|             if let playlist = recents.presentedPlaylist { | ||||
|                 NavigationView { | ||||
|                     ChannelPlaylistView(playlist: playlist) | ||||
|                         .environment(\.inNavigationView, true) | ||||
|                         .background(playerNavigationLink) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private var playerNavigationLink: some View { | ||||
|         NavigationLink(isActive: $player.playerNavigationLinkActive, destination: { | ||||
|             VideoPlayerView() | ||||
|                 .environment(\.inNavigationView, true) | ||||
|         }) { | ||||
|             EmptyView() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var toolbarContent: some ToolbarContent { | ||||
|   | ||||
| @@ -4,8 +4,6 @@ import SwiftUI | ||||
|  | ||||
| struct VideoCell: View { | ||||
|     var video: Video | ||||
|  | ||||
|     @State private var playerNavigationLinkActive = false | ||||
|     @State private var lowQualityThumbnail = false | ||||
|  | ||||
|     @Environment(\.inNavigationView) private var inNavigationView | ||||
| @@ -23,24 +21,17 @@ struct VideoCell: View { | ||||
|                 player.playNow(video) | ||||
|  | ||||
|                 if inNavigationView { | ||||
|                     playerNavigationLinkActive = true | ||||
|                     player.playerNavigationLinkActive = true | ||||
|                 } else { | ||||
|                     player.presentPlayer() | ||||
|                 } | ||||
|             }) { | ||||
|                 content | ||||
|             } | ||||
|  | ||||
|             NavigationLink(isActive: $playerNavigationLinkActive, destination: { | ||||
|                 VideoPlayerView() | ||||
|                     .environment(\.inNavigationView, true) | ||||
|             }) { | ||||
|                 EmptyView() | ||||
|             } | ||||
|         } | ||||
|         .buttonStyle(.plain) | ||||
|         .contentShape(RoundedRectangle(cornerRadius: 12)) | ||||
|         .contextMenu { VideoContextMenuView(video: video, playerNavigationLinkActive: $playerNavigationLinkActive) } | ||||
|         .contextMenu { VideoContextMenuView(video: video, playerNavigationLinkActive: $player.playerNavigationLinkActive) } | ||||
|     } | ||||
|  | ||||
|     var content: some View { | ||||
| @@ -90,7 +81,7 @@ struct VideoCell: View { | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             if video.views != 0 { | ||||
|                             if video.views > 0 { | ||||
|                                 VStack { | ||||
|                                     Image(systemName: "eye") | ||||
|                                     Text(video.viewsCount!) | ||||
| @@ -125,6 +116,7 @@ struct VideoCell: View { | ||||
|  | ||||
|                             Spacer() | ||||
|                         } | ||||
|                         .lineLimit(1) | ||||
|                     } | ||||
|                 #endif | ||||
|             } | ||||
| @@ -154,7 +146,7 @@ struct VideoCell: View { | ||||
|                                 Text(date) | ||||
|                             } | ||||
|  | ||||
|                             if video.views != 0 { | ||||
|                             if video.views > 0 { | ||||
|                                 Image(systemName: "eye") | ||||
|                                 Text(video.viewsCount!) | ||||
|                             } | ||||
| @@ -210,6 +202,7 @@ struct VideoCell: View { | ||||
|                 } | ||||
|                 .padding(10) | ||||
|             } | ||||
|             .lineLimit(1) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -253,7 +246,7 @@ struct VideoCell: View { | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct VideoView_Preview: PreviewProvider { | ||||
| struct VideoCell_Preview: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         Group { | ||||
|             VideoCell(video: Video.fixture) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ struct ChannelCell: View { | ||||
|         Button { | ||||
|             let recent = RecentItem(from: channel) | ||||
|             recents.add(recent) | ||||
|             navigation.isChannelOpen = true | ||||
|             navigation.presentingChannel = true | ||||
|  | ||||
|             if navigationStyle == .sidebar { | ||||
|                 navigation.sidebarSectionChanged.toggle() | ||||
| @@ -30,10 +30,13 @@ struct ChannelCell: View { | ||||
|  | ||||
|     var content: some View { | ||||
|         VStack { | ||||
|             Text("Channel".uppercased()) | ||||
|                 .foregroundColor(.secondary) | ||||
|                 .fontWeight(.light) | ||||
|                 .opacity(0.6) | ||||
|             HStack(alignment: .top, spacing: 3) { | ||||
|                 Image(systemName: "person.crop.rectangle") | ||||
|                 Text("Channel".uppercased()) | ||||
|                     .fontWeight(.light) | ||||
|                     .opacity(0.6) | ||||
|             } | ||||
|             .foregroundColor(.secondary) | ||||
|  | ||||
|             WebImage(url: channel.thumbnailURL) | ||||
|                 .resizable() | ||||
| @@ -44,20 +47,17 @@ struct ChannelCell: View { | ||||
|                 .frame(width: 88, height: 88) | ||||
|                 .clipShape(Circle()) | ||||
|  | ||||
|             Group { | ||||
|                 DetailBadge(text: channel.name, style: .prominent) | ||||
|             DetailBadge(text: channel.name, style: .prominent) | ||||
|  | ||||
|                 Group { | ||||
|                     if let subscriptions = channel.subscriptionsString { | ||||
|                         Text("\(subscriptions) subscribers") | ||||
|                             .foregroundColor(.secondary) | ||||
|                     } else { | ||||
|                         Text("") | ||||
|                     } | ||||
|             Group { | ||||
|                 if let subscriptions = channel.subscriptionsString { | ||||
|                     Text("\(subscriptions) subscribers") | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } else { | ||||
|                     Text("") | ||||
|                 } | ||||
|                 .frame(height: 20) | ||||
|             } | ||||
|             .offset(x: 0, y: -15) | ||||
|             .frame(height: 20) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										68
									
								
								Shared/Views/ChannelPlaylistCell.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								Shared/Views/ChannelPlaylistCell.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import SDWebImageSwiftUI | ||||
| import SwiftUI | ||||
|  | ||||
| struct ChannelPlaylistCell: View { | ||||
|     let playlist: ChannelPlaylist | ||||
|  | ||||
|     @Environment(\.navigationStyle) private var navigationStyle | ||||
|  | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|     @EnvironmentObject<RecentsModel> private var recents | ||||
|  | ||||
|     var body: some View { | ||||
|         Button { | ||||
|             let recent = RecentItem(from: playlist) | ||||
|             recents.add(recent) | ||||
|             navigation.presentingPlaylist = true | ||||
|  | ||||
|             if navigationStyle == .sidebar { | ||||
|                 navigation.sidebarSectionChanged.toggle() | ||||
|                 navigation.tabSelection = .recentlyOpened(recent.tag) | ||||
|             } | ||||
|         } label: { | ||||
|             content | ||||
|                 .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) | ||||
|                 .contentShape(RoundedRectangle(cornerRadius: 12)) | ||||
|         } | ||||
|         .buttonStyle(.plain) | ||||
|     } | ||||
|  | ||||
|     var content: some View { | ||||
|         VStack { | ||||
|             HStack(alignment: .top, spacing: 3) { | ||||
|                 Image(systemName: "list.and.film") | ||||
|                 Text("Playlist".uppercased()) | ||||
|                     .fontWeight(.light) | ||||
|                     .opacity(0.6) | ||||
|             } | ||||
|             .foregroundColor(.secondary) | ||||
|  | ||||
|             WebImage(url: playlist.thumbnailURL) | ||||
|                 .resizable() | ||||
|                 .placeholder { | ||||
|                     Rectangle().fill(Color("PlaceholderColor")) | ||||
|                 } | ||||
|                 .indicator(.progress) | ||||
|                 .frame(width: 165, height: 88) | ||||
|                 .clipShape(RoundedRectangle(cornerRadius: 10)) | ||||
|  | ||||
|             Group { | ||||
|                 DetailBadge(text: playlist.title, style: .prominent) | ||||
|                     .lineLimit(2) | ||||
|  | ||||
|                 Text("\(playlist.videosCount ?? playlist.videos.count) videos") | ||||
|                     .foregroundColor(.secondary) | ||||
|  | ||||
|                     .frame(height: 20) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChannelPlaylistCell_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         ChannelPlaylistCell(playlist: ChannelPlaylist.fixture) | ||||
|             .frame(maxWidth: 320) | ||||
|             .injectFixtureEnvironmentObjects() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								Shared/Views/ChannelPlaylistView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								Shared/Views/ChannelPlaylistView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import Siesta | ||||
| import SwiftUI | ||||
|  | ||||
| struct ChannelPlaylistView: View { | ||||
|     var playlist: ChannelPlaylist | ||||
|  | ||||
|     @StateObject private var store = Store<ChannelPlaylist>() | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|     @Environment(\.inNavigationView) private var inNavigationView | ||||
|  | ||||
|     @EnvironmentObject<AccountsModel> private var accounts | ||||
|  | ||||
|     var items: [ContentItem] { | ||||
|         ContentItem.array(of: store.item?.videos ?? []) | ||||
|     } | ||||
|  | ||||
|     var resource: Resource? { | ||||
|         accounts.api.channelPlaylist(playlist.id) | ||||
|     } | ||||
|  | ||||
|     var body: some View { | ||||
|         #if os(iOS) | ||||
|             if inNavigationView { | ||||
|                 content | ||||
|             } else { | ||||
|                 PlayerControlsView { | ||||
|                     content | ||||
|                 } | ||||
|             } | ||||
|         #else | ||||
|             PlayerControlsView { | ||||
|                 content | ||||
|             } | ||||
|         #endif | ||||
|     } | ||||
|  | ||||
|     var content: some View { | ||||
|         VStack(alignment: .leading) { | ||||
|             #if os(tvOS) | ||||
|                 Text(playlist.title) | ||||
|                     .font(.title2) | ||||
|                     .frame(alignment: .leading) | ||||
|             #endif | ||||
|             VerticalCells(items: items) | ||||
|         } | ||||
|         .onAppear { | ||||
|             resource?.addObserver(store) | ||||
|             resource?.loadIfNeeded() | ||||
|         } | ||||
|         #if !os(tvOS) | ||||
|             .toolbar { | ||||
|                 ToolbarItem(placement: .cancellationAction) { | ||||
|                     if inNavigationView { | ||||
|                         Button("Done") { | ||||
|                             dismiss() | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .navigationTitle(playlist.title) | ||||
|  | ||||
|         #else | ||||
|             .background(.thickMaterial) | ||||
|         #endif | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ChannelPlaylistView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         ChannelPlaylistView(playlist: ChannelPlaylist.fixture) | ||||
|             .injectFixtureEnvironmentObjects() | ||||
|     } | ||||
| } | ||||
| @@ -6,17 +6,17 @@ struct ChannelVideosView: View { | ||||
|  | ||||
|     @StateObject private var store = Store<Channel>() | ||||
|  | ||||
|     @EnvironmentObject<AccountsModel> private var accounts | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|     @EnvironmentObject<SubscriptionsModel> private var subscriptions | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|     @Environment(\.inNavigationView) private var inNavigationView | ||||
|  | ||||
|     @Environment(\.dismiss) private var dismiss | ||||
|     #if os(iOS) | ||||
|         @Environment(\.horizontalSizeClass) private var horizontalSizeClass | ||||
|     #endif | ||||
|  | ||||
|     @EnvironmentObject<AccountsModel> private var accounts | ||||
|     @EnvironmentObject<NavigationModel> private var navigation | ||||
|     @EnvironmentObject<SubscriptionsModel> private var subscriptions | ||||
|  | ||||
|     @Namespace private var focusNamespace | ||||
|  | ||||
|     var videos: [ContentItem] { | ||||
| @@ -88,8 +88,7 @@ struct ChannelVideosView: View { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         #endif | ||||
|         #if os(tvOS) | ||||
|         #else | ||||
|             .background(.thickMaterial) | ||||
|         #endif | ||||
|         .modifier(UnsubscribeAlertModifier()) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ struct ContentItemView: View { | ||||
|         Group { | ||||
|             switch item.contentType { | ||||
|             case .playlist: | ||||
|                 VideoCell(video: item.video) | ||||
|                 ChannelPlaylistCell(playlist: item.playlist) | ||||
|             case .channel: | ||||
|                 ChannelCell(channel: item.channel) | ||||
|             default: | ||||
|   | ||||
| @@ -73,7 +73,6 @@ struct DetailBadge: View { | ||||
|  | ||||
|     var body: some View { | ||||
|         Text(text) | ||||
|             .lineLimit(1) | ||||
|             .truncationMode(.middle) | ||||
|             .padding(10) | ||||
|             .modifier(StyleModifier(style: style)) | ||||
|   | ||||
| @@ -25,6 +25,10 @@ struct SearchView: View { | ||||
|  | ||||
|     private var videos = [Video]() | ||||
|  | ||||
|     var items: [ContentItem] { | ||||
|         state.store.collection.sorted { $0 < $1 } | ||||
|     } | ||||
|  | ||||
|     init(_ query: SearchQuery? = nil, videos: [Video] = [Video]()) { | ||||
|         self.query = query | ||||
|         self.videos = videos | ||||
| @@ -42,11 +46,11 @@ struct SearchView: View { | ||||
|                                 filtersHorizontalStack | ||||
|                             } | ||||
|  | ||||
|                             HorizontalCells(items: state.store.collection) | ||||
|                             HorizontalCells(items: items) | ||||
|                         } | ||||
|                         .edgesIgnoringSafeArea(.horizontal) | ||||
|                     #else | ||||
|                         VerticalCells(items: state.store.collection) | ||||
|                         VerticalCells(items: items) | ||||
|                     #endif | ||||
|  | ||||
|                     if noResults { | ||||
| @@ -173,7 +177,7 @@ struct SearchView: View { | ||||
|     } | ||||
|  | ||||
|     fileprivate var noResults: Bool { | ||||
|         state.store.collection.isEmpty && !state.isLoading && !state.query.isEmpty | ||||
|         items.isEmpty && !state.isLoading && !state.query.isEmpty | ||||
|     } | ||||
|  | ||||
|     var recentQueries: some View { | ||||
|   | ||||
| @@ -85,7 +85,7 @@ struct VideoContextMenuView: View { | ||||
|         Button { | ||||
|             let recent = RecentItem(from: video.channel) | ||||
|             recents.add(recent) | ||||
|             navigation.isChannelOpen = true | ||||
|             navigation.presentingChannel = true | ||||
|  | ||||
|             if navigationStyle == .sidebar { | ||||
|                 navigation.sidebarSectionChanged.toggle() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Arkadiusz Fal
					Arkadiusz Fal