From c40b4a69d56af702d26ff603ad87a4728f7d90cd Mon Sep 17 00:00:00 2001 From: PikachuEXE Date: Thu, 4 Jan 2024 02:44:57 +0800 Subject: [PATCH] (Multiple) Local Playlist (instead of saving video in one hidden list) (#4234) * ! Fix add/removing videos from favourite list * ! Fix playlist type not passed into video list item component * ! Fix playlist data loading issue on new windows * ! Fix user playlist data parsing * * Ensure playlist view rerender on video additonal removal for displayed playlist * ! Fix icons & moving videos up/down in user playlist * * Don't show move up/down buttons when cannot be done * * Make user playlist view filtering function now works for filtering playlists * ! Fix style of prompt from any button within playlist-info component * $ Refactor a function to use early return style * * Disallow empty playlist name when saving in playlist edit mode * * Update add video to playlist prompt to have simple filtering like user playlist view * * Update playlist view to only show video reorder/remove buttons in edit mode With notice message * ! Fix playlist view for remote playlist unnecessary reload on save * ! Fix add to playlist prompt behaviour when some playlists are hidden due to filtering * $ Remove unused watch function arguments * * Update user playlist view create new playlist button position * * Update playlist view to add transitions to video reorder/removal * * Allow playlist with different case to be created * * Update visibility of user playlist related action components according to hide playlist settings Only those visible on video, remote playlist are updated * * Update video items to only show add to playlist buttons on hover * - Remove add to favorites button from video list item component * * Update watch-video-info component to add "add to playlist" button * * Update watch-video-info component to remove "add to favourite" button * * Ensure video thumbnail image displayed in 16/9 aspect ratio Mainly due to YT missing video thumbnail image got different aspect ratio 4/3 * * Update new buttons theme * * Put edit playlist button before copy button * * Move "remove video from playlist" button to most right * * Make video reorder & remove button visible on hover in user playlist view, regardless of "edit mode" * ! Fix video count parsing for user playlist * * Update user playlist to allow duplicate video entries (per video ID) * * Remove extra attributes assignment to video object except `timeAdded` Unintended change * * Update copy playlist button to like adding multiple videos to playlists * * Update copy playlist prompt to prefill title if create new playlist prompt shown * * Ensure all playlists have `playlistName` * $- Remove unused playlist property `removeOnWatched` * * Update read all playlist code to ensure existing "default" playlist properties (_id, protected) have same values as those in default Also fix issue discovered during testing * *- Remove unnecessary code line * * Update playlists from default playlists to be deletable * ! Fix duplicate `timeAdded` value for videos copied from user playlist * ! Fix playlist import (tested with db file exported from this branch) * * Update grid view to only display playlist title up to 255 chars Still can see full title in single playlist view * * Update add video to playlist prompt to only display playlist title up to 255 chars * * Update text of new playlist button to be same as new profile button * * Ignore many extra keys from playlist objectrs Also make data import check for object keys against required keys not key counts * * Disallow playlist import from changing internal playlist ID of existing playlists Existing playlists are matched by playlist name, not by internal playlist ID * * Ensure playlist name & description trimmed when saved * * Add missing change for importing playlist to ignore `protected` * $ Refactor code for "video object has all require keys" * ! Fix style of prompt from any button within playlist-info component again z-index war * * Update some empty message & button text * * Ensure playlist item action buttons are hidden during animations * ! Fix other prompt z-index * * Update `showAddToPlaylistPromptForManyVideos` to throw error when required keys missing from passed in video data * *- Remove unused video property `paid` from playlist related code * *- Remove unused video property `type` from playlist related code * ! Fix duplicate playlist displayed on playlist conversion * *- Remove unused video property `published` from playlist related code * * Don't require & generate property `timeAdded` when display a prompt * *- Remove unused video property `isLive` from playlist related code * ! Fix text * ! Revert code change for debugging only * ! Fix CSS for "Ensure playlist item action buttons are hidden during animations" * * Update add playlist button to be shown for live videos * + Add clear all playlists button * * Update add video to playlist prompt to swap "save" & "create playlist" button * ! Fix playlist view won't load after default playlist added due to lack of playlist * * Show toast message when no playlist selected after pressing save button * * Update add video to playlist prompt to show latest updated playlist first Easier for copying playlist with new playlist(s) * ! Fix watch page when visited via history after viewing the video from a deleted user playlist * ! Fix share buttons emitting user playlist IDs * * Add toast message after removing all playlists * ! Fix remote playlist video list rendering with duplicate videos * ! Fix missing key for some direct child elements under `transition-group` * ! Fix animation introduced to unnecessary elements * ! Prevent video link click during animation * ! Fix playlist creation/removal not reflected in other windows * * Ensure playlist visual selected state in sync with data in parent * * Update add video to playlist prompt to auto select playlists created after prompt shown once * * Update copy playlist button to show warning about not loaded videos when some not loaded yet For remote playlist Won't work for Invidious though (coz no way to load more) * * Update add video prompt to make it easier to access buttons via keyboard * * Make video reorder & remove button always visible in user playlist view * * Make plus button can be focused with tab Side effect is thumbnail link can be focused too (otherwise can't make plus button visible) * * Update add video to playlist prompt to focus on search input on shown * * Accept playlist optional property `createdAt` when importing * * Update playlist view to always show add to playlist button * $ Refactor `inUserPlaylist` Only playlist view would supply `playlistType` * * Improve accessibility for new prompts * * Make edit playlist inputs focused on entering edit mode * * Make most prompts focus back to last focused element on close * $ Use tabindex 0 for prompts Possible tabindex values unnecessary with elements programatically focused * * Update add video to playlist prompt to focus back to search input after new playlist created * * Update ft-list-video to focus back on thumbnail link element after add to playlist prompt closed * ! Fix unable to import duplicate video entries into an existing playlist * ! Fix create playlist prompt looks unclosed after playlist created when opened via add video to playlist prompt * ! Fix duplicate video entry ID when playlist with duplicate videoId entries copied to another playlist * ! Fix issues caused by last merge in e291cc8b41b2273a0d77c9f35ea760f0e0f7ac10 * $ Remove unused imports * ! Fix default playlists added when no playlist stored had no required attributes added on 1st run * ! Fix invalid prop type warning * * Use v-if instead of v-show * $ Just assign bool value instead of if-else * + Update playlists view to add sorting option * $- Remove unnecessary property assignments * ! Fix issues caused by "Remove unnecessary property assignments" * ! Fix issues caused by "Remove unnecessary property assignments" * * Ensure sort by time also fallbacks to playlist name sorting * ! Fix add/remove video does not update playlist last created time * ! Fix multi window playlist sync * * Show filtering input & sorting element when no. of playlist > 1 * * Update upcoming video to allow saving in playlist * ! Fix unable to visit previous video when playing first video in a playlist * * Update sorting options labels * ! Fix add to play prompt max width * ! Fix user playlist view incorrectly sorted playlists by latest updated first when filtered * * Update add to playlist prompt to add sort options * $ Remove unused classname * ! Ensure new playlist button focused after add to playlist prompt closed while thumbnail link unselectable * * Add playlists sort order for last played at, update it on play * ! Fix bug introduced by last merge * ! Fix thumbnail not respecting backend preference * ! Fix empty playlist thumbnail in single playlist view * * Make adding large no. of videos faster * $ Remove unused mapActions entries * $ Remove unused and unusable playlist code for inserting video IDs into a playlist * * Update single playlist view to only show `Remove watched videos` btn when there is any video * ! Fix prompt style issue caused by old branch changes * ! Fix removing video from local playlist remove all entries with same video ID * ! Fix adding video does not have uniqueID & timeAdded set Caused by previous add many video performance fix * ! Fix navigation in user playlist between entries of same video * * Reflect playlist updates on watch page playlist panel * $- Remove outdated incorrect prompt style fix * ! Fix add video to playlist prompt too high when there are too many playlists * ! Fix watch page play next/prev video in local playlist * ! Fix error in online playlist playing * * Keep partial playlist backward compatibility by restoring `type` in persisted playlist videos * ! Fix grid item for playlist to hide external player button for local playlist * * Make history rememeber uniqueId (local playlist video entry) when playing video with local playlist * ! Fix grid item for video in user playlist to hide external player button * ! Fix grid item for video in history with user playlist to hide external player button * ! Fix grid item for video in watch page playlist component with user playlist to hide external player button * * Update watch page to clear `uniqueID` from query when not playing local playlist * ! Fix watch page about user playlist detection on new window * * Reflect playlist updates on watch page playlist panel, even for currently playing video * ! Allow video with local playlist to be played in external player, but without playlist Also add missing fix for WatchVideoInfo * ! Fix external player handling for video in local playlist * ! Fix ft-video having different URLs in links * yarn run lint-style-fix * $ Rename uniqueId to playlistItemId * * Ensure local playlist ID won't conflict with remote playlist ID * - Remove import video while creating playlist related code * * Make text translatable * * Make sort option text translatable * ! Fix reverse condition * $ Just use ?. * Code review changes * Spaces * beforeDestroy * Remove unused methods * Remove infoData * filter > find * avoid importing `crypto` from NodeJS * Compress CSS ruleset * $ Remove useless prefix from key * Use template for element with v-if only * Make playlist selector look more selectable (cursor: pointer) * Translate toast messages and fix remove watched videos from playlist * ! Fix translation * - Remove unused methods * $ Remove workaround for issue which cannot be reproduced anymore * $ Remove remaining useless code following last commit removing workaround * you have no playlist > you have no playlists * CSS use var --horizontal-directionality-coefficient * * Update danger buttons color to primary (like clear search cache button) * ! Fix playlist icon container size * ! Fix sorting in add video to playlist prompt when filtered * $- Update ft-inputs to removed prop input-tabindex * ! Fix vue warning * + Add toggle to export playlist for older FT * * Update tooltip text * ! Fix vue warning * * Update text align * * Remove the extra surrounding spaces * Backtick > quote * - Remove useless prop * Fix tootip in lower width view * Update default filename * * Make tooltip auto-wrap * Revert "* Make tooltip auto-wrap" This reverts commit 7d932b936c7814349e50204a7979d2d0f5b0ea73. * Adjust tooltip styling * * Wait for local playlist loading only when playing with a user playlist * * Wait for local playlist loading only when playing with a user playlist requested Request user playlist can still be absent (but not requested for no playlist or remote playlist) * ! Fix single playlist view cover image having link when no video * ! Workaround create playlist prompt won't close when enter pressed in input box * ! Fix clear text button style * ! Fix copying playlist twice to the same user playlist produces same playlistItemId * ! Fix add video to playlist prompt input box style * ! Fix workaround for "create playlist prompt won't close when enter pressed in input box" * ! Ensure add video to playlist prompt updated when a playlist deleted (in another window) * ! Avoid saving and displaying unwanted video attributes into playlists * ! Fix playlist view for deleted user playlists * ! Fix copying playlist twice to the same user playlist produces same playlistItemId Real fix Cause of issue: updating property of input objects (which is shared across multiple run) * ! Fix search suggestions being blocked by playlist info component in single playlist view * ! Fix warning when reversing playlist in watch view * ! Fix prompt in playlist view (playlistinfo component) not above sidebar * Remove CSS top comments absent in other CSS files * Remove not useful comment for CSS * Float with variable Co-authored-by: Jason <84899178+jasonhenriquez@users.noreply.github.com> * Revert "! Fix clear text button style" This reverts commit 44ed286848e3cdf4c52f29a867fe09964adffd78. * Missing change for Revert "! Fix clear text button style" * $ Remove unused style for ft-playlist-selector * Apply changes from review * * Use $tc for translation entries with different value according to count values * ! Fix playlist link in watch view --------- Co-authored-by: Jason Henriquez Co-authored-by: Jason <84899178+jasonhenriquez@users.noreply.github.com> --- src/constants.js | 2 +- src/datastores/handlers/base.js | 36 +- src/datastores/handlers/electron.js | 33 +- src/datastores/handlers/web.js | 28 +- src/main/index.js | 44 ++- src/renderer/App.js | 12 +- src/renderer/App.vue | 6 + .../components/data-settings/data-settings.js | 138 ++++++-- .../data-settings/data-settings.vue | 12 +- .../ft-create-playlist-prompt.css | 8 + .../ft-create-playlist-prompt.js | 79 +++++ .../ft-create-playlist-prompt.vue | 34 ++ .../ft-element-list/ft-element-list.js | 4 + .../ft-element-list/ft-element-list.vue | 3 +- .../ft-list-lazy-wrapper.js | 19 +- .../ft-list-lazy-wrapper.vue | 8 +- .../ft-list-playlist/ft-list-playlist.js | 59 +++- .../ft-list-playlist/ft-list-playlist.vue | 8 +- .../ft-list-video-lazy/ft-list-video-lazy.js | 24 ++ .../ft-list-video-lazy/ft-list-video-lazy.vue | 9 + .../components/ft-list-video/ft-list-video.js | 184 ++++++---- .../ft-list-video/ft-list-video.vue | 62 ++-- .../ft-playlist-add-video-prompt.css | 36 ++ .../ft-playlist-add-video-prompt.js | 261 ++++++++++++++ .../ft-playlist-add-video-prompt.vue | 69 ++++ .../ft-playlist-selector.js | 75 ++++ .../ft-playlist-selector.scss | 89 +++++ .../ft-playlist-selector.vue | 43 +++ .../components/ft-prompt/ft-prompt.css | 7 +- .../components/ft-prompt/ft-prompt.js | 6 +- .../components/ft-prompt/ft-prompt.vue | 2 +- .../ft-share-button/ft-share-button.js | 22 +- src/renderer/components/ft-toast/ft-toast.css | 3 +- .../ft-toggle-switch/ft-toggle-switch.js | 6 +- .../ft-toggle-switch/ft-toggle-switch.vue | 1 + .../components/ft-tooltip/ft-tooltip.css | 10 + .../components/ft-tooltip/ft-tooltip.js | 6 +- .../components/ft-tooltip/ft-tooltip.vue | 10 +- .../components/playlist-info/playlist-info.js | 326 ++++++++++++++---- .../playlist-info/playlist-info.scss | 16 +- .../playlist-info/playlist-info.vue | 115 +++++- .../privacy-settings/privacy-settings.js | 12 + .../privacy-settings/privacy-settings.vue | 13 + .../watch-video-info/watch-video-info.js | 72 ++-- .../watch-video-info/watch-video-info.vue | 10 +- .../watch-video-playlist.js | 306 +++++++++------- .../watch-video-playlist.vue | 44 ++- src/renderer/main.js | 10 + src/renderer/scss-partials/_ft-list-item.scss | 35 +- src/renderer/store/modules/history.js | 12 +- src/renderer/store/modules/playlists.js | 326 +++++++++++++++--- src/renderer/store/modules/settings.js | 16 + src/renderer/store/modules/utils.js | 120 +++++++ src/renderer/views/Playlist/Playlist.js | 289 +++++++++++++--- .../Playlist/{Playlist.css => Playlist.scss} | 37 ++ src/renderer/views/Playlist/Playlist.vue | 108 ++++-- .../views/UserPlaylists/UserPlaylists.css | 17 + .../views/UserPlaylists/UserPlaylists.js | 187 ++++++++-- .../views/UserPlaylists/UserPlaylists.vue | 58 ++-- src/renderer/views/Watch/Watch.js | 124 +++++-- src/renderer/views/Watch/Watch.vue | 2 + static/locales/en-US.yaml | 85 +++++ 62 files changed, 3101 insertions(+), 697 deletions(-) create mode 100644 src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.css create mode 100644 src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.js create mode 100644 src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue 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 rename src/renderer/views/Playlist/{Playlist.css => Playlist.scss} (51%) diff --git a/src/constants.js b/src/constants.js index bd80da2ffdc17..f1d68b592466a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -44,7 +44,7 @@ const DBActions = { PLAYLISTS: { UPSERT_VIDEO: 'db-action-playlists-upsert-video-by-playlist-name', - UPSERT_VIDEO_IDS: 'db-action-playlists-upsert-video-ids-by-playlist-id', + UPSERT_VIDEOS: 'db-action-playlists-upsert-videos-by-playlist-name', DELETE_VIDEO_ID: 'db-action-playlists-delete-video-by-playlist-name', DELETE_VIDEO_IDS: 'db-action-playlists-delete-video-ids', DELETE_ALL_VIDEOS: 'db-action-playlists-delete-all-videos' diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 71e1ca3889682..070ebda8e8e2a 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -64,8 +64,8 @@ class History { return db.history.updateAsync({ videoId }, { $set: { watchProgress } }, { upsert: true }) } - static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { - return db.history.updateAsync({ videoId }, { $set: { lastViewedPlaylistId } }, { upsert: true }) + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) { + return db.history.updateAsync({ videoId }, { $set: { lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId } }, { upsert: true }) } static delete(videoId) { @@ -112,18 +112,22 @@ class Playlists { return db.playlists.findAsync({}) } - static upsertVideoByPlaylistName(playlistName, videoData) { + static upsert(playlist) { + return db.playlists.updateAsync({ _id: playlist._id }, { $set: playlist }, { upsert: true }) + } + + static upsertVideoByPlaylistId(_id, videoData) { return db.playlists.updateAsync( - { playlistName }, + { _id }, { $push: { videos: videoData } }, { upsert: true } ) } - static upsertVideoIdsByPlaylistId(_id, videoIds) { + static upsertVideosByPlaylistId(_id, videos) { return db.playlists.updateAsync( { _id }, - { $push: { videos: { $each: videoIds } } }, + { $push: { videos: { $each: videos } } }, { upsert: true } ) } @@ -132,25 +136,25 @@ class Playlists { return db.playlists.removeAsync({ _id, protected: { $ne: true } }) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { + static deleteVideoIdByPlaylistId(_id, playlistItemId) { return db.playlists.updateAsync( - { playlistName }, - { $pull: { videos: { videoId } } }, + { _id }, + { $pull: { videos: { playlistItemId } } }, { upsert: true } ) } - static deleteVideoIdsByPlaylistName(playlistName, videoIds) { + static deleteVideoIdsByPlaylistId(_id, videoIds) { return db.playlists.updateAsync( - { playlistName }, + { _id }, { $pull: { videos: { $in: videoIds } } }, { upsert: true } ) } - static deleteAllVideosByPlaylistName(playlistName) { + static deleteAllVideosByPlaylistId(_id) { return db.playlists.updateAsync( - { playlistName }, + { _id }, { $set: { videos: [] } }, { upsert: true } ) @@ -161,7 +165,7 @@ class Playlists { } static deleteAll() { - return db.playlists.removeAsync({ protected: { $ne: true } }) + return db.playlists.removeAsync({}, { multi: true }) } static persist() { @@ -174,7 +178,7 @@ function compactAllDatastores() { Settings.persist(), History.persist(), Profiles.persist(), - Playlists.persist() + Playlists.persist(), ]) } @@ -184,7 +188,7 @@ const baseHandlers = { profiles: Profiles, playlists: Playlists, - compactAllDatastores + compactAllDatastores, } export default baseHandlers diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index ddb90ff82434e..d84ccac13c78d 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -42,12 +42,12 @@ class History { ) } - static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) { return ipcRenderer.invoke( IpcChannels.DB_HISTORY, { action: DBActions.HISTORY.UPDATE_PLAYLIST, - data: { videoId, lastViewedPlaylistId } + data: { videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId } } ) } @@ -126,22 +126,29 @@ class Playlists { ) } - static upsertVideoByPlaylistName(playlistName, videoData) { + static upsert(playlist) { + return ipcRenderer.invoke( + IpcChannels.DB_PLAYLISTS, + { action: DBActions.GENERAL.UPSERT, data: playlist } + ) + } + + static upsertVideoByPlaylistId(_id, videoData) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.UPSERT_VIDEO, - data: { playlistName, videoData } + data: { _id, videoData } } ) } - static upsertVideoIdsByPlaylistId(_id, videoIds) { + static upsertVideosByPlaylistId(_id, videos) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { - action: DBActions.PLAYLISTS.UPSERT_VIDEO_IDS, - data: { _id, videoIds } + action: DBActions.PLAYLISTS.UPSERT_VIDEOS, + data: { _id, videos } } ) } @@ -153,32 +160,32 @@ class Playlists { ) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { + static deleteVideoIdByPlaylistId(_id, playlistItemId) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.DELETE_VIDEO_ID, - data: { playlistName, videoId } + data: { _id, playlistItemId } } ) } - 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 a81eb305d1dd9..103d93d441a2b 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -33,8 +33,8 @@ class History { return baseHandlers.history.updateWatchProgress(videoId, watchProgress) } - static updateLastViewedPlaylist(videoId, lastViewedPlaylistId) { - return baseHandlers.history.updateLastViewedPlaylist(videoId, lastViewedPlaylistId) + static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) { + return baseHandlers.history.updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType, lastViewedPlaylistItemId) } static delete(videoId) { @@ -81,28 +81,32 @@ class Playlists { return baseHandlers.playlists.find() } - static upsertVideoByPlaylistName(playlistName, videoData) { - return baseHandlers.playlists.upsertVideoByPlaylistName(playlistName, videoData) + static upsert(playlist) { + return baseHandlers.playlists.upsert(playlist) } - static upsertVideoIdsByPlaylistId(_id, videoIds) { - return baseHandlers.playlists.upsertVideoIdsByPlaylistId(_id, videoIds) + static upsertVideoByPlaylistId(_id, videoData) { + return baseHandlers.playlists.upsertVideoByPlaylistId(_id, videoData) + } + + static upsertVideosByPlaylistId(_id, videoData) { + return baseHandlers.playlists.upsertVideosByPlaylistId(_id, videoData) } static delete(_id) { return baseHandlers.playlists.delete(_id) } - static deleteVideoIdByPlaylistName(playlistName, videoId) { - return baseHandlers.playlists.deleteVideoIdByPlaylistName(playlistName, videoId) + static deleteVideoIdByPlaylistId(_id, playlistItemId) { + return baseHandlers.playlists.deleteVideoIdByPlaylistId(_id, playlistItemId) } - 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 95bc4aebdf27c..0ca9917a3c86d 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -847,7 +847,7 @@ function runApp() { return null case DBActions.HISTORY.UPDATE_PLAYLIST: - await baseHandlers.history.updateLastViewedPlaylist(data.videoId, data.lastViewedPlaylistId) + await baseHandlers.history.updateLastViewedPlaylist(data.videoId, data.lastViewedPlaylistId, data.lastViewedPlaylistType, data.lastViewedPlaylistItemId) syncOtherWindows( IpcChannels.SYNC_HISTORY, event, @@ -948,15 +948,27 @@ function runApp() { switch (action) { case DBActions.GENERAL.CREATE: await baseHandlers.playlists.create(data) - // TODO: Syncing (implement only when it starts being used) - // syncOtherWindows(IpcChannels.SYNC_PLAYLISTS, event, { event: '_', data }) + syncOtherWindows( + IpcChannels.SYNC_PLAYLISTS, + event, + { event: SyncEvents.GENERAL.CREATE, data } + ) return null 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.GENERAL.UPSERT, data } + ) + 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, @@ -964,20 +976,26 @@ function runApp() { ) return null - case DBActions.PLAYLISTS.UPSERT_VIDEO_IDS: - await baseHandlers.playlists.upsertVideoIdsByPlaylistId(data._id, data.videoIds) - // TODO: Syncing (implement only when it starts being used) - // syncOtherWindows(IpcChannels.SYNC_PLAYLISTS, event, { event: '_', data }) + case DBActions.PLAYLISTS.UPSERT_VIDEOS: + await baseHandlers.playlists.upsertVideosByPlaylistId(data._id, data.videos) + syncOtherWindows( + IpcChannels.SYNC_PLAYLISTS, + event, + { event: SyncEvents.PLAYLISTS.UPSERT_VIDEOS, data } + ) return null case DBActions.GENERAL.DELETE: await baseHandlers.playlists.delete(data) - // TODO: Syncing (implement only when it starts being used) - // syncOtherWindows(IpcChannels.SYNC_PLAYLISTS, event, { event: '_', data }) + syncOtherWindows( + IpcChannels.SYNC_PLAYLISTS, + event, + { event: SyncEvents.GENERAL.DELETE, data } + ) return null case DBActions.PLAYLISTS.DELETE_VIDEO_ID: - await baseHandlers.playlists.deleteVideoIdByPlaylistName(data.playlistName, data.videoId) + await baseHandlers.playlists.deleteVideoIdByPlaylistId(data._id, data.playlistItemId) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, @@ -986,13 +1004,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/App.js b/src/renderer/App.js index 25ef07ca4e8b5..e152ea8fb2d27 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -9,6 +9,8 @@ import FtPrompt from './components/ft-prompt/ft-prompt.vue' import FtButton from './components/ft-button/ft-button.vue' import FtToast from './components/ft-toast/ft-toast.vue' import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' +import FtPlaylistAddVideoPrompt from './components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue' +import FtCreatePlaylistPrompt from './components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue' import { marked } from 'marked' import { IpcChannels } from '../constants' import packageDetails from '../../package.json' @@ -28,7 +30,9 @@ export default defineComponent({ FtPrompt, FtButton, FtToast, - FtProgressBar + FtProgressBar, + FtPlaylistAddVideoPrompt, + FtCreatePlaylistPrompt, }, data: function () { return { @@ -66,6 +70,12 @@ export default defineComponent({ checkForBlogPosts: function () { return this.$store.getters.getCheckForBlogPosts }, + showAddToPlaylistPrompt: function () { + return this.$store.getters.getShowAddToPlaylistPrompt + }, + showCreatePlaylistPrompt: function () { + return this.$store.getters.getShowCreatePlaylistPrompt + }, windowTitle: function () { const routeTitle = this.$route.meta.title if (routeTitle !== 'Channel' && routeTitle !== 'Watch' && routeTitle !== 'Hashtag') { diff --git a/src/renderer/App.vue b/src/renderer/App.vue index e0394fae9711b..cb563b887b91f 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -74,6 +74,12 @@ :option-values="externalLinkOpeningPromptValues" @click="handleExternalLinkOpeningPromptAnswer" /> + + { + playlists.forEach((playlistData) => { // We would technically already be done by the time the data is parsed, // however we want to limit the possibility of malicious data being sent // to the app, so we'll only grab the data we need here. @@ -911,58 +930,71 @@ export default defineComponent({ const playlistObject = {} Object.keys(playlistData).forEach((key) => { - if (!requiredKeys.includes(key) && !optionalKeys.includes(key)) { + if ([requiredKeys, optionalKeys, ignoredKeys].every((ks) => !ks.includes(key))) { const message = `${this.$t('Settings.Data Settings.Unknown data key')}: ${key}` showToast(message) } else if (key === 'videos') { const videoArray = [] playlistData.videos.forEach((video) => { - let hasAllKeys = true - requiredVideoKeys.forEach((videoKey) => { - if (!Object.keys(video).includes(videoKey)) { - hasAllKeys = false - } - }) + const videoPropertyKeys = Object.keys(video) + const videoObjectHasAllRequiredKeys = requiredVideoKeys.every((k) => videoPropertyKeys.includes(k)) - if (hasAllKeys) { + if (videoObjectHasAllRequiredKeys) { videoArray.push(video) } }) playlistObject[key] = videoArray - } else { + } else if (!ignoredKeys.includes(key)) { + // Do nothing for keys to be ignored playlistObject[key] = playlistData[key] } }) - const objectKeys = Object.keys(playlistObject) + const playlistObjectKeys = Object.keys(playlistObject) + const playlistObjectHasAllRequiredKeys = requiredKeys.every((k) => playlistObjectKeys.includes(k)) - if ((objectKeys.length < requiredKeys.length) || playlistObject.videos.length === 0) { - const message = this.$t('Settings.Data Settings.Playlist insufficient data', { playlist: playlistData.playlistName }) - showToast(message) - } else { + if (playlistObjectHasAllRequiredKeys) { const existingPlaylist = this.allPlaylists.find((playlist) => { return playlist.playlistName === playlistObject.playlistName }) if (existingPlaylist !== undefined) { playlistObject.videos.forEach((video) => { - const videoExists = existingPlaylist.videos.some((x) => { - return x.videoId === video.videoId - }) + let videoExists = false + if (video.playlistItemId != null) { + // Find by `playlistItemId` if present + videoExists = existingPlaylist.videos.some((x) => { + // Allow duplicate (by videoId) videos to be added + return x.videoId === video.videoId && x.playlistItemId === video.playlistItemId + }) + } else { + // Older playlist exports have no `playlistItemId` but have `timeAdded` + // Which might be duplicate for copied playlists with duplicate `videoId` + videoExists = existingPlaylist.videos.some((x) => { + // Allow duplicate (by videoId) videos to be added + return x.videoId === video.videoId && x.timeAdded === video.timeAdded + }) + } if (!videoExists) { + // Keep original `timeAdded` value const payload = { - playlistName: existingPlaylist.playlistName, - videoData: video + _id: existingPlaylist._id, + videoData: video, } this.addVideo(payload) } }) + // Update playlist's `lastUpdatedAt` + this.updatePlaylist({ _id: existingPlaylist._id }) } else { this.addPlaylist(playlistObject) } + } else { + const message = this.$t('Settings.Data Settings.Playlist insufficient data', { playlist: playlistData.playlistName }) + showToast(message) } }) @@ -986,6 +1018,55 @@ export default defineComponent({ await this.promptAndWriteToFile(options, JSON.stringify(this.allPlaylists), 'All playlists has been successfully exported') }, + exportPlaylistsForOlderVersionsSometimes: function () { + if (this.shouldExportPlaylistForOlderVersions) { + this.exportPlaylistsForOlderVersions() + } else { + this.exportPlaylists() + } + }, + + exportPlaylistsForOlderVersions: async function () { + const dateStr = getTodayDateStrLocalTimezone() + const exportFileName = 'freetube-playlists-as-single-favorites-playlist-' + dateStr + '.db' + + const options = { + defaultPath: exportFileName, + filters: [ + { + name: 'Database File', + extensions: ['db'] + } + ] + } + + const favoritesPlaylistData = { + playlistName: 'Favorites', + protected: true, + videos: [], + } + + this.allPlaylists.forEach((playlist) => { + playlist.videos.forEach((video) => { + const videoAlreadyAdded = favoritesPlaylistData.videos.some((v) => { + return v.videoId === video.videoId + }) + if (videoAlreadyAdded) { return } + + favoritesPlaylistData.videos.push( + Object.assign({ + // The "required" keys during import (but actually unused) in older versions + isLive: false, + paid: false, + published: '', + }, video) + ) + }) + }) + + await this.promptAndWriteToFile(options, JSON.stringify([favoritesPlaylistData]), 'All playlists has been successfully exported') + }, + convertOldFreeTubeFormatToNew(oldData) { const convertedData = [] for (const channel of oldData) { @@ -1151,7 +1232,8 @@ export default defineComponent({ 'updateShowProgressBar', 'updateHistory', 'addPlaylist', - 'addVideo' + 'addVideo', + 'updatePlaylist', ]), ...mapMutations([ diff --git a/src/renderer/components/data-settings/data-settings.vue b/src/renderer/components/data-settings/data-settings.vue index cdc55727fd752..456c6d3585c57 100644 --- a/src/renderer/components/data-settings/data-settings.vue +++ b/src/renderer/components/data-settings/data-settings.vue @@ -49,7 +49,17 @@ /> + + + { + return playlist.playlistName === this.playlistName + }) + if (nameExists !== -1) { + showToast(this.$t('User Playlists.CreatePlaylistPrompt.Toast["There is already a playlist with this name. Please pick a different name."]')) + return + } + + const playlistObject = { + playlistName: this.playlistName, + protected: false, + description: '', + videos: [], + } + + try { + this.addPlaylist(playlistObject) + showToast(this.$t('User Playlists.CreatePlaylistPrompt.Toast["Playlist {playlistName} has been successfully created."]', { + playlistName: this.playlistName, + })) + } catch (e) { + showToast(this.$t('User Playlists.CreatePlaylistPrompt.Toast["There was an issue with creating the playlist."]')) + console.error(e) + } finally { + this.hideCreatePlaylistPrompt() + } + }, + + ...mapActions([ + 'addPlaylist', + 'hideCreatePlaylistPrompt', + ]) + } +}) diff --git a/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue new file mode 100644 index 0000000000000..f541d08892a36 --- /dev/null +++ b/src/renderer/components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue @@ -0,0 +1,34 @@ + + +