mirror of
https://github.com/yattee/yattee.git
synced 2025-10-28 19:21:58 +00:00
View options, video details screen
This commit is contained in:
@@ -16,7 +16,7 @@ struct ChannelView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VideosListView(videos: store.collection)
|
||||
VideosView(videos: store.collection)
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
|
||||
19
Apple TV/OptionRowView.swift
Normal file
19
Apple TV/OptionRowView.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OptionRowView<Content: View>: View {
|
||||
let label: String
|
||||
let controlView: Content
|
||||
|
||||
init(_ label: String, @ViewBuilder controlView: @escaping () -> Content) {
|
||||
self.label = label
|
||||
self.controlView = controlView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(label)
|
||||
Spacer()
|
||||
controlView
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Apple TV/OptionsSectionView.swift
Normal file
35
Apple TV/OptionsSectionView.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OptionsSectionView<Content: View>: View {
|
||||
let title: String?
|
||||
|
||||
let rowsView: Content
|
||||
let divider: Bool
|
||||
|
||||
init(_ title: String? = nil, divider: Bool = true, @ViewBuilder rowsView: @escaping () -> Content) {
|
||||
self.title = title
|
||||
self.divider = divider
|
||||
self.rowsView = rowsView()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if title != nil {
|
||||
sectionTitle
|
||||
}
|
||||
|
||||
rowsView
|
||||
}
|
||||
|
||||
if divider {
|
||||
Divider()
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
var sectionTitle: some View {
|
||||
Text(title ?? "")
|
||||
.font(.title3)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
72
Apple TV/OptionsView.swift
Normal file
72
Apple TV/OptionsView.swift
Normal file
@@ -0,0 +1,72 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct OptionsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Default(.layout) private var layout
|
||||
@Default(.tabSelection) private var tabSelection
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
|
||||
tabSelectionOptions
|
||||
|
||||
OptionsSectionView("View Options") {
|
||||
OptionRowView("Show videos as") { nextLayoutButton }
|
||||
}
|
||||
|
||||
OptionsSectionView(divider: false) {
|
||||
OptionRowView("Close View Options") { Button("Close") { dismiss() } }
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: 800)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
var tabSelectionOptions: some View {
|
||||
VStack {
|
||||
switch 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()
|
||||
}
|
||||
}
|
||||
66
Apple TV/SearchOptionsView.swift
Normal file
66
Apple TV/SearchOptionsView.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
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 {
|
||||
OptionsSectionView("Search Options") {
|
||||
OptionRowView("Sort By") { searchSortOrderButton }
|
||||
OptionRowView("Upload date") { searchDateButton }
|
||||
OptionRowView("Duration") { searchDurationButton }
|
||||
}
|
||||
}
|
||||
|
||||
var searchSortOrderButton: some View {
|
||||
Button(self.searchSortOrder.name) {
|
||||
self.searchSortOrder = self.searchSortOrder.next()
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(SearchSortOrder.allCases) { sortOrder in
|
||||
Button(sortOrder.name) {
|
||||
self.searchSortOrder = sortOrder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchDateButton: some View {
|
||||
Button(self.searchDate?.name ?? "All") {
|
||||
self.searchDate = self.searchDate == nil ? SearchDate.allCases.first : self.searchDate!.next(nilAtEnd: true)
|
||||
}
|
||||
|
||||
.contextMenu {
|
||||
ForEach(SearchDate.allCases) { searchDate in
|
||||
Button(searchDate.name) {
|
||||
self.searchDate = searchDate
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset") {
|
||||
self.searchDate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var searchDurationButton: some View {
|
||||
Button(self.searchDuration?.name ?? "All") {
|
||||
let duration = Defaults[.searchDuration]
|
||||
|
||||
Defaults[.searchDuration] = duration == nil ? SearchDuration.allCases.first : duration!.next(nilAtEnd: true)
|
||||
}
|
||||
.contextMenu {
|
||||
ForEach(SearchDuration.allCases) { searchDuration in
|
||||
Button(searchDuration.name) {
|
||||
Defaults[.searchDuration] = searchDuration
|
||||
}
|
||||
}
|
||||
|
||||
Button("Reset") {
|
||||
Defaults.reset(.searchDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,33 +3,68 @@ import Siesta
|
||||
import SwiftUI
|
||||
|
||||
struct SearchView: View {
|
||||
@Default(.searchQuery) var query
|
||||
@Default(.searchQuery) private var queryText
|
||||
@Default(.searchSortOrder) private var searchSortOrder
|
||||
@Default(.searchDate) private var searchDate
|
||||
@Default(.searchDuration) private var searchDuration
|
||||
|
||||
@ObservedObject private var store = Store<[Video]>()
|
||||
@ObservedObject private var query = SearchQuery()
|
||||
|
||||
var body: some View {
|
||||
VideosView(videos: store.collection)
|
||||
.searchable(text: $query)
|
||||
.onAppear {
|
||||
queryChanged(new: query)
|
||||
VStack {
|
||||
if !store.collection.isEmpty {
|
||||
VideosView(videos: store.collection)
|
||||
}
|
||||
.onChange(of: query) { newQuery in
|
||||
queryChanged(old: query, new: newQuery)
|
||||
|
||||
if store.collection.isEmpty && !resource.isLoading {
|
||||
Text("No results")
|
||||
|
||||
if searchFiltersActive {
|
||||
Button("Reset search filters") {
|
||||
Defaults.reset(.searchDate, .searchDuration)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.searchable(text: $queryText)
|
||||
.onAppear {
|
||||
changeQuery {
|
||||
query.query = queryText
|
||||
query.sortBy = searchSortOrder
|
||||
query.date = searchDate
|
||||
query.duration = searchDuration
|
||||
}
|
||||
}
|
||||
.onChange(of: queryText) { queryText in
|
||||
changeQuery { query.query = queryText }
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
changeQuery { query.sortBy = order }
|
||||
}
|
||||
.onChange(of: searchDate) { date in
|
||||
changeQuery { query.date = date }
|
||||
}
|
||||
.onChange(of: searchDuration) { duration in
|
||||
changeQuery { query.duration = duration }
|
||||
}
|
||||
}
|
||||
|
||||
func queryChanged(old: String? = nil, new: String) {
|
||||
if old != nil {
|
||||
let oldResource = resource(old!)
|
||||
oldResource.removeObservers(ownedBy: store)
|
||||
}
|
||||
func changeQuery(_ change: @escaping () -> Void = {}) {
|
||||
resource.removeObservers(ownedBy: store)
|
||||
change()
|
||||
|
||||
let resource = resource(new)
|
||||
resource.addObserver(store)
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
|
||||
func resource(_ query: String) -> Resource {
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.search(query)
|
||||
}
|
||||
|
||||
var searchFiltersActive: Bool {
|
||||
searchDate != nil || searchDuration != nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ struct VideoCellView: View {
|
||||
NavigationLink(destination: PlayerView(id: video.id)) {
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .trailing) {
|
||||
if let thumbnail = video.thumbnailURL {
|
||||
if let thumbnail = video.thumbnailURL(quality: "high") {
|
||||
// to replace with AsyncImage when it is fixed with lazy views
|
||||
URLImage(thumbnail) { image in
|
||||
image
|
||||
|
||||
@@ -6,12 +6,20 @@ struct VideoContextMenuView: View {
|
||||
|
||||
let video: Video
|
||||
|
||||
@Default(.openVideoID) var openVideoID
|
||||
@Default(.showingVideoDetails) var showDetails
|
||||
|
||||
var body: some View {
|
||||
if tabSelection == .channel {
|
||||
closeChannelButton(from: video)
|
||||
} else {
|
||||
openChannelButton(from: video)
|
||||
}
|
||||
|
||||
Button("Open video details") {
|
||||
openVideoID = video.id
|
||||
showDetails = true
|
||||
}
|
||||
}
|
||||
|
||||
func openChannelButton(from video: Video) -> some View {
|
||||
|
||||
80
Apple TV/VideoDetailsView.swift
Normal file
80
Apple TV/VideoDetailsView.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import Defaults
|
||||
import Siesta
|
||||
import SwiftUI
|
||||
import URLImage
|
||||
|
||||
struct VideoDetailsView: View {
|
||||
@Default(.showingVideoDetails) var showDetails
|
||||
|
||||
@ObservedObject private var store = Store<Video>()
|
||||
|
||||
var resource: Resource {
|
||||
InvidiousAPI.shared.video(Defaults[.openVideoID])
|
||||
}
|
||||
|
||||
init() {
|
||||
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: 10) {
|
||||
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)
|
||||
Button("A") {}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
resource.loadIfNeeded()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
var openChannelButton: some View {
|
||||
let channel = Channel.from(video: store.item!)
|
||||
|
||||
return Button("Open \(channel.name) channel") {
|
||||
Defaults[.openChannel] = channel
|
||||
Defaults[.tabSelection] = .channel
|
||||
showDetails = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ struct VideoListRowView: View {
|
||||
NavigationLink(destination: PlayerView(id: video.id)) {
|
||||
HStack(alignment: .top, spacing: 2) {
|
||||
Section {
|
||||
if let thumbnail = video.thumbnailURL {
|
||||
if let thumbnail = video.thumbnailURL(quality: "medium") {
|
||||
// to replace with AsyncImage when it is fixed with lazy views
|
||||
URLImage(thumbnail) { image in
|
||||
image
|
||||
|
||||
@@ -3,23 +3,19 @@ import SwiftUI
|
||||
|
||||
struct VideosView: View {
|
||||
@State private var profile = Profile()
|
||||
|
||||
var videos: [Video]
|
||||
|
||||
|
||||
@Default(.layout) var layout
|
||||
@Default(.tabSelection) var tabSelection
|
||||
|
||||
@State private var showingViewOptions = false
|
||||
|
||||
var videos: [Video]
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Group {
|
||||
if layout == .cells {
|
||||
VideosCellsView(videos: videos, columns: self.profile.cellsColumns)
|
||||
} else {
|
||||
VideosListView(videos: videos)
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingViewOptions) { ViewOptionsView() }
|
||||
.onPlayPauseCommand { showingViewOptions.toggle() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct ViewOptionsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Default(.layout) var layout
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .center) {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
nextLayoutButton
|
||||
|
||||
Button("Close") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
|
||||
var nextLayoutButton: some View {
|
||||
Button(layout.next().name, action: nextLayout)
|
||||
}
|
||||
|
||||
func nextLayout() {
|
||||
Defaults[.layout] = layout.next()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user