From 09c3947fefdabd5598b0fb82603e97994b31b4f5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 16 Aug 2021 17:52:42 +0200 Subject: [PATCH] Forms improvements for trending and playlists --- Fixtures/Playlist+Fixtures.swift | 7 + Model/Playlist.swift | 7 + Pearvidious.xcodeproj/project.pbxproj | 14 +- {tvOS => Shared}/CoverSectionRowView.swift | 7 +- {tvOS => Shared}/CoverSectionView.swift | 8 +- Shared/PlaylistFormView.swift | 194 +++++++++++++++++++++ Shared/PlaylistsView.swift | 183 +++++++++++++------ Shared/TrendingCountrySelection.swift | 3 + Shared/VideoView.swift | 2 +- Shared/VideosCellsView.swift | 12 +- Shared/VideosListView.swift | 11 +- tvOS/PlaylistFormView.swift | 120 ------------- 12 files changed, 379 insertions(+), 189 deletions(-) create mode 100644 Fixtures/Playlist+Fixtures.swift rename {tvOS => Shared}/CoverSectionRowView.swift (71%) rename {tvOS => Shared}/CoverSectionView.swift (90%) create mode 100644 Shared/PlaylistFormView.swift delete mode 100644 tvOS/PlaylistFormView.swift diff --git a/Fixtures/Playlist+Fixtures.swift b/Fixtures/Playlist+Fixtures.swift new file mode 100644 index 00000000..c3424b07 --- /dev/null +++ b/Fixtures/Playlist+Fixtures.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Playlist { + static var fixture: Playlist { + Playlist(id: "ABC", title: "The Playlist", visibility: .public, updated: 1) + } +} diff --git a/Model/Playlist.swift b/Model/Playlist.swift index ddcb147d..b790f4cb 100644 --- a/Model/Playlist.swift +++ b/Model/Playlist.swift @@ -22,6 +22,13 @@ struct Playlist: Identifiable, Equatable, Hashable { var videos = [Video]() + init(id: String, title: String, visibility: Visibility, updated: TimeInterval) { + self.id = id + self.title = title + self.visibility = visibility + self.updated = updated + } + init(_ json: JSON) { id = json["playlistId"].stringValue title = json["title"].stringValue diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index e5393895..3da688cf 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -174,6 +174,9 @@ 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; + 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; + 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; + 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F4AE7226828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; 37F4AE7326828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; 37F4AE7426828F0900BD60EA /* VideosCellsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */; }; @@ -270,6 +273,7 @@ 37D4B1AE26729DEB00C925CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = ""; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = ""; }; + 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosCellsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -340,6 +344,7 @@ children = ( 3748186926A764FB0084E870 /* Thumbnail+Fixtures.swift */, 3748186526A7627F0084E870 /* Video+Fixtures.swift */, + 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */, ); path = Fixtures; sourceTree = ""; @@ -394,11 +399,14 @@ 37D4B0C32671614700C925CA /* AppTabNavigation.swift */, 37BD07B42698AA4D003EBB87 /* ContentView.swift */, 37141672267A8E10006CA35D /* Country.swift */, + 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */, + 373CFABD26966115003CB2C6 /* CoverSectionView.swift */, 372915E52687E3B900F5A35B /* Defaults.swift */, 3748186D26A769D60084E870 /* DetailBadge.swift */, 37D4B0C22671614700C925CA /* PearvidiousApp.swift */, 37BE0BD226A1D4780092E2DB /* Player.swift */, 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */, + 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, 376578902685490700D4EA09 /* PlaylistsView.swift */, 37AAF27D26737323007FC770 /* PopularView.swift */, 37AAF27F26737550007FC770 /* SearchView.swift */, @@ -450,10 +458,7 @@ children = ( 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */, 37AAF2892673AB89007FC770 /* ChannelView.swift */, - 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */, - 373CFABD26966115003CB2C6 /* CoverSectionView.swift */, 37B76E95268747C900CE5671 /* OptionsView.swift */, - 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, 373CFAC52696617C003CB2C6 /* SearchOptionsView.swift */, 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */, @@ -778,6 +783,7 @@ 379775932689365600DD52A8 /* Array+Next.swift in Sources */, 377FC7E1267A082600A6BBAF /* ChannelView.swift in Sources */, 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, + 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFAC226966159003CB2C6 /* CoverSectionRowView.swift in Sources */, @@ -808,6 +814,7 @@ 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */, 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */, 37BD07C32698AD4F003EBB87 /* ContentView.swift in Sources */, + 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, 377FC7E2267A084A00A6BBAF /* VideoView.swift in Sources */, @@ -872,6 +879,7 @@ 37754C9F26B7500000DBD602 /* VideosView.swift in Sources */, 37AAF28026737550007FC770 /* SearchView.swift in Sources */, 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */, + 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37BE0BD426A1D47D0092E2DB /* Player.swift in Sources */, 37977585268922F600DD52A8 /* InvidiousAPI.swift in Sources */, diff --git a/tvOS/CoverSectionRowView.swift b/Shared/CoverSectionRowView.swift similarity index 71% rename from tvOS/CoverSectionRowView.swift rename to Shared/CoverSectionRowView.swift index 681a0549..b57fdeba 100644 --- a/tvOS/CoverSectionRowView.swift +++ b/Shared/CoverSectionRowView.swift @@ -1,10 +1,10 @@ import SwiftUI -struct CoverSectionRowView: View { +struct CoverSectionRowView: View { let label: String? - let controlView: Content + let controlView: ControlContent - init(_ label: String? = nil, @ViewBuilder controlView: @escaping () -> Content) { + init(_ label: String? = nil, @ViewBuilder controlView: @escaping () -> ControlContent) { self.label = label self.controlView = controlView() } @@ -12,6 +12,7 @@ struct CoverSectionRowView: View { var body: some View { HStack { Text(label ?? "") + Spacer() controlView } diff --git a/tvOS/CoverSectionView.swift b/Shared/CoverSectionView.swift similarity index 90% rename from tvOS/CoverSectionView.swift rename to Shared/CoverSectionView.swift index 58e12d2d..2e49e392 100644 --- a/tvOS/CoverSectionView.swift +++ b/Shared/CoverSectionView.swift @@ -42,7 +42,11 @@ struct CoverSectionView: View { var sectionTitle: some View { Text(title ?? "") - .font(.title3) - .padding(.bottom) + + .font(.title2) + #if os(macOS) + .bold() + #endif + .padding(.bottom) } } diff --git a/Shared/PlaylistFormView.swift b/Shared/PlaylistFormView.swift new file mode 100644 index 00000000..9c483250 --- /dev/null +++ b/Shared/PlaylistFormView.swift @@ -0,0 +1,194 @@ +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 { + #if os(macOS) || os(iOS) + VStack(alignment: .leading) { + HStack(alignment: .center) { + Text(editing ? "Edit Playlist" : "Create Playlist") + .font(.title2.bold()) + + Spacer() + + Button("Cancel") { + dismiss() + }.keyboardShortcut(.cancelAction) + } + Form { + TextField("Name", text: $name, onCommit: validate) + .frame(maxWidth: 450) + .padding(.leading, 10) + + Picker("Visibility", selection: $visibility) { + ForEach(Playlist.Visibility.allCases, id: \.self) { visibility in + Text(visibility.name) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + Divider() + .padding(.vertical, 4) + HStack { + if editing { + deletePlaylistButton + } + + Spacer() + + Button("Save", action: submitForm).disabled(!valid) + .keyboardShortcut(.defaultAction) + } + } + .onChange(of: name) { _ in validate() } + .onAppear(perform: setFieldsFromPlaylist) + .padding(.horizontal) + #if !os(iOS) + .frame(width: 400, height: 150) + #endif + + #else + 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() + } + #endif + } + + func setFieldsFromPlaylist() { + guard editing else { + return + } + + name = playlist.title + visibility = 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 modifiedPlaylist: Playlist = response.typedContent() { + playlist = modifiedPlaylist + } + + dismiss() + } + } + + var resource: Resource { + editing ? InvidiousAPI.shared.playlist(playlist.id) : InvidiousAPI.shared.playlists + } + + var visibilityButton: some View { + #if os(macOS) + Picker("Visibility", selection: $visibility) { + ForEach(Playlist.Visibility.allCases) { visibility in + Text(visibility.name) + } + } + #else + Button(self.visibility.name) { + self.visibility = self.visibility.next() + } + .contextMenu { + ForEach(Playlist.Visibility.allCases) { visibility in + Button(visibility.name) { + self.visibility = visibility + } + } + } + #endif + } + + 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() + ) + } + #if os(macOS) + .foregroundColor(.red) + #endif + } + + func deletePlaylistAndDismiss() { + let resource = InvidiousAPI.shared.playlist(playlist.id) + resource.request(.delete).onSuccess { _ in + playlist = nil + dismiss() + } + } +} + +struct PlaylistFormView_Previews: PreviewProvider { + static var previews: some View { + PlaylistFormView(playlist: .constant(Playlist.fixture)) + } +} diff --git a/Shared/PlaylistsView.swift b/Shared/PlaylistsView.swift index 41ec8a97..a0ea5dfb 100644 --- a/Shared/PlaylistsView.swift +++ b/Shared/PlaylistsView.swift @@ -21,56 +21,71 @@ struct PlaylistsView: View { resource.addObserver(store) } + var videos: [Video] { + currentPlaylist?.videos ?? [] + } + + var videosViewMaxHeight: CGFloat { + #if os(tvOS) + videos.isEmpty ? 150 : .infinity + #else + videos.isEmpty ? 0 : .infinity + #endif + } + var body: some View { - Section { - VStack(alignment: .center, spacing: 2) { - #if os(tvOS) - HStack { - if store.collection.isEmpty { - Text("No Playlists") - .foregroundColor(.secondary) - } else { - Text("Current Playlist") - .foregroundColor(.secondary) + VStack { + #if os(tvOS) + toolbar + .font(.system(size: 28)) - selectPlaylistButton - } - - if currentPlaylist != nil { - editPlaylistButton - } - - newPlaylistButton - .padding(.leading, 40) - } - .scaleEffect(0.85) - #endif - - if currentPlaylist != nil { - if currentPlaylist!.videos.isEmpty { - Spacer() - - Text("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"") - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - Spacer() - } else { - VideosView(videos: currentPlaylist!.videos) - } - } else { - Spacer() - } + #endif + if currentPlaylist != nil, videos.isEmpty { + hintText("Playlist is empty\n\nTap and hold on a video and then tap \"Add to Playlist\"") + } else if store.collection.isEmpty { + hintText("You have no playlists\n\nTap on \"New Playlist\" to create one") + } else { + VideosView(videos: videos) } + + #if os(iOS) + toolbar + .font(.system(size: 14)) + .animation(nil) + .padding(.horizontal) + .padding(.vertical, 10) + .overlay(Divider().offset(x: 0, y: -2), alignment: .topTrailing) + #endif } - #if !os(macOS) + #if os(tvOS) .fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { PlaylistFormView(playlist: $createdPlaylist) } .fullScreenCover(isPresented: $showingEditPlaylist, onDismiss: selectEditedPlaylist) { PlaylistFormView(playlist: $editedPlaylist) } + #else + .sheet(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { + PlaylistFormView(playlist: $createdPlaylist) + } + .sheet(isPresented: $showingEditPlaylist, onDismiss: selectEditedPlaylist) { + PlaylistFormView(playlist: $editedPlaylist) + } #endif + .toolbar { + ToolbarItemGroup { + #if !os(iOS) + if !store.collection.isEmpty { + selectPlaylistButton + } + + if currentPlaylist != nil { + editPlaylistButton + } + #endif + newPlaylistButton + } + } .onAppear { resource.loadIfNeeded()?.onSuccess { _ in selectPlaylist(selectedPlaylistID) @@ -83,6 +98,51 @@ struct PlaylistsView: View { #endif } + var scaledToolbar: some View { + toolbar.scaleEffect(0.85) + } + + var toolbar: some View { + HStack { + if store.collection.isEmpty { + Text("No Playlists") + .foregroundColor(.secondary) + } else { + Text("Current Playlist") + .foregroundColor(.secondary) + + selectPlaylistButton + } + + #if os(iOS) + Spacer() + #endif + + if currentPlaylist != nil { + editPlaylistButton + } + + #if !os(iOS) + newPlaylistButton + .padding(.leading, 40) + #endif + } + } + + func hintText(_ text: String) -> some View { + VStack { + Spacer() + Text(text) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + Spacer() + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + #if os(macOS) + .background() + #endif + } + func selectPlaylist(_ id: String?) { selectedPlaylistID = id } @@ -116,20 +176,34 @@ struct PlaylistsView: View { } var selectPlaylistButton: some View { - Button(currentPlaylist?.title ?? "Select playlist") { - guard currentPlaylist != nil else { - return - } + #if os(tvOS) + Button(currentPlaylist?.title ?? "Select playlist") { + guard currentPlaylist != nil else { + return + } - selectPlaylist(store.collection.next(after: currentPlaylist!)?.id) - } - .contextMenu { - ForEach(store.collection) { playlist in - Button(playlist.title) { - selectPlaylist(playlist.id) + selectPlaylist(store.collection.next(after: currentPlaylist!)?.id) + } + .contextMenu { + ForEach(store.collection) { playlist in + Button(playlist.title) { + selectPlaylist(playlist.id) + } } } - } + #else + Menu(currentPlaylist?.title ?? "Select playlist") { + ForEach(store.collection) { playlist in + Button(action: { selectPlaylist(playlist.id) }) { + if playlist == self.currentPlaylist { + Label(playlist.title, systemImage: "checkmark") + } else { + Text(playlist.title) + } + } + } + } + #endif } var editPlaylistButton: some View { @@ -138,7 +212,7 @@ struct PlaylistsView: View { self.showingEditPlaylist = true }) { HStack(spacing: 8) { - Image(systemName: "pencil") + Image(systemName: "slider.horizontal.3") Text("Edit") } } @@ -155,3 +229,10 @@ struct PlaylistsView: View { } } } + +struct PlaylistsView_Provider: PreviewProvider { + static var previews: some View { + PlaylistsView() + .environmentObject(NavigationState()) + } +} diff --git a/Shared/TrendingCountrySelection.swift b/Shared/TrendingCountrySelection.swift index d4dc76ba..a535620d 100644 --- a/Shared/TrendingCountrySelection.swift +++ b/Shared/TrendingCountrySelection.swift @@ -18,7 +18,10 @@ struct TrendingCountrySelection: View { HStack { TextField("Country", text: $query, prompt: Text(TrendingCountrySelection.prompt)) .focused($countryIsFocused) + Button("Done") { selectCountryAndDismiss() } + .keyboardShortcut(.defaultAction) + .keyboardShortcut(.cancelAction) } .padding([.horizontal, .top]) diff --git a/Shared/VideoView.swift b/Shared/VideoView.swift index 405a56a7..b5a1b432 100644 --- a/Shared/VideoView.swift +++ b/Shared/VideoView.swift @@ -188,7 +188,7 @@ struct VideoView: View { } .frame(minWidth: 320, maxWidth: .infinity, minHeight: 180, maxHeight: .infinity) #if os(tvOS) - .frame(minHeight: 320) + .frame(minHeight: layout == .cells ? 320 : 200) #endif .aspectRatio(1.777, contentMode: .fit) } diff --git a/Shared/VideosCellsView.swift b/Shared/VideosCellsView.swift index 56b2c310..989a4127 100644 --- a/Shared/VideosCellsView.swift +++ b/Shared/VideosCellsView.swift @@ -22,11 +22,13 @@ struct VideosCellsView: View { .padding() } .onChange(of: videos) { [videos] newVideos in - guard !videos.isEmpty, let video = newVideos.first else { - return - } + #if !os(tvOS) + guard !videos.isEmpty, let video = newVideos.first else { + return + } - scrollView.scrollTo(video.id, anchor: .top) + scrollView.scrollTo(video.id, anchor: .top) + #endif } #if os(tvOS) .padding(.horizontal, 10) @@ -37,7 +39,7 @@ struct VideosCellsView: View { var items: [GridItem] { #if os(tvOS) - videos.count < 3 ? Array(repeating: GridItem(.fixed(540)), count: videos.count) : adaptiveItem + videos.count < 3 ? Array(repeating: GridItem(.fixed(540)), count: [videos.count, 1].max()!) : adaptiveItem #else adaptiveItem #endif diff --git a/Shared/VideosListView.swift b/Shared/VideosListView.swift index c792bce8..2ba6421d 100644 --- a/Shared/VideosListView.swift +++ b/Shared/VideosListView.swift @@ -13,11 +13,14 @@ struct VideosListView: View { .listRowInsets(EdgeInsets()) } .onChange(of: videos) { videos in - guard let video = videos.first else { - return - } + #if !os(tvOS) - scrollView.scrollTo(video.id, anchor: .top) + guard let video = videos.first else { + return + } + + scrollView.scrollTo(video.id, anchor: .top) + #endif } } } diff --git a/tvOS/PlaylistFormView.swift b/tvOS/PlaylistFormView.swift deleted file mode 100644 index da5fbcf9..00000000 --- a/tvOS/PlaylistFormView.swift +++ /dev/null @@ -1,120 +0,0 @@ -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() - } - } -}