From ca4378afc1d0507240abf70910099a7056d7630e Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 11 Jul 2021 22:52:49 +0200 Subject: [PATCH] Multiplatform UI support fixes --- .swift-version | 2 + Apple TV/AppTabNavigation.swift | 57 +++++ Apple TV/ChannelView.swift | 18 +- Apple TV/OptionsView.swift | 7 +- Apple TV/PlayerView.swift | 60 ++++- Apple TV/PlayerViewController.swift | 14 +- Apple TV/PlaylistsView.swift | 59 +++-- Apple TV/PopularVideosView.swift | 9 +- Apple TV/SearchView.swift | 5 +- Apple TV/SubscriptionsView.swift | 6 + Apple TV/TVNavigationView.swift | 53 ++++ Apple TV/TrendingView.swift | 36 +-- Apple TV/VideoCellView.swift | 26 +- Apple TV/VideoContextMenuView.swift | 17 +- Apple TV/VideoDetailsView.swift | 99 ++++---- Apple TV/VideoListRowView.swift | 234 ++++++++++++------ Apple TV/VideosCellsView.swift | 6 +- Apple TV/VideosListView.swift | 10 +- Apple TV/VideosView.swift | 29 ++- Model/NavigationState.swift | 43 ++++ Model/PlayerState.swift | 24 +- Model/SearchQuery.swift | 4 + Pearvidious.xcodeproj/project.pbxproj | 189 ++++++++++---- .../xcshareddata/swiftpm/Package.resolved | 9 + .../xcschemes/Pearvidious (iOS).xcscheme | 88 +++++++ .../xcschemes/xcschememanagement.plist | 27 +- Shared/AppSidebarNavigation.swift | 58 +++++ Shared/ContentView.swift | 62 ++--- Shared/Defaults.swift | 4 - Shared/Pearvidious.entitlements | 10 + 30 files changed, 947 insertions(+), 318 deletions(-) create mode 100644 .swift-version create mode 100644 Apple TV/AppTabNavigation.swift create mode 100644 Apple TV/TVNavigationView.swift create mode 100644 Model/NavigationState.swift create mode 100644 Pearvidious.xcodeproj/xcshareddata/xcschemes/Pearvidious (iOS).xcscheme create mode 100644 Shared/AppSidebarNavigation.swift create mode 100644 Shared/Pearvidious.entitlements diff --git a/.swift-version b/.swift-version new file mode 100644 index 00000000..51d7b8d1 --- /dev/null +++ b/.swift-version @@ -0,0 +1,2 @@ +5 + diff --git a/Apple TV/AppTabNavigation.swift b/Apple TV/AppTabNavigation.swift new file mode 100644 index 00000000..5d6570ca --- /dev/null +++ b/Apple TV/AppTabNavigation.swift @@ -0,0 +1,57 @@ +import Defaults +import SwiftUI + +struct AppTabNavigation: View { + @State private var showingOptions = false + + @State private var tabSelection: TabSelection? = .subscriptions + + var body: some View { + TabView(selection: $tabSelection) { + NavigationView { + SubscriptionsView() + } + .tabItem { + Label("Subscriptions", systemImage: "play.rectangle.fill") + .accessibility(label: Text("Subscriptions")) + } + .tag(TabSelection.subscriptions) + + NavigationView { + PopularVideosView() + } + .tabItem { + Label("Popular", systemImage: "chart.bar") + .accessibility(label: Text("Popular")) + } + .tag(TabSelection.popular) + + NavigationView { + TrendingView() + } + .tabItem { + Label("Trending", systemImage: "chart.line.uptrend.xyaxis") + .accessibility(label: Text("Trending")) + } + .tag(TabSelection.trending) + + NavigationView { + PlaylistsView() + } + .tabItem { + Label("Playlists", systemImage: "list.and.film") + .accessibility(label: Text("Playlists")) + } + .tag(TabSelection.playlists) + + NavigationView { + SearchView() + } + .tabItem { + Label("Search", systemImage: "magnifyingglass") + .accessibility(label: Text("Search")) + } + .tag(TabSelection.search) + } + } +} diff --git a/Apple TV/ChannelView.swift b/Apple TV/ChannelView.swift index 656f6cac..b6c59991 100644 --- a/Apple TV/ChannelView.swift +++ b/Apple TV/ChannelView.swift @@ -16,9 +16,21 @@ struct ChannelView: View { } var body: some View { - VideosView(videos: store.collection) - .onAppear { - resource.loadIfNeeded() + HStack { + Spacer() + + VStack { + Spacer() + VideosView(videos: store.collection) + .onAppear { + resource.loadIfNeeded() + } + Spacer() } + + Spacer() + } + .edgesIgnoringSafeArea(.all) + .background(.ultraThickMaterial) } } diff --git a/Apple TV/OptionsView.swift b/Apple TV/OptionsView.swift index 97212486..6d526c1a 100644 --- a/Apple TV/OptionsView.swift +++ b/Apple TV/OptionsView.swift @@ -2,10 +2,11 @@ import Defaults import SwiftUI struct OptionsView: View { - @Environment(\.dismiss) private var dismiss + @EnvironmentObject private var navigationState @Default(.layout) private var layout - @Default(.tabSelection) private var tabSelection + + @Environment(\.dismiss) private var dismiss var body: some View { HStack { @@ -41,7 +42,7 @@ struct OptionsView: View { var tabSelectionOptions: some View { VStack { - switch tabSelection { + switch navigationState.tabSelection { case .search: SearchOptionsView() diff --git a/Apple TV/PlayerView.swift b/Apple TV/PlayerView.swift index 27a1fe2b..445aa6b6 100644 --- a/Apple TV/PlayerView.swift +++ b/Apple TV/PlayerView.swift @@ -15,19 +15,63 @@ struct PlayerView: View { var body: some View { VStack { - pvc? - .edgesIgnoringSafeArea(.all) + #if os(tvOS) + pvc + .edgesIgnoringSafeArea(.all) + #else + if let video = store.item { + VStack(alignment: .leading) { + Text(video.title) + + .bold() + + Text("\(video.author)") + + .foregroundColor(.secondary) + .bold() + + if !video.published.isEmpty || video.views != 0 { + HStack(spacing: 8) { + #if os(iOS) + Text(video.playTime ?? "?") + .layoutPriority(1) + #endif + + if !video.published.isEmpty { + Image(systemName: "calendar") + Text(video.published) + .lineLimit(1) + .truncationMode(.middle) + } + + if video.views != 0 { + Image(systemName: "eye") + Text(video.viewsCount) + } + } + + .padding(.top) + } + } + #if os(tvOS) + .padding() + #else + #endif + } + #endif } .onAppear { resource.loadIfNeeded() } } - var pvc: PlayerViewController? { - guard store.item != nil else { - return nil - } + #if !os(macOS) + var pvc: PlayerViewController? { + guard store.item != nil else { + return nil + } - return PlayerViewController(video: store.item!) - } + return PlayerViewController(video: store.item!) + } + #endif } diff --git a/Apple TV/PlayerViewController.swift b/Apple TV/PlayerViewController.swift index 2fa374e4..23f7b810 100644 --- a/Apple TV/PlayerViewController.swift +++ b/Apple TV/PlayerViewController.swift @@ -4,6 +4,12 @@ import Logging import SwiftUI struct PlayerViewController: UIViewControllerRepresentable { + #if os(tvOS) + typealias PlayerController = StreamAVPlayerViewController + #else + typealias PlayerController = AVPlayerViewController + #endif + let logger = Logger(label: "net.arekf.Pearvidious.pvc") @ObservedObject private var state: PlayerState @@ -73,11 +79,11 @@ struct PlayerViewController: UIViewControllerRepresentable { loadStream(video.bestStream) } - func makeUIViewController(context _: Context) -> StreamAVPlayerViewController { - let controller = StreamAVPlayerViewController() - controller.state = state + func makeUIViewController(context _: Context) -> PlayerController { + let controller = PlayerController() #if os(tvOS) + controller.state = state controller.transportBarCustomMenuItems = [streamingQualityMenu] #endif controller.modalPresentationStyle = .fullScreen @@ -86,7 +92,7 @@ struct PlayerViewController: UIViewControllerRepresentable { return controller } - func updateUIViewController(_ controller: StreamAVPlayerViewController, context _: Context) { + func updateUIViewController(_ controller: PlayerController, context _: Context) { var items: [UIMenuElement] = [] if state.nextStream != nil { diff --git a/Apple TV/PlaylistsView.swift b/Apple TV/PlaylistsView.swift index ded0ec7e..41ec8a97 100644 --- a/Apple TV/PlaylistsView.swift +++ b/Apple TV/PlaylistsView.swift @@ -24,25 +24,27 @@ struct PlaylistsView: View { var body: some View { Section { VStack(alignment: .center, spacing: 2) { - HStack { - if store.collection.isEmpty { - Text("No Playlists") - .foregroundColor(.secondary) - } else { - Text("Current Playlist") - .foregroundColor(.secondary) + #if os(tvOS) + HStack { + if store.collection.isEmpty { + Text("No Playlists") + .foregroundColor(.secondary) + } else { + Text("Current Playlist") + .foregroundColor(.secondary) - selectPlaylistButton + selectPlaylistButton + } + + if currentPlaylist != nil { + editPlaylistButton + } + + newPlaylistButton + .padding(.leading, 40) } - - if currentPlaylist != nil { - editPlaylistButton - } - - newPlaylistButton - .padding(.leading, 40) - } - .scaleEffect(0.85) + .scaleEffect(0.85) + #endif if currentPlaylist != nil { if currentPlaylist!.videos.isEmpty { @@ -61,17 +63,24 @@ struct PlaylistsView: View { } } } - .fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { - PlaylistFormView(playlist: $createdPlaylist) - } - .fullScreenCover(isPresented: $showingEditPlaylist, onDismiss: selectEditedPlaylist) { - PlaylistFormView(playlist: $editedPlaylist) - } + #if !os(macOS) + .fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { + PlaylistFormView(playlist: $createdPlaylist) + } + .fullScreenCover(isPresented: $showingEditPlaylist, onDismiss: selectEditedPlaylist) { + PlaylistFormView(playlist: $editedPlaylist) + } + #endif .onAppear { resource.loadIfNeeded()?.onSuccess { _ in selectPlaylist(selectedPlaylistID) } } + #if !os(tvOS) + .navigationTitle("Playlists") + #elseif os(iOS) + .navigationBarItems(trailing: newPlaylistButton) + #endif } func selectPlaylist(_ id: String?) { @@ -139,7 +148,9 @@ struct PlaylistsView: View { Button(action: { self.showingNewPlaylist = true }) { HStack(spacing: 8) { Image(systemName: "plus") - Text("New Playlist") + #if os(tvOS) + Text("New Playlist") + #endif } } } diff --git a/Apple TV/PopularVideosView.swift b/Apple TV/PopularVideosView.swift index c4420d21..47daa666 100644 --- a/Apple TV/PopularVideosView.swift +++ b/Apple TV/PopularVideosView.swift @@ -12,8 +12,11 @@ struct PopularVideosView: View { var body: some View { VideosView(videos: store.collection) - .onAppear { - resource.loadIfNeeded() - } + #if !os(tvOS) + .navigationTitle("Popular") + #endif + .onAppear { + resource.loadIfNeeded() + } } } diff --git a/Apple TV/SearchView.swift b/Apple TV/SearchView.swift index cbbc73c3..53b1831c 100644 --- a/Apple TV/SearchView.swift +++ b/Apple TV/SearchView.swift @@ -17,7 +17,7 @@ struct SearchView: View { VideosView(videos: store.collection) } - if store.collection.isEmpty && !resource.isLoading { + if store.collection.isEmpty && !resource.isLoading && !query.isEmpty { Text("No results") if searchFiltersActive { @@ -50,6 +50,9 @@ struct SearchView: View { .onChange(of: searchDuration) { duration in changeQuery { query.duration = duration } } + #if !os(tvOS) + .navigationTitle("Search") + #endif } func changeQuery(_ change: @escaping () -> Void = {}) { diff --git a/Apple TV/SubscriptionsView.swift b/Apple TV/SubscriptionsView.swift index fa6eddaa..4b971f45 100644 --- a/Apple TV/SubscriptionsView.swift +++ b/Apple TV/SubscriptionsView.swift @@ -14,5 +14,11 @@ struct SubscriptionsView: View { .onAppear { resource.loadIfNeeded() } + .refreshable { + resource.load() + } + #if !os(tvOS) + .navigationTitle("Subscriptions") + #endif } } diff --git a/Apple TV/TVNavigationView.swift b/Apple TV/TVNavigationView.swift new file mode 100644 index 00000000..06edd538 --- /dev/null +++ b/Apple TV/TVNavigationView.swift @@ -0,0 +1,53 @@ +import Defaults +import SwiftUI + +struct TVNavigationView: View { + @EnvironmentObject private var navigationState + + @State private var showingOptions = false + + var body: some View { + NavigationView { + TabView(selection: $navigationState.tabSelection) { + SubscriptionsView() + .tabItem { Text("Subscriptions") } + .tag(TabSelection.subscriptions) + + PopularVideosView() + .tabItem { Text("Popular") } + .tag(TabSelection.popular) + + TrendingView() + .tabItem { Text("Trending") } + .tag(TabSelection.trending) + + PlaylistsView() + .tabItem { Text("Playlists") } + .tag(TabSelection.playlists) + + SearchView() + .tabItem { Image(systemName: "magnifyingglass") } + .tag(TabSelection.search) + } + .fullScreenCover(isPresented: $showingOptions) { OptionsView() } + .fullScreenCover(isPresented: $navigationState.showingVideoDetails) { + if let video = navigationState.video { + VideoDetailsView(video) + } + } + .fullScreenCover(isPresented: $navigationState.showingChannel) { + if let channel = navigationState.channel { + ChannelView(id: channel.id) + } + } + + .onPlayPauseCommand { showingOptions.toggle() } + } + } +} + +struct TVNavigationView_Previews: PreviewProvider { + static var previews: some View { + TVNavigationView() + } +} diff --git a/Apple TV/TrendingView.swift b/Apple TV/TrendingView.swift index 5dfc1e95..d18469a0 100644 --- a/Apple TV/TrendingView.swift +++ b/Apple TV/TrendingView.swift @@ -19,23 +19,29 @@ struct TrendingView: View { var body: some View { Section { VStack(alignment: .center, spacing: 2) { - HStack { - Text("Category") - .foregroundColor(.secondary) + #if os(tvOS) + HStack { + Text("Category") + .foregroundColor(.secondary) - categoryButton + categoryButton - Text("Country") - .foregroundColor(.secondary) + Text("Country") + .foregroundColor(.secondary) - countryFlag - countryButton - } - .scaleEffect(0.85) + countryFlag + countryButton + } + .scaleEffect(0.85) + #endif VideosView(videos: store.collection) } - }.onAppear { + } + #if !os(tvOS) + .navigationTitle("Trending") + #endif + .onAppear { resource.loadIfNeeded() } } @@ -61,9 +67,11 @@ struct TrendingView: View { selectingCountry.toggle() resource.removeObservers(ownedBy: store) } - .fullScreenCover(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) { - TrendingCountrySelectionView(selectedCountry: $country) - } + #if os(tvOS) + .fullScreenCover(isPresented: $selectingCountry, onDismiss: { setCountry(country) }) { + TrendingCountrySelectionView(selectedCountry: $country) + } + #endif } fileprivate func setCategory(_ category: TrendingCategory) { diff --git a/Apple TV/VideoCellView.swift b/Apple TV/VideoCellView.swift index 307ab533..fdbcf0e0 100644 --- a/Apple TV/VideoCellView.swift +++ b/Apple TV/VideoCellView.swift @@ -24,20 +24,24 @@ struct VideoCellView: View { .frame(width: 550, height: 310) } - Text(video.author) - .padding(8) - .background(.thickMaterial) - .mask(RoundedRectangle(cornerRadius: 12)) - .offset(x: -10, y: -120) - .truncationMode(.middle) - - if let time = video.playTime { - Text(time) - .fontWeight(.bold) + VStack(alignment: .trailing) { + Text(video.author) .padding(8) .background(.thickMaterial) .mask(RoundedRectangle(cornerRadius: 12)) - .offset(x: -10, y: 115) + .offset(x: -5, y: 5) + .truncationMode(.middle) + + Spacer() + + if let time = video.playTime { + Text(time) + .fontWeight(.bold) + .padding(8) + .background(.thickMaterial) + .mask(RoundedRectangle(cornerRadius: 12)) + .offset(x: -5, y: -5) + } } } .frame(width: 550, height: 310) diff --git a/Apple TV/VideoContextMenuView.swift b/Apple TV/VideoContextMenuView.swift index 2f962775..d583f92d 100644 --- a/Apple TV/VideoContextMenuView.swift +++ b/Apple TV/VideoContextMenuView.swift @@ -2,18 +2,15 @@ import Defaults import SwiftUI struct VideoContextMenuView: View { - @Default(.tabSelection) var tabSelection + @EnvironmentObject private var navigationState let video: Video - @Default(.openVideoID) var openVideoID - @Default(.showingVideoDetails) var showDetails - @Default(.showingAddToPlaylist) var showingAddToPlaylist @Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist var body: some View { - if tabSelection == .channel { + if navigationState.tabSelection == .channel { closeChannelButton(from: video) } else { openChannelButton(from: video) @@ -21,7 +18,7 @@ struct VideoContextMenuView: View { openVideoDetailsButton - if tabSelection == .playlists { + if navigationState.tabSelection == .playlists { removeFromPlaylistButton } else { addToPlaylistButton @@ -30,21 +27,19 @@ struct VideoContextMenuView: View { func openChannelButton(from video: Video) -> some View { Button("\(video.author) Channel") { - Defaults[.openChannel] = Channel.from(video: video) - tabSelection = .channel + navigationState.openChannel(Channel.from(video: video)) } } func closeChannelButton(from video: Video) -> some View { Button("Close \(Channel.from(video: video).name) Channel") { - Defaults.reset(.openChannel) + navigationState.closeChannel() } } var openVideoDetailsButton: some View { Button("Open video details") { - openVideoID = video.id - showDetails = true + navigationState.openVideoDetails(video) } } diff --git a/Apple TV/VideoDetailsView.swift b/Apple TV/VideoDetailsView.swift index 208712a3..c2197c08 100644 --- a/Apple TV/VideoDetailsView.swift +++ b/Apple TV/VideoDetailsView.swift @@ -4,64 +4,80 @@ import SwiftUI import URLImage struct VideoDetailsView: View { - @Default(.showingVideoDetails) var showDetails + @Environment(\.dismiss) private var dismiss + + @EnvironmentObject private var navigationState @ObservedObject private var store = Store