From 2a7bf560358bab61ba25d258fd634a560fca4ca6 Mon Sep 17 00:00:00 2001 From: PikachuEXE Date: Tue, 16 May 2023 14:30:45 +0800 Subject: [PATCH 001/236] (Replicate) Push up batch 1 of playlist functionality --- src/datastores/handlers/base.js | 4 + src/datastores/handlers/electron.js | 7 + src/datastores/handlers/web.js | 4 + src/main/index.js | 9 + .../ft-list-playlist/ft-list-playlist.js | 15 +- .../components/playlist-info/playlist-info.js | 158 +++++++++++------- .../playlist-info/playlist-info.vue | 88 +++++++++- .../watch-video-playlist.js | 22 +++ .../watch-video-playlist.vue | 5 +- src/renderer/store/modules/playlists.js | 70 +++++++- src/renderer/views/Playlist/Playlist.css | 4 + src/renderer/views/Playlist/Playlist.js | 97 +++++++++-- src/renderer/views/Playlist/Playlist.vue | 81 +++++---- .../views/UserPlaylists/UserPlaylists.js | 70 ++++---- .../views/UserPlaylists/UserPlaylists.vue | 5 - 15 files changed, 492 insertions(+), 147 deletions(-) diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 11eebc32e2d0e..f5595715afcc9 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -108,6 +108,10 @@ class Playlists { return db.playlists.find({}) } + static upsert(playlist) { + return db.profiles.update({ _id: playlist._id }, playlist, { upsert: true }) + } + static upsertVideoByPlaylistName(playlistName, videoData) { return db.playlists.update( { playlistName }, diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index ddb90ff82434e..98d76b5435bbe 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -126,6 +126,13 @@ class Playlists { ) } + static upsert(playlist) { + return ipcRenderer.invoke( + IpcChannels.DB_PLAYLISTS, + { action: DBActions.GENERAL.UPSERT, data: playlist } + ) + } + static upsertVideoByPlaylistName(playlistName, videoData) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index a81eb305d1dd9..e781e01b10a16 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -81,6 +81,10 @@ class Playlists { return baseHandlers.playlists.find() } + static upsert(playlist) { + return baseHandlers.playlists.upsert(playlist) + } + static upsertVideoByPlaylistName(playlistName, videoData) { return baseHandlers.playlists.upsertVideoByPlaylistName(playlistName, videoData) } diff --git a/src/main/index.js b/src/main/index.js index 9d44efd68020f..bffdf946c7f23 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -897,6 +897,15 @@ function runApp() { case DBActions.GENERAL.FIND: return await baseHandlers.playlists.find() + case DBActions.GENERAL.UPSERT: + await baseHandlers.playlists.upsert(data) + syncOtherWindows( + IpcChannels.SYNC_PLAYLISTS, + event, + { event: SyncEvents.PLAYLISTS.UPSERT, data } + ) + return null + case DBActions.PLAYLISTS.UPSERT_VIDEO: await baseHandlers.playlists.upsertVideoByPlaylistName(data.playlistName, data.videoData) syncOtherWindows( diff --git a/src/renderer/components/ft-list-playlist/ft-list-playlist.js b/src/renderer/components/ft-list-playlist/ft-list-playlist.js index 5eb0532e5bc48..6788a69759531 100644 --- a/src/renderer/components/ft-list-playlist/ft-list-playlist.js +++ b/src/renderer/components/ft-list-playlist/ft-list-playlist.js @@ -45,7 +45,9 @@ export default defineComponent({ } }, created: function () { - if (this.data.dataSource === 'local') { + if (this.data._id != null) { + this.parseUserData() + } else if (this.data.dataSource === 'local') { this.parseLocalData() } else { this.parseInvidiousData() @@ -87,6 +89,17 @@ export default defineComponent({ this.videoCount = this.data.videoCount }, + parseUserData: function () { + this.title = this.data.title + if (this.data.videos.length > 0) { + this.thumbnail = `https://i.ytimg.com/vi/${this.data.videos[0].videoId}/mqdefault.jpg` + } else { + this.thumbnail = 'https://i.ytimg.com/vi/aaaaaa/mqdefault.jpg' + } + this.channelName = '' + this.videoCount = this.data.videoCount + }, + ...mapActions([ 'openInExternalPlayer' ]) diff --git a/src/renderer/components/playlist-info/playlist-info.js b/src/renderer/components/playlist-info/playlist-info.js index 907417ba58431..2b5d0656749f9 100644 --- a/src/renderer/components/playlist-info/playlist-info.js +++ b/src/renderer/components/playlist-info/playlist-info.js @@ -1,35 +1,73 @@ import { defineComponent } from 'vue' +import { mapActions } from 'vuex' import FtShareButton from '../ft-share-button/ft-share-button.vue' -import { copyToClipboard, formatNumber, openExternalLink } from '../../helpers/utils' +import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' +import FtIconButton from '../ft-icon-button/ft-icon-button.vue' +import FtInput from '../ft-input/ft-input.vue' export default defineComponent({ name: 'PlaylistInfo', components: { - 'ft-share-button': FtShareButton + 'ft-share-button': FtShareButton, + 'ft-flex-box': FtFlexBox, + 'ft-icon-button': FtIconButton, + 'ft-input': FtInput, }, props: { - data: { - type: Object, - required: true - } + id: { + type: String, + required: true, + }, + firstVideoId: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + channelThumbnail: { + type: String, + required: true, + }, + channelName: { + type: String, + required: true, + }, + channelId: { + type: String, + required: true, + }, + videoCount: { + type: Number, + required: true, + }, + viewCount: { + type: Number, + required: true, + }, + lastUpdated: { + type: String, + default: undefined, + }, + description: { + type: String, + required: true, + }, + infoSource: { + type: String, + required: true, + }, }, data: function () { return { - id: '', - firstVideoId: '', - title: '', - channelThumbnail: '', - channelName: '', - channelId: '', - videoCount: 0, - viewCount: 0, - lastUpdated: '', - description: '', - infoSource: '' + editMode: false, + newTitle: '', + newDescription: '', } }, computed: { - hideSharingActions: function() { + hideSharingActions: function () { return this.$store.getters.getHideSharingActions }, @@ -49,6 +87,14 @@ export default defineComponent({ return this.$store.getters.getHideVideoViews }, + userPlaylists: function () { + return this.$store.getters.getAllPlaylists + }, + + selectedPlaylist: function () { + return this.userPlaylists.find((playlist) => playlist._id === this.id) + }, + thumbnail: function () { let baseUrl if (this.backendPreference === 'invidious') { @@ -67,49 +113,49 @@ export default defineComponent({ default: return `${baseUrl}/vi/${this.firstVideoId}/mqdefault.jpg` } - } + }, }, mounted: function () { - this.id = this.data.id - this.firstVideoId = this.data.firstVideoId - this.title = this.data.title - this.channelName = this.data.channelName - this.channelThumbnail = this.data.channelThumbnail - this.channelId = this.data.channelId - this.uploadedTime = this.data.uploaded_at - this.description = this.data.description - this.infoSource = this.data.infoSource + this.newTitle = this.title + this.newDescription = this.description // Causes errors if not put inside of a check - if (typeof (this.data.viewCount) !== 'undefined' && !isNaN(this.data.viewCount)) { - this.viewCount = this.hideViews ? null : formatNumber(this.data.viewCount) - } - - if (typeof (this.data.videoCount) !== 'undefined' && !isNaN(this.data.videoCount)) { - this.videoCount = formatNumber(this.data.videoCount) - } - - this.lastUpdated = this.data.lastUpdated + // if ( + // typeof this.data.viewCount !== 'undefined' && + // !isNaN(this.data.viewCount) + // ) { + // this.viewCount = this.hideViews ? null : formatNumber(this.data.viewCount) + // } + // + // if ( + // typeof this.data.videoCount !== 'undefined' && + // !isNaN(this.data.videoCount) + // ) { + // this.videoCount = formatNumber(this.data.videoCount) + // } + // + // this.lastUpdated = this.data.lastUpdated }, methods: { - sharePlaylist: function (method) { - const youtubeUrl = `https://youtube.com/playlist?list=${this.id}` - const invidiousUrl = `${this.currentInvidiousInstance}/playlist?list=${this.id}` - - switch (method) { - case 'copyYoutube': - copyToClipboard(youtubeUrl, { messageOnSuccess: this.$t('Share.YouTube URL copied to clipboard') }) - break - case 'openYoutube': - openExternalLink(youtubeUrl) - break - case 'copyInvidious': - copyToClipboard(invidiousUrl, { messageOnSuccess: this.$t('Share.Invidious URL copied to clipboard') }) - break - case 'openInvidious': - openExternalLink(invidiousUrl) - break + savePlaylistInfo: function () { + const playlist = { + playlistName: this.newTitle, + protected: this.selectedPlaylist.protected, + removeOnWatched: this.selectedPlaylist.removeOnWatched, + description: this.newDescription, + videos: this.selectedPlaylist.videos, + _id: this.id, } - } - } + this.updatePlaylist(playlist) + this.cancelEditMode() + }, + + cancelEditMode: function () { + this.newTitle = this.title + this.newDescription = this.description + this.editMode = false + }, + + ...mapActions(['updatePlaylist']), + }, }) diff --git a/src/renderer/components/playlist-info/playlist-info.vue b/src/renderer/components/playlist-info/playlist-info.vue index d7a2083de7c37..fd5cc613d6a34 100644 --- a/src/renderer/components/playlist-info/playlist-info.vue +++ b/src/renderer/components/playlist-info/playlist-info.vue @@ -18,19 +18,45 @@
-

+ +

{{ title }}

- {{ videoCount }} {{ $t("Playlist.Videos") }} - {{ viewCount }} {{ $t("Playlist.Views") }} - - - {{ $t("Playlist.Last Updated On") }} + {{ videoCount }} {{ $t("Playlist.Videos") }} + + - {{ viewCount }} {{ $t("Playlist.Views") }} + + + - + + {{ $t("Playlist.Last Updated On") }} + + {{ lastUpdated }} - {{ lastUpdated }}

+

@@ -41,6 +67,7 @@ class="channelShareWrapper" > @@ -56,6 +83,57 @@ +
+ + + + + + + + + + + + + + + + + + + + playlist._id === this.playlistId) + }, + currentVideoIndex: function () { const index = this.playlistItems.findIndex((item) => { if (typeof item.videoId !== 'undefined') { @@ -135,6 +143,8 @@ export default defineComponent({ if (cachedPlaylist?.id === this.playlistId) { this.loadCachedPlaylistInformation(cachedPlaylist) + } else if (this.selectedPlaylist != null) { + this.parseUserPlaylist(this.selectedPlaylist) } else if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') { this.getPlaylistInformationInvidious() } else { @@ -390,6 +400,18 @@ export default defineComponent({ }) }, + parseUserPlaylist: function (playlist) { + this.playlistTitle = playlist.title + this.videoCount = playlist.videoCount + this.channelName = playlist.author ? playlist.author.name : '' + this.channelThumbnail = playlist.author ? playlist.author.bestAvatar.url : '' + this.channelId = playlist.author ? playlist.author.channelID : '' + + this.playlistItems = playlist.videos + + this.isLoading = false + }, + shufflePlaylistItems: function () { // Prevents the array from affecting the original object const remainingItems = [].concat(this.playlistItems) diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue index 4c2e23db8c0f7..d3053c621ce9c 100644 --- a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue +++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue @@ -15,15 +15,16 @@ - {{ channelName }} + {{ channelName }} - - - {{ currentVideoIndex }} / {{ playlistVideoCount }} + {{ currentVideoIndex }} / {{ playlistVideoCount }} >') + console.log({ payload }) if (payload.length === 0) { commit('setAllPlaylists', state.playlists) dispatch('addPlaylists', payload) + dispatch('addPlaylists', state.playlists) } else { + const findFavorites = payload.filter((playlist) => { + return playlist.playlistName === 'Favorites' || playlist._id === 'favorites' + }) + const findWatchLater = payload.filter((playlist) => { + return playlist.playlistName === 'Watch Later' || playlist._id === 'watchLater' + }) + + if (findFavorites.length === 0) { + dispatch('addPlaylist', state.playlists[0]) + payload.push(state.playlists[0]) + } else { + const favoritesPlaylist = findFavorites[0] + + if (favoritesPlaylist._id !== 'favorites') { + const oldId = favoritesPlaylist._id + favoritesPlaylist._id = 'favorites' + dispatch('addPlaylist', favoritesPlaylist) + dispatch('removePlaylist', oldId) + } + } + + if (findWatchLater.length === 0) { + dispatch('addPlaylist', state.playlists[1]) + payload.push(state.playlists[1]) + } else { + const watchLaterPlaylist = findFavorites[0] + + if (watchLaterPlaylist._id !== 'favorites') { + const oldId = watchLaterPlaylist._id + watchLaterPlaylist._id = 'favorites' + dispatch('addPlaylist', watchLaterPlaylist) + dispatch('removePlaylist', oldId) + } + } + commit('setAllPlaylists', payload) } } catch (errMessage) { @@ -142,6 +194,18 @@ const mutations = { state.playlists = state.playlists.concat(payload) }, + upsertPlaylistToList(state, updatedPlaylist) { + const i = state.playlists.findIndex((p) => { + return p._id === updatedPlaylist._id + }) + + if (i === -1) { + state.playlists.push(updatedPlaylist) + } else { + state.playlists.splice(i, 1, updatedPlaylist) + } + }, + addVideo(state, payload) { const playlist = state.playlists.find(playlist => playlist.playlistName === payload.playlistName) if (playlist) { diff --git a/src/renderer/views/Playlist/Playlist.css b/src/renderer/views/Playlist/Playlist.css index f762bfc71f93d..7b2414b74014a 100644 --- a/src/renderer/views/Playlist/Playlist.css +++ b/src/renderer/views/Playlist/Playlist.css @@ -70,3 +70,7 @@ max-width: 35vw; } } + +.message { + color: var(--tertiary-text-color); +} diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js index 8551c4f27289f..0c98dd84639d8 100644 --- a/src/renderer/views/Playlist/Playlist.js +++ b/src/renderer/views/Playlist/Playlist.js @@ -36,11 +36,20 @@ export default defineComponent({ data: function () { return { isLoading: false, - playlistId: null, - infoData: {}, + playlistId: '', + playlistTitle: '', + playlistDescription: '', + firstVideoId: '', + viewCount: 0, + videoCount: 0, + lastUpdated: undefined, + channelName: '', + channelThumbnail: '', + channelId: '', + infoSource: 'local', playlistItems: [], continuationData: null, - isLoadingMore: false + isLoadingMore: false, } }, computed: { @@ -55,21 +64,33 @@ export default defineComponent({ }, currentLocale: function () { return this.$i18n.locale.replace('_', '-') - } + }, + userPlaylists: function () { + return this.$store.getters.getAllPlaylists + }, + selectedPlaylist: function () { + return this.userPlaylists.find(playlist => playlist._id === this.playlistId) + }, }, watch: { $route () { // react to route changes... - this.getPlaylist() + this.getPlaylistInfo() } }, mounted: function () { - this.getPlaylist() + this.getPlaylistInfo() }, methods: { - getPlaylist: function () { + getPlaylistInfo: function () { + this.isLoading = true this.playlistId = this.$route.params.id + if (this.selectedPlaylist != null) { + this.parseUserPlaylist(this.selectedPlaylist) + return + } + switch (this.backendPreference) { case 'local': this.getPlaylistLocal() @@ -80,8 +101,6 @@ export default defineComponent({ } }, getPlaylistLocal: function () { - this.isLoading = true - getLocalPlaylist(this.playlistId).then((result) => { this.infoData = { id: this.playlistId, @@ -97,10 +116,22 @@ export default defineComponent({ infoSource: 'local' } + this.playlistId = result.id + this.playlistTitle = result.title + this.playlistDescription = result.info.description ?? '' + this.firstVideoId = result.items[0].id + this.viewCount = extractNumberFromString(result.info.views) + this.videoCount = extractNumberFromString(result.info.total_items) + this.lastUpdated = result.info.last_updated ?? '' + this.channelName = result.info.author?.name ?? '' + this.channelThumbnail = result.info.author?.best_thumbnail?.url ?? '' + this.channelId = result.info.author?.id + this.infoSource = 'local' + this.updateSubscriptionDetails({ - channelThumbnailUrl: this.infoData.channelThumbnail, - channelName: this.infoData.channelName, - channelId: this.infoData.channelId + channelThumbnailUrl: this.channelThumbnail, + channelName: this.channelName, + channelId: this.channelId }) this.playlistItems = result.items.map(parseLocalPlaylistVideo) @@ -122,8 +153,6 @@ export default defineComponent({ }, getPlaylistInvidious: function () { - this.isLoading = true - invidiousGetPlaylistInfo(this.playlistId).then((result) => { this.infoData = { id: result.playlistId, @@ -138,14 +167,25 @@ export default defineComponent({ infoSource: 'invidious' } + this.id = result.playlistId + this.title = result.title + this.description = result.description + this.firstVideoId = result.videos[0].videoId + this.viewCount = result.viewCount + this.videoCount = result.videoCount + this.channelName = result.author + this.channelThumbnail = youtubeImageUrlToInvidious(result.authorThumbnails[2].url, this.currentInvidiousInstance) + this.channelId = result.authorId + this.infoSource = 'invidious' + this.updateSubscriptionDetails({ channelThumbnailUrl: result.authorThumbnails[2].url, - channelName: this.infoData.channelName, - channelId: this.infoData.channelId + channelName: this.channelName, + channelId: this.channelId }) const dateString = new Date(result.updated * 1000) - this.infoData.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' }) + this.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' }) this.playlistItems = this.playlistItems.concat(result.videos) @@ -162,6 +202,29 @@ export default defineComponent({ }) }, + parseUserPlaylist: function (playlist) { + this.playlistId = playlist._id + this.playlistTitle = playlist.title + this.playlistDescription = playlist.description + + if (playlist.videos.length > 0) { + this.firstVideoId = playlist.videos[0].videoId + } else { + this.firstVideoId = '' + } + this.viewCount = 0 + this.videoCount = playlist.videoCount + this.lastUpdated = undefined + this.channelName = playlist.author ? playlist.author.name : '' + this.channelThumbnail = playlist.author ? playlist.author.bestAvatar.url : '' + this.channelId = playlist.author ? playlist.author.channelID : '' + this.infoSource = 'user' + + this.playlistItems = playlist.videos + + this.isLoading = false + }, + getNextPage: function () { switch (this.infoData.infoSource) { case 'local': diff --git a/src/renderer/views/Playlist/Playlist.vue b/src/renderer/views/Playlist/Playlist.vue index b7d60a8c774d7..db1abb3ae4aaf 100644 --- a/src/renderer/views/Playlist/Playlist.vue +++ b/src/renderer/views/Playlist/Playlist.vue @@ -7,7 +7,17 @@ @@ -15,40 +25,51 @@ v-if="!isLoading" class="playlistItems" > -

-

- {{ index + 1 }} -

- -
+

+ {{ index + 1 }} +

+ + + + + +
+ +
+ - +

+ This playlist currently has no videos. +

-
- -
diff --git a/src/renderer/views/UserPlaylists/UserPlaylists.js b/src/renderer/views/UserPlaylists/UserPlaylists.js index b3477dbc13657..ff57605d552da 100644 --- a/src/renderer/views/UserPlaylists/UserPlaylists.js +++ b/src/renderer/views/UserPlaylists/UserPlaylists.js @@ -34,9 +34,23 @@ export default defineComponent({ return this.$store.getters.getFavorites }, + allPlaylists: function () { + return this.$store.getters.getAllPlaylists.map((playlist) => { + playlist.title = playlist.playlistName + playlist.type = 'playlist' + playlist.thumbnail = '' + playlist.channelName = '' + playlist.channelId = '' + playlist.playlistId = '' + playlist.description = playlist.description ? playlist.description : '' + playlist.videoCount = playlist.videos.length + return playlist + }) + }, + fullData: function () { - const data = [].concat(this.favoritesPlaylist.videos).reverse() - if (this.favoritesPlaylist.videos.length < this.dataLimit) { + const data = this.allPlaylists + if (this.allPlaylists.length < this.dataLimit) { return data } else { return data.slice(0, this.dataLimit) @@ -62,7 +76,7 @@ export default defineComponent({ this.activeData = this.fullData - if (this.activeData.length < this.favoritesPlaylist.videos.length) { + if (this.activeData.length < this.allPlaylists.length) { this.showLoadMoreButton = true } else { this.showLoadMoreButton = false @@ -86,31 +100,31 @@ export default defineComponent({ this.filterPlaylistDebounce() }, filterPlaylist: function() { - if (this.query === '') { - this.activeData = this.fullData - if (this.activeData.length < this.favoritesPlaylist.videos.length) { - this.showLoadMoreButton = true - } else { - this.showLoadMoreButton = false - } - } else { - const lowerCaseQuery = this.query.toLowerCase() - const filteredQuery = this.favoritesPlaylist.videos.filter((video) => { - if (typeof (video.title) !== 'string' || typeof (video.author) !== 'string') { - return false - } else { - return video.title.toLowerCase().includes(lowerCaseQuery) || video.author.toLowerCase().includes(lowerCaseQuery) - } - }).sort((a, b) => { - return b.timeAdded - a.timeAdded - }) - if (filteredQuery.length <= this.searchDataLimit) { - this.showLoadMoreButton = false - } else { - this.showLoadMoreButton = true - } - this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit) - } + // if (this.query === '') { + // this.activeData = this.fullData + // if (this.activeData.length < this.allPlaylists.length) { + // this.showLoadMoreButton = true + // } else { + // this.showLoadMoreButton = false + // } + // } else { + // const lowerCaseQuery = this.query.toLowerCase() + // const filteredQuery = this.favoritesPlaylist.videos.filter((video) => { + // if (typeof (video.title) !== 'string' || typeof (video.author) !== 'string') { + // return false + // } else { + // return video.title.toLowerCase().includes(lowerCaseQuery) || video.author.toLowerCase().includes(lowerCaseQuery) + // } + // }).sort((a, b) => { + // return b.timeAdded - a.timeAdded + // }) + // if (filteredQuery.length <= this.searchDataLimit) { + // this.showLoadMoreButton = false + // } else { + // this.showLoadMoreButton = true + // } + // this.activeData = filteredQuery.length < this.searchDataLimit ? filteredQuery : filteredQuery.slice(0, this.searchDataLimit) + // } }, } }) diff --git a/src/renderer/views/UserPlaylists/UserPlaylists.vue b/src/renderer/views/UserPlaylists/UserPlaylists.vue index 09097ef8f2186..d2f8660a2491c 100644 --- a/src/renderer/views/UserPlaylists/UserPlaylists.vue +++ b/src/renderer/views/UserPlaylists/UserPlaylists.vue @@ -10,11 +10,6 @@ >

{{ $t("User Playlists.Your Playlists") }} -

Date: Tue, 16 May 2023 15:20:02 +0800 Subject: [PATCH 002/236] (Replicate) Push up batch two of playlists functionality --- src/constants.js | 1 + src/datastores/handlers/base.js | 18 +- src/datastores/handlers/electron.js | 18 +- src/datastores/handlers/web.js | 16 +- src/main/index.js | 10 +- .../components/ft-list-video/ft-list-video.js | 57 +++- .../ft-list-video/ft-list-video.vue | 11 +- .../ft-playlist-add-video-prompt.css | 23 ++ .../ft-playlist-add-video-prompt.js | 88 ++++++ .../ft-playlist-add-video-prompt.vue | 34 +++ .../ft-playlist-selector.js | 62 +++++ .../ft-playlist-selector.scss | 254 ++++++++++++++++++ .../ft-playlist-selector.vue | 41 +++ .../components/ft-prompt/ft-prompt.css | 6 +- .../components/playlist-info/playlist-info.js | 10 +- .../playlist-info/playlist-info.vue | 4 +- src/renderer/scss-partials/_ft-list-item.scss | 9 + src/renderer/store/modules/playlists.js | 41 ++- src/renderer/store/modules/settings.js | 4 + src/renderer/store/modules/utils.js | 27 ++ src/renderer/views/Playlist/Playlist.js | 11 +- .../views/UserPlaylists/UserPlaylists.js | 8 +- 22 files changed, 674 insertions(+), 79 deletions(-) create mode 100644 src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.css create mode 100644 src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.js create mode 100644 src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue create mode 100644 src/renderer/components/ft-playlist-selector/ft-playlist-selector.js create mode 100644 src/renderer/components/ft-playlist-selector/ft-playlist-selector.scss create mode 100644 src/renderer/components/ft-playlist-selector/ft-playlist-selector.vue diff --git a/src/constants.js b/src/constants.js index bd80da2ffdc17..cc82cdcce9cf0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -65,6 +65,7 @@ const SyncEvents = { }, PLAYLISTS: { + UPSERT: 'sync-playlists-upsert', UPSERT_VIDEO: 'sync-playlists-upsert-video', DELETE_VIDEO: 'sync-playlists-delete-video' } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index f5595715afcc9..f9f110e3f7e7c 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -109,12 +109,12 @@ class Playlists { } static upsert(playlist) { - return db.profiles.update({ _id: playlist._id }, playlist, { upsert: true }) + return db.playlists.update({ _id: playlist._id }, playlist, { upsert: true }) } - static upsertVideoByPlaylistName(playlistName, videoData) { + static upsertVideoByPlaylistId(_id, videoData) { return db.playlists.update( - { playlistName }, + { _id }, { $push: { videos: videoData } }, { upsert: true } ) @@ -132,25 +132,25 @@ class Playlists { return db.playlists.remove({ _id, protected: { $ne: true } }) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { + static deleteVideoIdByPlaylistId(_id, videoId) { return db.playlists.update( - { playlistName }, + { _id }, { $pull: { videos: { videoId } } }, { upsert: true } ) } - static deleteVideoIdsByPlaylistName(playlistName, videoIds) { + static deleteVideoIdsByPlaylistId(_id, videoIds) { return db.playlists.update( - { playlistName }, + { _id }, { $pull: { videos: { $in: videoIds } } }, { upsert: true } ) } - static deleteAllVideosByPlaylistName(playlistName) { + static deleteAllVideosByPlaylistId(_id) { return db.playlists.update( - { playlistName }, + { _id }, { $set: { videos: [] } }, { upsert: true } ) diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index 98d76b5435bbe..caba5752ff406 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -129,16 +129,16 @@ class Playlists { static upsert(playlist) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, - { action: DBActions.GENERAL.UPSERT, data: playlist } + { action: DBActions.PLAYLISTS.UPSERT, data: playlist } ) } - static upsertVideoByPlaylistName(playlistName, videoData) { + static upsertVideoByPlaylistId(_id, videoData) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.UPSERT_VIDEO, - data: { playlistName, videoData } + data: { _id, videoData } } ) } @@ -160,32 +160,32 @@ class Playlists { ) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { + static deleteVideoIdByPlaylistId(_id, videoId) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.DELETE_VIDEO_ID, - data: { playlistName, videoId } + data: { _id, videoId } } ) } - static deleteVideoIdsByPlaylistName(playlistName, videoIds) { + static deleteVideoIdsByPlaylistId(_id, videoIds) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.DELETE_VIDEO_IDS, - data: { playlistName, videoIds } + data: { _id, videoIds } } ) } - static deleteAllVideosByPlaylistName(playlistName) { + static deleteAllVideosByPlaylistId(_id) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.DELETE_ALL_VIDEOS, - data: playlistName + data: _id } ) } diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index e781e01b10a16..16ccc5c5bbb44 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -85,8 +85,8 @@ class Playlists { return baseHandlers.playlists.upsert(playlist) } - static upsertVideoByPlaylistName(playlistName, videoData) { - return baseHandlers.playlists.upsertVideoByPlaylistName(playlistName, videoData) + static upsertVideoByPlaylistId(_id, videoData) { + return baseHandlers.playlists.upsertVideoByPlaylistId(_id, videoData) } static upsertVideoIdsByPlaylistId(_id, videoIds) { @@ -97,16 +97,16 @@ class Playlists { return baseHandlers.playlists.delete(_id) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { - return baseHandlers.playlists.deleteVideoIdByPlaylistName(playlistName, videoId) + static deleteVideoIdByPlaylistId(_id, videoId) { + return baseHandlers.playlists.deleteVideoIdByPlaylistId(_id, videoId) } - static deleteVideoIdsByPlaylistName(playlistName, videoIds) { - return baseHandlers.playlists.deleteVideoIdsByPlaylistName(playlistName, videoIds) + static deleteVideoIdsByPlaylistId(_id, videoIds) { + return baseHandlers.playlists.deleteVideoIdsByPlaylistId(_id, videoIds) } - static deleteAllVideosByPlaylistName(playlistName) { - return baseHandlers.playlists.deleteAllVideosByPlaylistName(playlistName) + static deleteAllVideosByPlaylistId(_id) { + return baseHandlers.playlists.deleteAllVideosByPlaylistId(_id) } static deleteMultiple(ids) { diff --git a/src/main/index.js b/src/main/index.js index bffdf946c7f23..da1d603f9f852 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -897,7 +897,7 @@ function runApp() { case DBActions.GENERAL.FIND: return await baseHandlers.playlists.find() - case DBActions.GENERAL.UPSERT: + case DBActions.PLAYLISTS.UPSERT: await baseHandlers.playlists.upsert(data) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, @@ -907,7 +907,7 @@ function runApp() { return null case DBActions.PLAYLISTS.UPSERT_VIDEO: - await baseHandlers.playlists.upsertVideoByPlaylistName(data.playlistName, data.videoData) + await baseHandlers.playlists.upsertVideoByPlaylistId(data._id, data.videoData) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, @@ -928,7 +928,7 @@ function runApp() { return null case DBActions.PLAYLISTS.DELETE_VIDEO_ID: - await baseHandlers.playlists.deleteVideoIdByPlaylistName(data.playlistName, data.videoId) + await baseHandlers.playlists.deleteVideoIdByPlaylistId(data._id, data.videoId) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, @@ -937,13 +937,13 @@ function runApp() { return null case DBActions.PLAYLISTS.DELETE_VIDEO_IDS: - await baseHandlers.playlists.deleteVideoIdsByPlaylistName(data.playlistName, data.videoIds) + await baseHandlers.playlists.deleteVideoIdsByPlaylistId(data._id, data.videoIds) // TODO: Syncing (implement only when it starts being used) // syncOtherWindows(IpcChannels.SYNC_PLAYLISTS, event, { event: '_', data }) return null case DBActions.PLAYLISTS.DELETE_ALL_VIDEOS: - await baseHandlers.playlists.deleteAllVideosByPlaylistName(data) + await baseHandlers.playlists.deleteAllVideosByPlaylistId(data) // TODO: Syncing (implement only when it starts being used) // syncOtherWindows(IpcChannels.SYNC_PLAYLISTS, event, { event: '_', data }) return null diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index df374042a5dba..7b0cd23873070 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -347,11 +347,32 @@ export default defineComponent({ } }, - toggleSave: function () { + addToPlaylist: function () { + const videoData = { + videoId: this.id, + title: this.title, + author: this.channelName, + authorId: this.channelId, + published: '', + description: this.description, + viewCount: this.viewCount, + lengthSeconds: this.data.lengthSeconds, + timeAdded: new Date().getTime(), + isLive: false, + paid: false, + type: 'video' + } + + this.$emit('add-to-playlist', videoData) + }, + + toggleFavorite: function () { if (this.inFavoritesPlaylist) { - this.removeFromPlaylist() + this.removeFromFavorites() + showToast(this.$t('Video.Video has been removed from your saved list')) } else { - this.addToPlaylist() + this.addToFavorites() + showToast(this.$t('Video.Video has been saved')) } }, @@ -536,7 +557,7 @@ export default defineComponent({ this.watchProgress = 0 }, - addToPlaylist: function () { + addToFavorites: function () { const videoData = { videoId: this.id, title: this.title, @@ -553,24 +574,39 @@ export default defineComponent({ } const payload = { - playlistName: 'Favorites', + _id: 'favorites', videoData: videoData } this.addVideo(payload) - - showToast(this.$t('Video.Video has been saved')) }, - removeFromPlaylist: function () { + removeFromFavorites: function () { const payload = { playlistName: 'Favorites', videoId: this.id } this.removeVideo(payload) + }, + + togglePlaylistPrompt: function () { + const videoData = { + videoId: this.id, + title: this.title, + author: this.channelName, + authorId: this.channelId, + published: '', + description: this.description, + viewCount: this.viewCount, + lengthSeconds: this.data.lengthSeconds, + timeAdded: new Date().getTime(), + isLive: false, + paid: false, + type: 'video' + } - showToast(this.$t('Video.Video has been removed from your saved list')) + this.showAddToPlaylistPrompt(videoData) }, ...mapActions([ @@ -578,7 +614,8 @@ export default defineComponent({ 'updateHistory', 'removeFromHistory', 'addVideo', - 'removeVideo' + 'removeVideo', + 'showAddToPlaylistPrompt', ]) } }) diff --git a/src/renderer/components/ft-list-video/ft-list-video.vue b/src/renderer/components/ft-list-video/ft-list-video.vue index 2e5967b383190..1cb53aa8b5081 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -52,7 +52,16 @@ :theme="favoriteIconTheme" :padding="appearance === `watchPlaylistItem` ? 5 : 6" :size="appearance === `watchPlaylistItem` ? 14 : 18" - @click="toggleSave" + @click="toggleFavorite" + /> +
. +*/ + +/* +* Credit goes to pavelvaravko for making this css. +* https://codepen.io/pavelvaravko/pen/qjojOr +*/ + +/* select starting stylings ------------------------------*/ +.center { + text-align: center; +} diff --git a/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.js b/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.js new file mode 100644 index 0000000000000..2ba7dbc3602a4 --- /dev/null +++ b/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.js @@ -0,0 +1,88 @@ +import Vue from 'vue' +import { mapActions } from 'vuex' +import FtFlexBox from '../ft-flex-box/ft-flex-box.vue' +import FtPrompt from '../ft-prompt/ft-prompt.vue' +import FtButton from '../ft-button/ft-button.vue' +import FtPlaylistSelector from '../ft-playlist-selector/ft-playlist-selector.vue' + +export default Vue.extend({ + name: 'FtPlaylistAddVideoPrompt', + components: { + FtFlexBox, + FtPrompt, + FtButton, + FtPlaylistSelector, + }, + data: function () { + return { + playlistAddVideoPromptValues: [ + 'save', + 'cancel' + ], + selectedPlaylists: [] + } + }, + computed: { + allPlaylists: function () { + return this.$store.getters.getAllPlaylists + }, + selectedPlaylistsCount: function () { + return this.selectedPlaylists.length + }, + showAddToPlaylistPrompt: function () { + return this.$store.getters.getShowAddToPlaylistPrompt + }, + playlistAddVideoObject: function () { + return this.$store.getters.getPlaylistAddVideoObject + }, + playlistAddVideoPromptNames: function () { + return [ + 'Save', + 'Cancel' + ] + } + }, + mounted: function () { + // this.parseUserData() + }, + methods: { + handleAddToPlaylistPrompt: function (option) { + console.log(option) + this.hideAddToPlaylistPrompt() + }, + + countSelected: function (index) { + const indexOfVideo = this.selectedPlaylists.indexOf(index) + if (indexOfVideo !== -1) { + this.selectedPlaylists.splice(indexOfVideo, 1) + } else { + this.selectedPlaylists.push(index) + } + }, + + addSelectedToPlaylists: function () { + let addedPlaylists = 0 + this.selectedPlaylists.forEach((index) => { + const playlist = this.allPlaylists[index] + const videoId = this.playlistAddVideoObject.videoId + const findVideo = playlist.videos.findIndex((video) => { + return video.videoId === videoId + }) + if (findVideo === -1) { + const payload = { + _id: playlist._id, + videoData: this.playlistAddVideoObject + } + this.addVideo(payload) + addedPlaylists++ + } + }) + this.handleAddToPlaylistPrompt(null) + }, + + ...mapActions([ + 'addVideo', + 'hideAddToPlaylistPrompt' + ]) + } +}) diff --git a/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue b/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue new file mode 100644 index 0000000000000..3cd2b8199977f --- /dev/null +++ b/src/renderer/components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue @@ -0,0 +1,34 @@ + + +