mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 04:04:07 +00:00
Rename Apple TV folder to tvOS
This commit is contained in:
97
tvOS/AddToPlaylistView.swift
Normal file
97
tvOS/AddToPlaylistView.swift
Normal file
@@ -0,0 +1,97 @@
|
||||
import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct AddToPlaylistView: View {
|
||||
@ObservedObject private var store = Store<[Playlist]>()
|
||||
|
||||
@State private var selectedPlaylist: Playlist?
|
||||
|
||||
@Default(.videoIDToAddToPlaylist) private var videoID
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.playlists
|
||||
}
|
||||
|
||||
init() {
|
||||
resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
if !resource.isLoading && store.collection.isEmpty {
|
||||
CoverSectionView("You have no Playlists", inline: true) {
|
||||
Text("Open \"Playlists\" tab to create new one")
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
Button("Go back") {
|
||||
dismiss()
|
||||
}
|
||||
.padding()
|
||||
} else if !store.collection.isEmpty {
|
||||
CoverSectionView("Add to Playlist", inline: true) { selectPlaylistButton }
|
||||
|
||||
CoverSectionRowView {
|
||||
Button("Add", action: addToPlaylist)
|
||||
.disabled(currentPlaylist == nil)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: 1200)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()?.onSuccess { _ in
|
||||
selectedPlaylist = store.collection.first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectPlaylistButton: some View {
|
||||
Button(currentPlaylist?.title ?? "Select playlist") {
|
||||
guard currentPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
self.selectedPlaylist = store.collection.next(after: currentPlaylist!)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(store.collection) { playlist in
|
||||
Button(playlist.title) {
|
||||
self.selectedPlaylist = playlist
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentPlaylist: Playlist? {
|
||||
selectedPlaylist ?? store.collection.first
|
||||
}
|
||||
|
||||
func addToPlaylist() {
|
||||
guard currentPlaylist != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let resource = InvidiousAPI.shared.playlistVideos(currentPlaylist!.id)
|
||||
let body = ["videoId": videoID]
|
||||
|
||||
resource.request(.post, json: body).onSuccess { _ in
|
||||
Defaults.reset(.videoIDToAddToPlaylist)
|
||||
InvidiousAPI.shared.playlists.load()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
11
tvOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
11
tvOS/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
]
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"assets" : [
|
||||
{
|
||||
"filename" : "App Icon - App Store.imagestack",
|
||||
"idiom" : "tv",
|
||||
"role" : "primary-app-icon",
|
||||
"size" : "1280x768"
|
||||
},
|
||||
{
|
||||
"filename" : "App Icon.imagestack",
|
||||
"idiom" : "tv",
|
||||
"role" : "primary-app-icon",
|
||||
"size" : "400x240"
|
||||
},
|
||||
{
|
||||
"filename" : "Top Shelf Image Wide.imageset",
|
||||
"idiom" : "tv",
|
||||
"role" : "top-shelf-image-wide",
|
||||
"size" : "2320x720"
|
||||
},
|
||||
{
|
||||
"filename" : "Top Shelf Image.imageset",
|
||||
"idiom" : "tv",
|
||||
"role" : "top-shelf-image",
|
||||
"size" : "1920x720"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "tv-marketing",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
tvOS/Assets.xcassets/Contents.json
Normal file
6
tvOS/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
36
tvOS/ChannelView.swift
Normal file
36
tvOS/ChannelView.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct ChannelView: View {
|
||||
@ObservedObject private var store = Store<[Video]>()
|
||||
|
||||
var id: String
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.channelVideos(id)
|
||||
}
|
||||
|
||||
init(id: String) {
|
||||
self.id = id
|
||||
resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
VideosView(videos: store.collection)
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.background(.ultraThickMaterial)
|
||||
}
|
||||
}
|
19
tvOS/CoverSectionRowView.swift
Normal file
19
tvOS/CoverSectionRowView.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CoverSectionRowView<Content: View>: View {
|
||||
let label: String?
|
||||
let controlView: Content
|
||||
|
||||
init(_ label: String? = nil, @ViewBuilder controlView: @escaping () -> Content) {
|
||||
self.label = label
|
||||
self.controlView = controlView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label ?? "")
|
||||
Spacer()
|
||||
controlView
|
||||
}
|
||||
}
|
||||
}
|
48
tvOS/CoverSectionView.swift
Normal file
48
tvOS/CoverSectionView.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CoverSectionView<Content: View>: View {
|
||||
let title: String?
|
||||
|
||||
let actionsView: Content
|
||||
let divider: Bool
|
||||
let inline: Bool
|
||||
|
||||
init(_ title: String? = nil, divider: Bool = true, inline: Bool = false, @ViewBuilder actionsView: @escaping () -> Content) {
|
||||
self.title = title
|
||||
self.divider = divider
|
||||
self.inline = inline
|
||||
self.actionsView = actionsView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if inline {
|
||||
HStack {
|
||||
if title != nil {
|
||||
sectionTitle
|
||||
}
|
||||
|
||||
Spacer()
|
||||
actionsView
|
||||
}
|
||||
} else if title != nil {
|
||||
sectionTitle
|
||||
}
|
||||
|
||||
if !inline {
|
||||
actionsView
|
||||
}
|
||||
}
|
||||
|
||||
if divider {
|
||||
Divider()
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
var sectionTitle: some View {
|
||||
Text(title ?? "")
|
||||
.font(.title3)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
8
tvOS/Info.plist
Normal file
8
tvOS/Info.plist
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
73
tvOS/OptionsView.swift
Normal file
73
tvOS/OptionsView.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct OptionsView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
|
||||
@Default(.layout) private var layout
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
||||
tabSelectionOptions
|
||||
|
||||
CoverSectionView("View Options") {
|
||||
CoverSectionRowView("Show videos as") { nextLayoutButton }
|
||||
}
|
||||
|
||||
CoverSectionView(divider: false) {
|
||||
CoverSectionRowView("Close View Options") { Button("Close") { dismiss() } }
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: 800)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
var tabSelectionOptions: some View {
|
||||
VStack {
|
||||
switch navigationState.tabSelection {
|
||||
case .search:
|
||||
SearchOptionsView()
|
||||
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var nextLayoutButton: some View {
|
||||
Button(layout.name) {
|
||||
self.layout = layout.next()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(ListingLayout.allCases) { layout in
|
||||
Button(layout.name) {
|
||||
Defaults[.layout] = layout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OptionsView()
|
||||
}
|
||||
}
|
120
tvOS/PlaylistFormView.swift
Normal file
120
tvOS/PlaylistFormView.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct PlaylistFormView: View {
|
||||
@State private var name = ""
|
||||
@State private var visibility = Playlist.Visibility.public
|
||||
|
||||
@State private var valid = false
|
||||
@State private var showingDeleteConfirmation = false
|
||||
|
||||
@Binding var playlist: Playlist!
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var editing: Bool {
|
||||
playlist != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
CoverSectionView(editing ? "Edit Playlist" : "Create Playlist") {
|
||||
CoverSectionRowView("Name") {
|
||||
TextField("Playlist Name", text: $name, onCommit: validate)
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
|
||||
CoverSectionRowView("Visibility") { visibilityButton }
|
||||
}
|
||||
|
||||
CoverSectionRowView {
|
||||
Button("Save", action: submitForm).disabled(!valid)
|
||||
}
|
||||
|
||||
if editing {
|
||||
CoverSectionView("Delete Playlist", divider: false, inline: true) { deletePlaylistButton }
|
||||
.padding(.top, 50)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: 800)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
.onAppear {
|
||||
guard editing else {
|
||||
return
|
||||
}
|
||||
|
||||
self.name = self.playlist.title
|
||||
self.visibility = self.playlist.visibility
|
||||
|
||||
validate()
|
||||
}
|
||||
}
|
||||
|
||||
func validate() {
|
||||
valid = !name.isEmpty
|
||||
}
|
||||
|
||||
func submitForm() {
|
||||
guard valid else {
|
||||
return
|
||||
}
|
||||
|
||||
let body = ["title": name, "privacy": visibility.rawValue]
|
||||
|
||||
resource.request(editing ? .patch : .post, json: body).onSuccess { response in
|
||||
if let createdPlaylist: Playlist = response.typedContent() {
|
||||
playlist = createdPlaylist
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
var resource: Resource {
|
||||
editing ? InvidiousAPI.shared.playlist(playlist.id) : InvidiousAPI.shared.playlists
|
||||
}
|
||||
|
||||
var visibilityButton: some View {
|
||||
Button(self.visibility.name) {
|
||||
self.visibility = self.visibility.next()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(Playlist.Visibility.allCases) { visibility in
|
||||
Button(visibility.name) {
|
||||
self.visibility = visibility
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deletePlaylistButton: some View {
|
||||
Button("Delete", role: .destructive) {
|
||||
showingDeleteConfirmation = true
|
||||
}.alert(isPresented: $showingDeleteConfirmation) {
|
||||
Alert(
|
||||
title: Text("Are you sure you want to delete playlist?"),
|
||||
message: Text("Playlist \"\(playlist.title)\" will be deleted.\nIt cannot be undone."),
|
||||
primaryButton: .destructive(Text("Delete"), action: deletePlaylistAndDismiss),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func deletePlaylistAndDismiss() {
|
||||
let resource = InvidiousAPI.shared.playlist(playlist.id)
|
||||
resource.request(.delete).onSuccess { _ in
|
||||
playlist = nil
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
64
tvOS/SearchOptionsView.swift
Normal file
64
tvOS/SearchOptionsView.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct SearchOptionsView: View {
|
||||
@Default(.searchSortOrder) private var searchSortOrder
|
||||
@Default(.searchDate) private var searchDate
|
||||
@Default(.searchDuration) private var searchDuration
|
||||
|
||||
var body: some View {
|
||||
CoverSectionView("Search Options") {
|
||||
CoverSectionRowView("Sort By") { searchSortOrderButton }
|
||||
CoverSectionRowView("Upload date") { searchDateButton }
|
||||
CoverSectionRowView("Duration") { searchDurationButton }
|
||||
}
|
||||
}
|
||||
|
||||
var searchSortOrderButton: some View {
|
||||
Button(self.searchSortOrder.name) {
|
||||
self.searchSortOrder = self.searchSortOrder.next()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(SearchQuery.SortOrder.allCases) { sortOrder in
|
||||
Button(sortOrder.name) {
|
||||
self.searchSortOrder = sortOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchDateButton: some View {
|
||||
Button(self.searchDate?.name ?? "All") {
|
||||
self.searchDate = self.searchDate == nil ? SearchQuery.Date.allCases.first : self.searchDate!.next(nilAtEnd: true)
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
ForEach(SearchQuery.Date.allCases) { searchDate in
|
||||
Button(searchDate.name) {
|
||||
self.searchDate = searchDate
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset") {
|
||||
self.searchDate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchDurationButton: some View {
|
||||
Button(self.searchDuration?.name ?? "All") {
|
||||
self.searchDuration = self.searchDuration == nil ? SearchQuery.Duration.allCases.first : self.searchDuration!.next(nilAtEnd: true)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(SearchQuery.Duration.allCases) { searchDuration in
|
||||
Button(searchDuration.name) {
|
||||
self.searchDuration = searchDuration
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset") {
|
||||
self.searchDuration = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
tvOS/TVNavigationView.swift
Normal file
62
tvOS/TVNavigationView.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct TVNavigationView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
|
||||
@State private var showingOptions = false
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
TabView(selection: $navigationState.tabSelection) {
|
||||
SubscriptionsView()
|
||||
.tabItem { Text("Subscriptions") }
|
||||
.tag(TabSelection.subscriptions)
|
||||
|
||||
PopularView()
|
||||
.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: $showingAddToPlaylist) { AddToPlaylistView() }
|
||||
.fullScreenCover(isPresented: $navigationState.showingVideoDetails) {
|
||||
if let video = navigationState.video {
|
||||
VideoDetailsView(video)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $navigationState.showingChannel, onDismiss: {
|
||||
navigationState.showVideoDetailsIfNeeded()
|
||||
}) {
|
||||
if let channel = navigationState.channel {
|
||||
ChannelView(id: channel.id)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $navigationState.showingVideo) {
|
||||
if let video = navigationState.video {
|
||||
VideoPlayerView(video)
|
||||
}
|
||||
}
|
||||
.onPlayPauseCommand { showingOptions.toggle() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TVNavigationView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TVNavigationView()
|
||||
}
|
||||
}
|
57
tvOS/VideoContextMenuView.swift
Normal file
57
tvOS/VideoContextMenuView.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct VideoContextMenuView: View {
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
|
||||
let video: Video
|
||||
|
||||
@Default(.showingAddToPlaylist) var showingAddToPlaylist
|
||||
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
|
||||
|
||||
var body: some View {
|
||||
openChannelButton(from: video)
|
||||
|
||||
openVideoDetailsButton
|
||||
|
||||
if navigationState.tabSelection == .playlists {
|
||||
removeFromPlaylistButton
|
||||
} else {
|
||||
addToPlaylistButton
|
||||
}
|
||||
}
|
||||
|
||||
func openChannelButton(from video: Video) -> some View {
|
||||
Button("\(video.author) Channel") {
|
||||
navigationState.openChannel(Channel.from(video: video))
|
||||
}
|
||||
}
|
||||
|
||||
func closeChannelButton(from video: Video) -> some View {
|
||||
Button("Close \(Channel.from(video: video).name) Channel") {
|
||||
navigationState.closeChannel()
|
||||
}
|
||||
}
|
||||
|
||||
var openVideoDetailsButton: some View {
|
||||
Button("Open video details") {
|
||||
navigationState.openVideoDetails(video)
|
||||
}
|
||||
}
|
||||
|
||||
var addToPlaylistButton: some View {
|
||||
Button("Add to playlist...") {
|
||||
videoIDToAddToPlaylist = video.id
|
||||
showingAddToPlaylist = true
|
||||
}
|
||||
}
|
||||
|
||||
var removeFromPlaylistButton: some View {
|
||||
Button("Remove from playlist", role: .destructive) {
|
||||
let resource = InvidiousAPI.shared.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!)
|
||||
resource.request(.delete).onSuccess { _ in
|
||||
InvidiousAPI.shared.playlists.load()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
112
tvOS/VideoDetailsView.swift
Normal file
112
tvOS/VideoDetailsView.swift
Normal file
@@ -0,0 +1,112 @@
|
||||
import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct VideoDetailsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@EnvironmentObject<NavigationState> private var navigationState
|
||||
|
||||
@ObservedObject private var store = Store<Video>()
|
||||
|
||||
@State private var playVideoLinkActive = false
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.video(video.id)
|
||||
}
|
||||
|
||||
var video: Video
|
||||
|
||||
init(_ video: Video) {
|
||||
self.video = video
|
||||
resource.addObserver(store)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
if let video = store.item {
|
||||
VStack(alignment: .center) {
|
||||
ZStack(alignment: .bottom) {
|
||||
Group {
|
||||
if let url = video.thumbnailURL(quality: .maxres) {
|
||||
AsyncImage(url: url) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 1600, height: 800)
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 1600, height: 800)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(video.title)
|
||||
.font(.system(size: 40))
|
||||
|
||||
HStack {
|
||||
playVideoButton
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
var playVideoButton: some View {
|
||||
Button(action: {
|
||||
navigationState.returnToDetails = true
|
||||
playVideoLinkActive = true
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "play.rectangle.fill")
|
||||
Text("Play")
|
||||
}
|
||||
}
|
||||
.background(NavigationLink(destination: VideoPlayerView(video), isActive: $playVideoLinkActive) { EmptyView() }.hidden())
|
||||
}
|
||||
|
||||
var openChannelButton: some View {
|
||||
let channel = Channel.from(video: store.item!)
|
||||
|
||||
return Button("Open \(channel.name) channel") {
|
||||
navigationState.openChannel(channel)
|
||||
navigationState.returnToDetails = true
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user