Multiplatform UI support fixes

This commit is contained in:
Arkadiusz Fal
2021-07-11 22:52:49 +02:00
parent 0fa0518f0a
commit ca4378afc1
30 changed files with 947 additions and 318 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -2,10 +2,11 @@ import Defaults
import SwiftUI
struct OptionsView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject<NavigationState> 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()

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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 = {}) {

View File

@@ -14,5 +14,11 @@ struct SubscriptionsView: View {
.onAppear {
resource.loadIfNeeded()
}
.refreshable {
resource.load()
}
#if !os(tvOS)
.navigationTitle("Subscriptions")
#endif
}
}

View File

@@ -0,0 +1,53 @@
import Defaults
import SwiftUI
struct TVNavigationView: View {
@EnvironmentObject<NavigationState> 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()
}
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -2,18 +2,15 @@ import Defaults
import SwiftUI
struct VideoContextMenuView: View {
@Default(.tabSelection) var tabSelection
@EnvironmentObject<NavigationState> 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)
}
}

View File

@@ -4,64 +4,80 @@ import SwiftUI
import URLImage
struct VideoDetailsView: View {
@Default(.showingVideoDetails) var showDetails
@Environment(\.dismiss) private var dismiss
@EnvironmentObject<NavigationState> private var navigationState
@ObservedObject private var store = Store<Video>()
var resource: Resource {
InvidiousAPI.shared.video(Defaults[.openVideoID])
InvidiousAPI.shared.video(video.id)
}
init() {
var video: Video
init(_ video: Video) {
self.video = video
resource.addObserver(store)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
if let video = store.item {
VStack(alignment: .center) {
ZStack(alignment: .bottom) {
Group {
if let thumbnail = video.thumbnailURL(quality: "maxres") {
// to replace with AsyncImage when it is fixed with lazy views
URLImage(thumbnail) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 1600, height: 800)
}
}
}
.frame(width: 1600, height: 800)
VStack(alignment: .leading) {
Text(video.title)
.font(.system(size: 40))
HStack {
NavigationLink(destination: PlayerView(id: video.id)) {
HStack(spacing: 8) {
Image(systemName: "play.rectangle.fill")
Text("Play")
HStack {
Spacer()
VStack {
Spacer()
ScrollView(.vertical, showsIndicators: false) {
if let video = store.item {
VStack(alignment: .center) {
ZStack(alignment: .bottom) {
Group {
if let thumbnail = video.thumbnailURL(quality: "maxres") {
// to replace with AsyncImage when it is fixed with lazy views
URLImage(thumbnail) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 1600, height: 800)
}
}
}
.frame(width: 1600, height: 800)
openChannelButton
VStack(alignment: .leading) {
Text(video.title)
.font(.system(size: 40))
HStack {
NavigationLink(destination: PlayerView(id: video.id)) {
HStack(spacing: 8) {
Image(systemName: "play.rectangle.fill")
Text("Play")
}
}
openChannelButton
}
}
.padding(40)
.frame(width: 1600, alignment: .leading)
.background(.thinMaterial)
}
.mask(RoundedRectangle(cornerRadius: 20))
VStack {
Text(video.description)
.lineLimit(nil)
.focusable()
}.frame(width: 1600, alignment: .leading)
}
.padding(40)
.frame(width: 1600, alignment: .leading)
.background(.thinMaterial)
}
.mask(RoundedRectangle(cornerRadius: 20))
VStack {
Text(video.description).lineLimit(nil).focusable()
}.frame(width: 1600, alignment: .leading)
Button("A") {}
}
Spacer()
}
Spacer()
}
.background(.thinMaterial)
.onAppear {
resource.loadIfNeeded()
}
@@ -72,9 +88,8 @@ struct VideoDetailsView: View {
let channel = Channel.from(video: store.item!)
return Button("Open \(channel.name) channel") {
Defaults[.openChannel] = channel
Defaults[.tabSelection] = .channel
showDetails = false
navigationState.openChannel(channel)
dismiss()
}
}
}

View File

@@ -5,85 +5,177 @@ import URLImageStore
struct VideoListRowView: View {
@Environment(\.isFocused) private var focused: Bool
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
#endif
var video: Video
var body: some View {
NavigationLink(destination: PlayerView(id: video.id)) {
HStack(alignment: .top, spacing: 2) {
Section {
if let thumbnail = video.thumbnailURL(quality: "medium") {
// to replace with AsyncImage when it is fixed with lazy views
URLImage(thumbnail) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 320, height: 180)
#if os(tvOS)
NavigationLink(destination: PlayerView(id: video.id)) {
HStack(alignment: .top, spacing: 2) {
roundedThumbnail
HStack {
VStack(alignment: .leading, spacing: 0) {
videoDetail(video.title, bold: true)
videoDetail(video.author, color: .secondary, bold: true)
Spacer()
additionalDetails
}
.mask(RoundedRectangle(cornerRadius: 12))
} else {
Image(systemName: "exclamationmark.square")
.padding()
Spacer()
}
.frame(minHeight: 180)
}
}
#elseif os(macOS)
NavigationLink(destination: PlayerView(id: video.id)) {
verticalyAlignedDetails
}
#else
ZStack {
if verticalSizeClass == .compact {
HStack(alignment: .top) {
thumbnailWithDetails
.frame(minWidth: 0, maxWidth: 320, minHeight: 0, maxHeight: 180)
.padding(4)
VStack(alignment: .leading) {
videoDetail(video.title, bold: true)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.top, 10)
additionalDetails
.padding(.top, 4)
}
}
} else {
verticalyAlignedDetails
}
NavigationLink(destination: PlayerView(id: video.id)) {
EmptyView()
}
.buttonStyle(PlainButtonStyle())
.opacity(0)
.frame(height: 0)
}
#endif
}
var additionalDetails: some View {
VStack {
if !video.published.isEmpty || video.views != 0 {
HStack(spacing: 8) {
if !video.published.isEmpty {
Image(systemName: "calendar")
Text(video.published)
}
if video.views != 0 {
Image(systemName: "eye")
Text(video.viewsCount)
}
}
.frame(width: 320, height: 180)
HStack {
VStack(alignment: .leading) {
Text(video.title)
.foregroundColor(.primary)
.bold()
.lineLimit(1)
Text("\(video.author)")
.foregroundColor(.secondary)
.bold()
.lineLimit(1)
if !video.published.isEmpty || video.views != 0 {
HStack(spacing: 8) {
if !video.published.isEmpty {
Image(systemName: "calendar")
Text(video.published)
}
if video.views != 0 {
Image(systemName: "eye")
Text(video.viewsCount)
}
}
.foregroundColor(.secondary)
.padding(.top)
}
}
.padding()
Spacer()
HStack(spacing: 8) {
if let time = video.playTime {
Image(systemName: "clock")
Text(time)
.fontWeight(.bold)
}
}
#if os(tvOS)
.foregroundColor(.secondary)
}
.frame(minHeight: 180)
#else
.foregroundColor(focused ? .white : .secondary)
#endif
}
}
}
}
// struct VideoThumbnailView_Previews: PreviewProvider {
// static var previews: some View {
// VideoThumbnailView(video: Video(
// id: "A",
// title: "A very very long text which",
// thumbnailURL: URL(string: "https://invidious.home.arekf.net/vi/yXohcxCKqvo/maxres.jpg")!,
// author: "Bear",
// length: 240,
// published: "2 days ago",
// channelID: ""
// )).frame(maxWidth: 350)
// }
// }
var verticalyAlignedDetails: some View {
VStack(alignment: .leading) {
thumbnailWithDetails
.frame(minWidth: 0, maxWidth: 600)
.padding([.leading, .top, .trailing], 4)
VStack(alignment: .leading) {
videoDetail(video.title, bold: true)
.padding(.bottom)
additionalDetails
.padding(.bottom, 10)
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8)
}
}
var thumbnailWithDetails: some View {
Group {
ZStack(alignment: .trailing) {
if let thumbnail = video.thumbnailURL(quality: "maxres") {
// to replace with AsyncImage when it is fixed with lazy views
URLImage(thumbnail) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(minWidth: 0, maxWidth: 600, minHeight: 0, maxHeight: .infinity)
.background(Color.black)
}
.mask(RoundedRectangle(cornerRadius: 12))
} else {
Image(systemName: "exclamationmark.square")
}
VStack(alignment: .trailing) {
Text(video.author)
.padding(8)
.background(.thinMaterial)
.mask(RoundedRectangle(cornerRadius: 12))
.offset(x: -5, y: 5)
.truncationMode(.middle)
Spacer()
if let time = video.playTime {
Text(time)
.fontWeight(.bold)
.padding(8)
.background(.thinMaterial)
.mask(RoundedRectangle(cornerRadius: 12))
.offset(x: -5, y: -5)
}
}
}
}
}
var roundedThumbnail: some View {
Section {
if let thumbnail = video.thumbnailURL(quality: "high") {
// to replace with AsyncImage when it is fixed with lazy views
URLImage(thumbnail) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: 320, minHeight: 0, maxHeight: 180)
}
.mask(RoundedRectangle(cornerRadius: 12))
} else {
Image(systemName: "exclamationmark.square")
}
}
.frame(width: 320, height: 180)
}
func videoDetail(_ text: String, color: Color? = .primary, bold: Bool = false) -> some View {
Text(text)
.fontWeight(bold ? .bold : .regular)
#if os(tvOS)
.foregroundColor(color)
.lineLimit(1)
.truncationMode(.middle)
#elseif os(iOS) || os(macOS)
.foregroundColor(focused ? .white : color)
#endif
}
}

View File

@@ -2,8 +2,6 @@ import Defaults
import SwiftUI
struct VideosCellsView: View {
@Default(.tabSelection) var tabSelection
@State private var columns: Int
init(videos: [Video], columns: Int = 3) {
@@ -15,7 +13,7 @@ struct VideosCellsView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: items, alignment: .center, spacing: 10) {
LazyVGrid(columns: items, alignment: .center) {
ForEach(videos) { video in
VideoCellView(video: video)
.contextMenu { VideoContextMenuView(video: video) }
@@ -28,7 +26,7 @@ struct VideosCellsView: View {
var items: [GridItem] {
Array(repeating: .init(.fixed(600)), count: gridColumns)
}
var gridColumns: Int {
videos.count < columns ? videos.count : columns
}

View File

@@ -10,10 +10,18 @@ struct VideosListView: View {
ForEach(videos) { video in
VideoListRowView(video: video)
.contextMenu { VideoContextMenuView(video: video) }
#if os(tvOS)
.listRowInsets(listRowInsets)
#elseif os(iOS)
.listRowInsets(EdgeInsets(.zero))
.listRowSeparator(.hidden)
#endif
}
}
.listStyle(GroupedListStyle())
#if os(tvOS)
.listStyle(GroupedListStyle())
#endif
}
}

View File

@@ -5,22 +5,35 @@ struct VideosView: View {
@State private var profile = Profile()
@Default(.layout) var layout
@Default(.tabSelection) var tabSelection
@Default(.showingAddToPlaylist) var showingAddToPlaylist
#if os(iOS)
@Environment(\.verticalSizeClass) private var horizontalSizeClass
#endif
var videos: [Video]
var body: some View {
VStack {
if layout == .cells {
VideosCellsView(videos: videos, columns: self.profile.cellsColumns)
} else {
#if os(tvOS)
if layout == .cells {
VideosCellsView(videos: videos, columns: self.profile.cellsColumns)
} else {
VideosListView(videos: videos)
}
#else
VideosListView(videos: videos)
#if os(macOS)
.frame(minWidth: 250, idealWidth: 350)
#endif
#endif
}
#if os(tvOS)
.fullScreenCover(isPresented: $showingAddToPlaylist) {
AddToPlaylistView()
}
}
.fullScreenCover(isPresented: $showingAddToPlaylist) {
AddToPlaylistView()
}
#endif
}
}