Adding/removing videos to/from playlists

This commit is contained in:
Arkadiusz Fal 2021-07-09 16:53:53 +02:00
parent 31bd2f7fe6
commit f397b13720
12 changed files with 218 additions and 17 deletions

View File

@ -0,0 +1,97 @@
import Defaults
import Siesta
import SwiftUI
struct AddToPlaylistView: View {
@ObservedObject private var store = Store<[Playlist]>()
@State private var selectedPlaylist: Playlist?
@Default(.videoIDToAddToPlaylist) private var videoID
@Environment(\.dismiss) private var dismiss
var resource: Resource {
InvidiousAPI.shared.playlists
}
init() {
resource.addObserver(store)
}
var body: some View {
HStack {
Spacer()
VStack {
Spacer()
if !resource.isLoading && store.collection.isEmpty {
CoverSectionView("You have no Playlists", inline: true) {
Text("Open \"Playlists\" tab to create new one")
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
Button("Go back") {
dismiss()
}
.padding()
} else if !store.collection.isEmpty {
CoverSectionView("Add to Playlist", inline: true) { selectPlaylistButton }
CoverSectionRowView {
Button("Add", action: addToPlaylist)
.disabled(currentPlaylist == nil)
}
}
Spacer()
}
.frame(maxWidth: 1200)
Spacer()
}
.background(.thinMaterial)
.onAppear {
resource.loadIfNeeded()?.onSuccess { _ in
selectedPlaylist = store.collection.first
}
}
}
var selectPlaylistButton: some View {
Button(currentPlaylist?.title ?? "Select playlist") {
guard currentPlaylist != nil else {
return
}
self.selectedPlaylist = store.collection.next(after: currentPlaylist!)
}
.contextMenu {
ForEach(store.collection) { playlist in
Button(playlist.title) {
self.selectedPlaylist = playlist
}
}
}
}
var currentPlaylist: Playlist? {
selectedPlaylist ?? store.collection.first
}
func addToPlaylist() {
guard currentPlaylist != nil else {
return
}
let resource = InvidiousAPI.shared.playlistVideos(currentPlaylist!.id)
let body = ["videoId": videoID]
resource.request(.post, json: body).onSuccess { _ in
Defaults.reset(.videoIDToAddToPlaylist)
InvidiousAPI.shared.playlists.load()
dismiss()
}
}
}

View File

@ -6,7 +6,6 @@ struct PlaylistsView: View {
@ObservedObject private var store = Store<[Playlist]>() @ObservedObject private var store = Store<[Playlist]>()
@Default(.selectedPlaylistID) private var selectedPlaylistID @Default(.selectedPlaylistID) private var selectedPlaylistID
@State private var selectedPlaylist: Playlist?
@State private var showingNewPlaylist = false @State private var showingNewPlaylist = false
@State private var createdPlaylist: Playlist? @State private var createdPlaylist: Playlist?
@ -26,18 +25,37 @@ struct PlaylistsView: View {
Section { Section {
VStack(alignment: .center, spacing: 2) { VStack(alignment: .center, spacing: 2) {
HStack { HStack {
selectPlaylistButton if store.collection.isEmpty {
Text("No Playlists")
.foregroundColor(.secondary)
} else {
Text("Current Playlist")
.foregroundColor(.secondary)
selectPlaylistButton
}
if currentPlaylist != nil { if currentPlaylist != nil {
editPlaylistButton editPlaylistButton
} }
newPlaylistButton newPlaylistButton
.padding(.leading, 40)
} }
.scaleEffect(0.85) .scaleEffect(0.85)
if currentPlaylist != nil { if currentPlaylist != nil {
VideosView(videos: currentPlaylist!.videos) 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 { } else {
Spacer() Spacer()
} }
@ -57,7 +75,6 @@ struct PlaylistsView: View {
} }
func selectPlaylist(_ id: String?) { func selectPlaylist(_ id: String?) {
selectedPlaylist = store.collection.first { $0.id == id }
selectedPlaylistID = id selectedPlaylistID = id
} }
@ -86,7 +103,7 @@ struct PlaylistsView: View {
} }
var currentPlaylist: Playlist? { var currentPlaylist: Playlist? {
selectedPlaylist ?? store.collection.first store.collection.first { $0.id == selectedPlaylistID } ?? store.collection.first
} }
var selectPlaylistButton: some View { var selectPlaylistButton: some View {
@ -111,13 +128,19 @@ struct PlaylistsView: View {
self.editedPlaylist = self.currentPlaylist self.editedPlaylist = self.currentPlaylist
self.showingEditPlaylist = true self.showingEditPlaylist = true
}) { }) {
Image(systemName: "pencil") HStack(spacing: 8) {
Image(systemName: "pencil")
Text("Edit")
}
} }
} }
var newPlaylistButton: some View { var newPlaylistButton: some View {
Button(action: { self.showingNewPlaylist = true }) { Button(action: { self.showingNewPlaylist = true }) {
Image(systemName: "plus") HStack(spacing: 8) {
Image(systemName: "plus")
Text("New Playlist")
}
} }
} }
} }

View File

@ -20,7 +20,14 @@ struct TrendingView: View {
Section { Section {
VStack(alignment: .center, spacing: 2) { VStack(alignment: .center, spacing: 2) {
HStack { HStack {
Text("Category")
.foregroundColor(.secondary)
categoryButton categoryButton
Text("Country")
.foregroundColor(.secondary)
countryFlag countryFlag
countryButton countryButton
} }

View File

@ -9,6 +9,9 @@ struct VideoContextMenuView: View {
@Default(.openVideoID) var openVideoID @Default(.openVideoID) var openVideoID
@Default(.showingVideoDetails) var showDetails @Default(.showingVideoDetails) var showDetails
@Default(.showingAddToPlaylist) var showingAddToPlaylist
@Default(.videoIDToAddToPlaylist) var videoIDToAddToPlaylist
var body: some View { var body: some View {
if tabSelection == .channel { if tabSelection == .channel {
closeChannelButton(from: video) closeChannelButton(from: video)
@ -16,9 +19,12 @@ struct VideoContextMenuView: View {
openChannelButton(from: video) openChannelButton(from: video)
} }
Button("Open video details") { openVideoDetailsButton
openVideoID = video.id
showDetails = true if tabSelection == .playlists {
removeFromPlaylistButton
} else {
addToPlaylistButton
} }
} }
@ -34,4 +40,27 @@ struct VideoContextMenuView: View {
Defaults.reset(.openChannel) Defaults.reset(.openChannel)
} }
} }
var openVideoDetailsButton: some View {
Button("Open video details") {
openVideoID = video.id
showDetails = true
}
}
var addToPlaylistButton: some View {
Button("Add to playlist...") {
videoIDToAddToPlaylist = video.id
showingAddToPlaylist = true
}
}
var removeFromPlaylistButton: some View {
Button("Remove from playlist", role: .destructive) {
let resource = InvidiousAPI.shared.playlistVideo(Defaults[.selectedPlaylistID]!, video.indexID!)
resource.request(.delete).onSuccess { _ in
InvidiousAPI.shared.playlists.load()
}
}
}
} }

View File

@ -40,7 +40,7 @@ struct VideoDetailsView: View {
HStack { HStack {
NavigationLink(destination: PlayerView(id: video.id)) { NavigationLink(destination: PlayerView(id: video.id)) {
HStack(spacing: 10) { HStack(spacing: 8) {
Image(systemName: "play.rectangle.fill") Image(systemName: "play.rectangle.fill")
Text("Play") Text("Play")

View File

@ -7,15 +7,20 @@ struct VideosView: View {
@Default(.layout) var layout @Default(.layout) var layout
@Default(.tabSelection) var tabSelection @Default(.tabSelection) var tabSelection
@Default(.showingAddToPlaylist) var showingAddToPlaylist
var videos: [Video] var videos: [Video]
var body: some View { var body: some View {
Group { VStack {
if layout == .cells { if layout == .cells {
VideosCellsView(videos: videos, columns: self.profile.cellsColumns) VideosCellsView(videos: videos, columns: self.profile.cellsColumns)
} else { } else {
VideosListView(videos: videos) VideosListView(videos: videos)
} }
} }
.fullScreenCover(isPresented: $showingAddToPlaylist) {
AddToPlaylistView()
}
} }
} }

View File

@ -112,6 +112,14 @@ final class InvidiousAPI: Service {
resource("/auth/playlists/\(id)") resource("/auth/playlists/\(id)")
} }
func playlistVideos(_ id: String) -> Resource {
playlist(id).child("videos")
}
func playlistVideo(_ playlistID: String, _ videoID: String) -> Resource {
playlist(playlistID).child("videos").child(videoID)
}
func search(_ query: SearchQuery) -> Resource { func search(_ query: SearchQuery) -> Resource {
var resource = resource("/search") var resource = resource("/search")
.withParam("q", searchQuery(query.query)) .withParam("q", searchQuery(query.query))

View File

@ -6,17 +6,20 @@ struct Playlist: Identifiable, Equatable, Hashable {
var title: String var title: String
var visibility: PlaylistVisibility var visibility: PlaylistVisibility
var updated: TimeInterval
var videos = [Video]() var videos = [Video]()
init(_ json: JSON) { init(_ json: JSON) {
id = json["playlistId"].stringValue id = json["playlistId"].stringValue
title = json["title"].stringValue title = json["title"].stringValue
visibility = json["isListed"].boolValue ? .public : .private visibility = json["isListed"].boolValue ? .public : .private
updated = json["updated"].doubleValue
videos = json["videos"].arrayValue.map { Video($0) } videos = json["videos"].arrayValue.map { Video($0) }
} }
static func == (lhs: Playlist, rhs: Playlist) -> Bool { static func == (lhs: Playlist, rhs: Playlist) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title && lhs.visibility == rhs.visibility lhs.id == rhs.id && lhs.updated == rhs.updated
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {

View File

@ -0,0 +1,8 @@
//
// PlaylistVideo.swift
// Pearvidious
//
// Created by Arkadiusz Fal on 09/07/2021.
//
import Foundation

View File

@ -15,10 +15,21 @@ struct Video: Identifiable {
var description: String var description: String
var genre: String var genre: String
let indexID: String?
var streams = [Stream]() var streams = [Stream]()
init(_ json: JSON) { init(_ json: JSON) {
id = json["videoId"].stringValue let videoID = json["videoId"].stringValue
if let id = json["indexId"].string {
indexID = id
self.id = videoID + id
} else {
indexID = nil
id = videoID
}
title = json["title"].stringValue title = json["title"].stringValue
author = json["author"].stringValue author = json["author"].stringValue
length = json["lengthSeconds"].doubleValue length = json["lengthSeconds"].doubleValue

View File

@ -60,6 +60,9 @@
373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; }; 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; };
373CFAEC26975CBF003CB2C6 /* 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 */; }; 373CFAED26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */; };
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3741B52F2676213400125C5E /* PlayerViewController.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 */; }; 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 */; }; 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; };
@ -231,6 +234,7 @@
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; }; 373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVisibility.swift; sourceTree = "<group>"; }; 373CFAE226974812003CB2C6 /* PlaylistVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistVisibility.swift; sourceTree = "<group>"; };
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; }; 373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToPlaylistView.swift; sourceTree = "<group>"; };
3741B52F2676213400125C5E /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; }; 3741B52F2676213400125C5E /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
@ -420,6 +424,7 @@
37D4B159267164AE00C925CA /* Apple TV */ = { 37D4B159267164AE00C925CA /* Apple TV */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */,
37AAF2892673AB89007FC770 /* ChannelView.swift */, 37AAF2892673AB89007FC770 /* ChannelView.swift */,
373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */, 373CFAC126966159003CB2C6 /* CoverSectionRowView.swift */,
373CFABD26966115003CB2C6 /* CoverSectionView.swift */, 373CFABD26966115003CB2C6 /* CoverSectionView.swift */,
@ -442,8 +447,8 @@
37F4AE7126828F0900BD60EA /* VideosCellsView.swift */, 37F4AE7126828F0900BD60EA /* VideosCellsView.swift */,
37AAF29926740A01007FC770 /* VideosListView.swift */, 37AAF29926740A01007FC770 /* VideosListView.swift */,
371231832683E62F0000B307 /* VideosView.swift */, 371231832683E62F0000B307 /* VideosView.swift */,
37D4B15E267164AF00C925CA /* Assets.xcassets */,
37D4B1AE26729DEB00C925CA /* Info.plist */, 37D4B1AE26729DEB00C925CA /* Info.plist */,
37D4B15E267164AF00C925CA /* Assets.xcassets */,
); );
path = "Apple TV"; path = "Apple TV";
sourceTree = "<group>"; sourceTree = "<group>";
@ -757,6 +762,7 @@
37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */, 37C7A1DC267CE9D90010EAD6 /* Profile.swift in Sources */,
373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */, 373CFAC026966149003CB2C6 /* CoverSectionView.swift in Sources */,
3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */, 3714166F267A8ACC006CA35D /* TrendingView.swift in Sources */,
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoListRowView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoListRowView.swift in Sources */,
37AAF29026740715007FC770 /* Channel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */,
37AAF2942674086B007FC770 /* TabSelection.swift in Sources */, 37AAF2942674086B007FC770 /* TabSelection.swift in Sources */,
@ -843,6 +849,7 @@
37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
373CFAD4269662AB003CB2C6 /* SearchDate.swift in Sources */, 373CFAD4269662AB003CB2C6 /* SearchDate.swift in Sources */,
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -890,6 +897,7 @@
3741B5302676213400125C5E /* PlayerViewController.swift in Sources */, 3741B5302676213400125C5E /* PlayerViewController.swift in Sources */,
373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */, 373CFABE26966148003CB2C6 /* CoverSectionView.swift in Sources */,
37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */, 37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */,
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
37D4B18E26717B3800C925CA /* VideoListRowView.swift in Sources */, 37D4B18E26717B3800C925CA /* VideoListRowView.swift in Sources */,
37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */, 37AAF27E26737323007FC770 /* PopularVideosView.swift in Sources */,
37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */, 37AAF29A26740A01007FC770 /* VideosListView.swift in Sources */,

View File

@ -7,10 +7,12 @@ extension Defaults.Keys {
static let openChannel = Key<Channel?>("openChannel") static let openChannel = Key<Channel?>("openChannel")
static let searchSortOrder = Key<SearchSortOrder>("searchSortOrder", default: .relevance) static let searchSortOrder = Key<SearchSortOrder>("searchSortOrder", default: .relevance)
static let searchDate = Key<SearchDate?>("searchDate", default: nil) static let searchDate = Key<SearchDate?>("searchDate")
static let searchDuration = Key<SearchDuration?>("searchDuration", default: nil) static let searchDuration = Key<SearchDuration?>("searchDuration")
static let openVideoID = Key<String>("videoID", default: "") static let openVideoID = Key<String>("videoID", default: "")
static let showingVideoDetails = Key<Bool>("showingVideoDetails", default: false) static let showingVideoDetails = Key<Bool>("showingVideoDetails", default: false)
static let selectedPlaylistID = Key<String?>("selectedPlaylistID") static let selectedPlaylistID = Key<String?>("selectedPlaylistID")
static let showingAddToPlaylist = Key<Bool>("showingAddToPlaylist", default: false)
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
} }