mirror of
https://github.com/yattee/yattee.git
synced 2025-10-29 11:41:54 +00:00
Multiplatform UI support fixes
This commit is contained in:
57
Apple TV/AppTabNavigation.swift
Normal file
57
Apple TV/AppTabNavigation.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -14,5 +14,11 @@ struct SubscriptionsView: View {
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
.refreshable {
|
||||
resource.load()
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle("Subscriptions")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
53
Apple TV/TVNavigationView.swift
Normal file
53
Apple TV/TVNavigationView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user