diff --git a/Apple TV/ChannelView.swift b/Apple TV/ChannelView.swift index eff625be..656f6cac 100644 --- a/Apple TV/ChannelView.swift +++ b/Apple TV/ChannelView.swift @@ -16,7 +16,7 @@ struct ChannelView: View { } var body: some View { - VideosListView(videos: store.collection) + VideosView(videos: store.collection) .onAppear { resource.loadIfNeeded() } diff --git a/Apple TV/OptionRowView.swift b/Apple TV/OptionRowView.swift new file mode 100644 index 00000000..f94c1cd3 --- /dev/null +++ b/Apple TV/OptionRowView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct OptionRowView: 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 + } + } +} diff --git a/Apple TV/OptionsSectionView.swift b/Apple TV/OptionsSectionView.swift new file mode 100644 index 00000000..e859965e --- /dev/null +++ b/Apple TV/OptionsSectionView.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct OptionsSectionView: 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) + } +} diff --git a/Apple TV/OptionsView.swift b/Apple TV/OptionsView.swift new file mode 100644 index 00000000..14eb4e0e --- /dev/null +++ b/Apple TV/OptionsView.swift @@ -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() + } +} diff --git a/Apple TV/SearchOptionsView.swift b/Apple TV/SearchOptionsView.swift new file mode 100644 index 00000000..5b6440a3 --- /dev/null +++ b/Apple TV/SearchOptionsView.swift @@ -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) + } + } + } +} diff --git a/Apple TV/SearchView.swift b/Apple TV/SearchView.swift index f3314661..cbbc73c3 100644 --- a/Apple TV/SearchView.swift +++ b/Apple TV/SearchView.swift @@ -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 + } } diff --git a/Apple TV/VideoCellView.swift b/Apple TV/VideoCellView.swift index 601fe29d..307ab533 100644 --- a/Apple TV/VideoCellView.swift +++ b/Apple TV/VideoCellView.swift @@ -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 diff --git a/Apple TV/VideoContextMenuView.swift b/Apple TV/VideoContextMenuView.swift index 832e0b97..59c82ffa 100644 --- a/Apple TV/VideoContextMenuView.swift +++ b/Apple TV/VideoContextMenuView.swift @@ -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 { diff --git a/Apple TV/VideoDetailsView.swift b/Apple TV/VideoDetailsView.swift new file mode 100644 index 00000000..dbe4d5b1 --- /dev/null +++ b/Apple TV/VideoDetailsView.swift @@ -0,0 +1,80 @@ +import Defaults +import Siesta +import SwiftUI +import URLImage + +struct VideoDetailsView: View { + @Default(.showingVideoDetails) var showDetails + + @ObservedObject private var store = Store