View options, video details screen

This commit is contained in:
Arkadiusz Fal
2021-07-08 00:39:18 +02:00
parent 6d35394ffd
commit 4a733f5a30
27 changed files with 652 additions and 108 deletions

View File

@@ -16,7 +16,7 @@ struct ChannelView: View {
}
var body: some View {
VideosListView(videos: store.collection)
VideosView(videos: store.collection)
.onAppear {
resource.loadIfNeeded()
}

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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