diff --git a/Apple TV/CoverSectionView.swift b/Apple TV/CoverSectionView.swift index 37113421..58e12d2d 100644 --- a/Apple TV/CoverSectionView.swift +++ b/Apple TV/CoverSectionView.swift @@ -3,22 +3,35 @@ import SwiftUI struct CoverSectionView: View { let title: String? - let rowsView: Content + let actionsView: Content let divider: Bool + let inline: Bool - init(_ title: String? = nil, divider: Bool = true, @ViewBuilder rowsView: @escaping () -> Content) { + init(_ title: String? = nil, divider: Bool = true, inline: Bool = false, @ViewBuilder actionsView: @escaping () -> Content) { self.title = title self.divider = divider - self.rowsView = rowsView() + self.inline = inline + self.actionsView = actionsView() } var body: some View { VStack(alignment: .leading) { - if title != nil { + if inline { + HStack { + if title != nil { + sectionTitle + } + + Spacer() + actionsView + } + } else if title != nil { sectionTitle } - rowsView + if !inline { + actionsView + } } if divider { diff --git a/Apple TV/NewPlaylistView.swift b/Apple TV/NewPlaylistView.swift deleted file mode 100644 index 1c7ef71d..00000000 --- a/Apple TV/NewPlaylistView.swift +++ /dev/null @@ -1,74 +0,0 @@ -import SwiftUI -import SwiftyJSON - -struct NewPlaylistView: View { - @State private var name = "" - @State private var visibility = PlaylistVisibility.public - - @State private var valid = false - - @Binding var createdPlaylist: Playlist? - - @Environment(\.dismiss) private var dismiss - - var body: some View { - HStack { - Spacer() - - VStack { - Spacer() - - CoverSectionView("New Playlist") { - CoverSectionRowView("Name") { - TextField("Playlist Name", text: $name, onCommit: validate) - .frame(maxWidth: 450) - } - - CoverSectionRowView("Visibility") { visibilityButton } - } - - CoverSectionRowView { - Button("Create", action: createPlaylistAndDismiss).disabled(!valid) - } - - Spacer() - } - .frame(maxWidth: 800) - - Spacer() - } - .background(.thinMaterial) - .onAppear { - createdPlaylist = nil - } - } - - func validate() { - valid = !name.isEmpty - } - - func createPlaylistAndDismiss() { - let resource = InvidiousAPI.shared.playlists - let body = ["title": name, "privacy": visibility.rawValue] - - resource.request(.post, json: body).onSuccess { response in - if let playlist: Playlist = response.typedContent() { - createdPlaylist = playlist - dismiss() - } - } - } - - var visibilityButton: some View { - Button(self.visibility.name) { - self.visibility = self.visibility.next() - } - .contextMenu { - ForEach(PlaylistVisibility.allCases) { visibility in - Button(visibility.name) { - self.visibility = visibility - } - } - } - } -} diff --git a/Apple TV/PlaylistFormView.swift b/Apple TV/PlaylistFormView.swift new file mode 100644 index 00000000..61e6e909 --- /dev/null +++ b/Apple TV/PlaylistFormView.swift @@ -0,0 +1,120 @@ +import Siesta +import SwiftUI + +struct PlaylistFormView: View { + @State private var name = "" + @State private var visibility = PlaylistVisibility.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(PlaylistVisibility.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() + } + } +} diff --git a/Apple TV/PlaylistsView.swift b/Apple TV/PlaylistsView.swift index 358a64a9..1d48a532 100644 --- a/Apple TV/PlaylistsView.swift +++ b/Apple TV/PlaylistsView.swift @@ -1,14 +1,19 @@ +import Defaults import Siesta import SwiftUI struct PlaylistsView: View { @ObservedObject private var store = Store<[Playlist]>() + @Default(.selectedPlaylistID) private var selectedPlaylistID @State private var selectedPlaylist: Playlist? @State private var showingNewPlaylist = false @State private var createdPlaylist: Playlist? + @State private var showingEditPlaylist = false + @State private var editedPlaylist: Playlist? + var resource: Resource { InvidiousAPI.shared.playlists } @@ -23,6 +28,10 @@ struct PlaylistsView: View { HStack { selectPlaylistButton + if currentPlaylist != nil { + editPlaylistButton + } + newPlaylistButton } .scaleEffect(0.85) @@ -35,20 +44,44 @@ struct PlaylistsView: View { } } .fullScreenCover(isPresented: $showingNewPlaylist, onDismiss: selectCreatedPlaylist) { - NewPlaylistView(createdPlaylist: $createdPlaylist) + PlaylistFormView(playlist: $createdPlaylist) + } + .fullScreenCover(isPresented: $showingEditPlaylist, onDismiss: selectEditedPlaylist) { + PlaylistFormView(playlist: $editedPlaylist) } .onAppear { - resource.loadIfNeeded() + resource.loadIfNeeded()?.onSuccess { _ in + selectPlaylist(selectedPlaylistID) + } } } + func selectPlaylist(_ id: String?) { + selectedPlaylist = store.collection.first { $0.id == id } + selectedPlaylistID = id + } + func selectCreatedPlaylist() { guard createdPlaylist != nil else { return } resource.load().onSuccess { _ in - self.selectedPlaylist = store.collection.first { $0 == createdPlaylist } + self.selectPlaylist(createdPlaylist?.id) + + self.createdPlaylist = nil + } + } + + func selectEditedPlaylist() { + if editedPlaylist == nil { + selectPlaylist(nil) + } + + resource.load().onSuccess { _ in + selectPlaylist(editedPlaylist?.id) + + self.editedPlaylist = nil } } @@ -62,17 +95,26 @@ struct PlaylistsView: View { return } - selectedPlaylist = store.collection.next(after: currentPlaylist!) + selectPlaylist(store.collection.next(after: currentPlaylist!)?.id) } .contextMenu { ForEach(store.collection) { playlist in Button(playlist.title) { - selectedPlaylist = playlist + selectPlaylist(playlist.id) } } } } + var editPlaylistButton: some View { + Button(action: { + self.editedPlaylist = self.currentPlaylist + self.showingEditPlaylist = true + }) { + Image(systemName: "pencil") + } + } + var newPlaylistButton: some View { Button(action: { self.showingNewPlaylist = true }) { Image(systemName: "plus") diff --git a/Model/InvidiousAPI.swift b/Model/InvidiousAPI.swift index 92449b30..a18ea055 100644 --- a/Model/InvidiousAPI.swift +++ b/Model/InvidiousAPI.swift @@ -54,7 +54,7 @@ final class InvidiousAPI: Service { content.json.arrayValue.map(Playlist.init) } - configureTransformer("/auth/playlists", requestMethods: [.post]) { (content: Entity) -> Playlist in + configureTransformer("/auth/playlists", requestMethods: [.post, .patch]) { (content: Entity) -> Playlist in // hacky, to verify if possible to get it in easier way Playlist(JSON(parseJSON: String(data: content.content, encoding: .utf8)!)) } @@ -108,6 +108,10 @@ final class InvidiousAPI: Service { resource("/auth/playlists") } + func playlist(_ id: String) -> Resource { + resource("/auth/playlists/\(id)") + } + func search(_ query: SearchQuery) -> Resource { var resource = resource("/search") .withParam("q", searchQuery(query.query)) diff --git a/Model/Playlist.swift b/Model/Playlist.swift index d2871c07..40d348e9 100644 --- a/Model/Playlist.swift +++ b/Model/Playlist.swift @@ -4,19 +4,19 @@ import SwiftyJSON struct Playlist: Identifiable, Equatable, Hashable { let id: String var title: String - var description: String + var visibility: PlaylistVisibility var videos = [Video]() init(_ json: JSON) { id = json["playlistId"].stringValue title = json["title"].stringValue - description = json["description"].stringValue + visibility = json["isListed"].boolValue ? .public : .private videos = json["videos"].arrayValue.map { Video($0) } } static func == (lhs: Playlist, rhs: Playlist) -> Bool { - lhs.id == rhs.id + lhs.id == rhs.id && lhs.title == rhs.title && lhs.visibility == rhs.visibility } func hash(into hasher: inout Hasher) { diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index d47d9c8b..aee06b7e 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -54,12 +54,12 @@ 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADA269663F1003CB2C6 /* Thumbnail.swift */; }; 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADA269663F1003CB2C6 /* Thumbnail.swift */; }; 373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADA269663F1003CB2C6 /* Thumbnail.swift */; }; - 373CFADF2696F88B003CB2C6 /* NewPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADE2696F861003CB2C6 /* NewPlaylistView.swift */; }; - 373CFAE02696F88B003CB2C6 /* NewPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADE2696F861003CB2C6 /* NewPlaylistView.swift */; }; - 373CFAE12696F88B003CB2C6 /* NewPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFADE2696F861003CB2C6 /* NewPlaylistView.swift */; }; 373CFAE326974812003CB2C6 /* PlaylistVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */; }; 373CFAE426974812003CB2C6 /* PlaylistVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */; }; 373CFAE526974812003CB2C6 /* PlaylistVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */; }; + 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; }; + 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; }; + 373CFAED26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; }; 3741B5302676213400125C5E /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741B52F2676213400125C5E /* PlayerViewController.swift */; }; 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; @@ -229,8 +229,8 @@ 373CFAD2269662AB003CB2C6 /* SearchDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDate.swift; sourceTree = ""; }; 373CFAD6269662CD003CB2C6 /* SearchDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchDuration.swift; sourceTree = ""; }; 373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = ""; }; - 373CFADE2696F861003CB2C6 /* NewPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPlaylistView.swift; sourceTree = ""; }; 373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVisibility.swift; sourceTree = ""; }; + 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = ""; }; 3741B52F2676213400125C5E /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = ""; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; @@ -423,10 +423,10 @@ 37AAF2892673AB89007FC770 /* ChannelView.swift */, 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */, 373CFABD26966115003CB2C6 /* CoverSectionView.swift */, - 373CFADE2696F861003CB2C6 /* NewPlaylistView.swift */, 37B76E95268747C900CE5671 /* OptionsView.swift */, 37D4B1822671681B00C925CA /* PlayerView.swift */, 3741B52F2676213400125C5E /* PlayerViewController.swift */, + 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */, 376578902685490700D4EA09 /* PlaylistsView.swift */, 37AAF27D26737323007FC770 /* PopularVideosView.swift */, 373CFAC52696617C003CB2C6 /* SearchOptionsView.swift */, @@ -753,7 +753,6 @@ 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */, 376578892685471400D4EA09 /* Playlist.swift in Sources */, 373CFADB269663F1003CB2C6 /* Thumbnail.swift in Sources */, - 373CFAE12696F88B003CB2C6 /* NewPlaylistView.swift in Sources */, 37CEE4B52677B628005A1EFE /* StreamType.swift in Sources */, 37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */, 373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */, @@ -778,6 +777,7 @@ 37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, 373CFAD7269662CD003CB2C6 /* SearchDuration.swift in Sources */, 373CFACF26966290003CB2C6 /* SearchSortOrder.swift in Sources */, + 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 377FC7DF267A082200A6BBAF /* VideosListView.swift in Sources */, 372915E62687E3B900F5A35B /* Defaults.swift in Sources */, 37D4B19726717E1500C925CA /* Video.swift in Sources */, @@ -838,7 +838,7 @@ 37D4B0E52671614900C925CA /* PearvidiousApp.swift in Sources */, 373CFAE426974812003CB2C6 /* PlaylistVisibility.swift in Sources */, 37CEE4BA2677B63F005A1EFE /* StreamResolution.swift in Sources */, - 373CFAE02696F88B003CB2C6 /* NewPlaylistView.swift in Sources */, + 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 373CFAD4269662AB003CB2C6 /* SearchDate.swift in Sources */, @@ -886,7 +886,6 @@ 37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, 3765788B2685471400D4EA09 /* Playlist.swift in Sources */, 373CFADD269663F1003CB2C6 /* Thumbnail.swift in Sources */, - 373CFADF2696F88B003CB2C6 /* NewPlaylistView.swift in Sources */, 37C7A1DE267CE9D90010EAD6 /* Profile.swift in Sources */, 3741B5302676213400125C5E /* PlayerViewController.swift in Sources */, 373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */, @@ -911,6 +910,7 @@ 37D4B19926717E1500C925CA /* Video.swift in Sources */, 373CFAD9269662CD003CB2C6 /* SearchDuration.swift in Sources */, 373CFAD126966290003CB2C6 /* SearchSortOrder.swift in Sources */, + 373CFAED26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */, 37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */, 372915E82687E3B900F5A35B /* Defaults.swift in Sources */, diff --git a/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index b4fe4dae..e956de23 100644 --- a/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Pearvidious.xcodeproj/xcuserdata/arek.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -23,49 +23,65 @@ + + + + - - - - - - + + + + + + + + diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 65fc5bc1..3d5dc459 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -11,4 +11,6 @@ extension Defaults.Keys { static let searchDuration = Key("searchDuration", default: nil) static let openVideoID = Key("videoID", default: "") static let showingVideoDetails = Key("showingVideoDetails", default: false) + + static let selectedPlaylistID = Key("selectedPlaylistID") }