diff --git a/src/constants.js b/src/constants.js index f1d68b592466a..b281fb168256f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -47,7 +47,7 @@ const DBActions = { 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' + DELETE_ALL_VIDEOS: 'db-action-playlists-delete-all-videos', } } @@ -66,7 +66,7 @@ const SyncEvents = { PLAYLISTS: { UPSERT_VIDEO: 'sync-playlists-upsert-video', - DELETE_VIDEO: 'sync-playlists-delete-video' + DELETE_VIDEO: 'sync-playlists-delete-video', } } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 070ebda8e8e2a..e2bff72ef7b99 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -136,18 +136,28 @@ class Playlists { return db.playlists.removeAsync({ _id, protected: { $ne: true } }) } - static deleteVideoIdByPlaylistId(_id, playlistItemId) { - return db.playlists.updateAsync( - { _id }, - { $pull: { videos: { playlistItemId } } }, - { upsert: true } - ) + static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { + if (playlistItemId != null) { + return db.playlists.updateAsync( + { _id }, + { $pull: { videos: { playlistItemId } } }, + { upsert: true } + ) + } else if (videoId != null) { + return db.playlists.updateAsync( + { _id }, + { $pull: { videos: { videoId } } }, + { upsert: true } + ) + } else { + throw new Error(`Both videoId & playlistItemId are absent, _id: ${_id}`) + } } static deleteVideoIdsByPlaylistId(_id, videoIds) { return db.playlists.updateAsync( { _id }, - { $pull: { videos: { $in: videoIds } } }, + { $pull: { videos: { videoId: { $in: videoIds } } } }, { upsert: true } ) } diff --git a/src/datastores/handlers/electron.js b/src/datastores/handlers/electron.js index d84ccac13c78d..9bf2526cf3585 100644 --- a/src/datastores/handlers/electron.js +++ b/src/datastores/handlers/electron.js @@ -160,12 +160,12 @@ class Playlists { ) } - static deleteVideoIdByPlaylistId(_id, playlistItemId) { + static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { return ipcRenderer.invoke( IpcChannels.DB_PLAYLISTS, { action: DBActions.PLAYLISTS.DELETE_VIDEO_ID, - data: { _id, playlistItemId } + data: { _id, videoId, playlistItemId } } ) } diff --git a/src/datastores/handlers/web.js b/src/datastores/handlers/web.js index 103d93d441a2b..e8618beab086a 100644 --- a/src/datastores/handlers/web.js +++ b/src/datastores/handlers/web.js @@ -97,8 +97,8 @@ class Playlists { return baseHandlers.playlists.delete(_id) } - static deleteVideoIdByPlaylistId(_id, playlistItemId) { - return baseHandlers.playlists.deleteVideoIdByPlaylistId(_id, playlistItemId) + static deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) { + return baseHandlers.playlists.deleteVideoIdByPlaylistId({ _id, videoId, playlistItemId }) } static deleteVideoIdsByPlaylistId(_id, videoIds) { diff --git a/src/main/index.js b/src/main/index.js index 629ef69f04b18..898f53258916f 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -997,7 +997,11 @@ function runApp() { return null case DBActions.PLAYLISTS.DELETE_VIDEO_ID: - await baseHandlers.playlists.deleteVideoIdByPlaylistId(data._id, data.playlistItemId) + await baseHandlers.playlists.deleteVideoIdByPlaylistId({ + _id: data._id, + videoId: data.videoId, + playlistItemId: data.playlistItemId, + }) syncOtherWindows( IpcChannels.SYNC_PLAYLISTS, event, diff --git a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js index 10639a3a43bae..ef5a044b27d44 100644 --- a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js +++ b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.js @@ -55,6 +55,10 @@ export default defineComponent({ type: Boolean, default: false, }, + quickBookmarkButtonEnabled: { + type: Boolean, + default: true, + }, canMoveVideoUp: { type: Boolean, default: false, diff --git a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue index 197d7226ca079..9af01de8af16a 100644 --- a/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue +++ b/src/renderer/components/ft-list-video-lazy/ft-list-video-lazy.vue @@ -18,6 +18,7 @@ :force-list-type="forceListType" :appearance="appearance" :always-show-add-to-playlist-button="alwaysShowAddToPlaylistButton" + :quick-bookmark-button-enabled="quickBookmarkButtonEnabled" :can-move-video-up="canMoveVideoUp" :can-move-video-down="canMoveVideoDown" :can-remove-from-playlist="canRemoveFromPlaylist" 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 b1fbb559230c7..c393ab4dc3301 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -68,6 +68,10 @@ export default defineComponent({ type: Boolean, default: false, }, + quickBookmarkButtonEnabled: { + type: Boolean, + default: true, + }, canMoveVideoUp: { type: Boolean, default: false, @@ -413,6 +417,36 @@ export default defineComponent({ return this.playlistIdTypePairFinal?.playlistItemId }, + quickBookmarkPlaylistId() { + return this.$store.getters.getQuickBookmarkTargetPlaylistId + }, + quickBookmarkPlaylist() { + return this.$store.getters.getPlaylist(this.quickBookmarkPlaylistId) + }, + isQuickBookmarkEnabled() { + return this.quickBookmarkPlaylist != null + }, + isInQuickBookmarkPlaylist: function () { + if (!this.isQuickBookmarkEnabled) { return false } + + return this.quickBookmarkPlaylist.videos.some((video) => { + return video.videoId === this.id + }) + }, + quickBookmarkIconText: function () { + if (!this.isQuickBookmarkEnabled) { return false } + + const translationProperties = { + playlistName: this.quickBookmarkPlaylist.playlistName, + } + return this.isInQuickBookmarkPlaylist + ? this.$t('User Playlists.Remove from Favorites', translationProperties) + : this.$t('User Playlists.Add to Favorites', translationProperties) + }, + quickBookmarkIconTheme: function () { + return this.isInQuickBookmarkPlaylist ? 'base favorite' : 'base' + }, + watchPageLinkTo() { // For `router-link` attribute `to` return { @@ -782,12 +816,61 @@ export default defineComponent({ showToast(this.$t('Channel Unhidden', { channel: channelName })) }, + toggleQuickBookmarked() { + if (!this.isQuickBookmarkEnabled) { + // This should be prevented by UI + return + } + + if (this.isInQuickBookmarkPlaylist) { + this.removeFromQuickBookmarkPlaylist() + } else { + this.addToQuickBookmarkPlaylist() + } + }, + addToQuickBookmarkPlaylist() { + const videoData = { + videoId: this.id, + title: this.title, + author: this.channelName, + authorId: this.channelId, + description: this.description, + viewCount: this.viewCount, + lengthSeconds: this.data.lengthSeconds, + } + + this.addVideos({ + _id: this.quickBookmarkPlaylist._id, + videos: [videoData], + }) + // Update playlist's `lastUpdatedAt` + this.updatePlaylist({ _id: this.quickBookmarkPlaylist._id }) + + // TODO: Maybe show playlist name + showToast(this.$t('Video.Video has been saved')) + }, + removeFromQuickBookmarkPlaylist() { + this.removeVideo({ + _id: this.quickBookmarkPlaylist._id, + // Remove all playlist items with same videoId + videoId: this.id, + }) + // Update playlist's `lastUpdatedAt` + this.updatePlaylist({ _id: this.quickBookmarkPlaylist._id }) + + // TODO: Maybe show playlist name + showToast(this.$t('Video.Video has been removed from your saved list')) + }, + ...mapActions([ 'openInExternalPlayer', 'updateHistory', 'removeFromHistory', 'updateChannelsHidden', 'showAddToPlaylistPromptForManyVideos', + 'addVideos', + 'updatePlaylist', + 'removeVideo', ]) } }) 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 d069a53b2409b..898364909ce1e 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -55,6 +55,20 @@ :size="appearance === `watchPlaylistItem` ? 14 : 18" @click="togglePlaylistPrompt" /> + { + this.updateQuickBookmarkTargetPlaylistId(currentQuickBookmarkTargetPlaylist._id) + showToast( + this.$t('User Playlists.SinglePlaylistView.Toast["Reverted to use {oldPlaylistName} for quick bookmark"]', { + oldPlaylistName: currentQuickBookmarkTargetPlaylist.playlistName, + }), + 5000, + ) + }, + ) + } else { + showToast(this.$t('User Playlists.SinglePlaylistView.Toast.This playlist is now used for quick bookmark')) + } + }, + disableQuickBookmark() { + this.updateQuickBookmarkTargetPlaylistId(null) + showToast(this.$t('User Playlists.SinglePlaylistView.Toast.Quick bookmark disabled')) + }, + ...mapActions([ 'showAddToPlaylistPromptForManyVideos', 'updatePlaylist', 'removePlaylist', + 'updateQuickBookmarkTargetPlaylistId', ]), }, }) diff --git a/src/renderer/components/playlist-info/playlist-info.vue b/src/renderer/components/playlist-info/playlist-info.vue index f481205053a4e..b064a1837be5b 100644 --- a/src/renderer/components/playlist-info/playlist-info.vue +++ b/src/renderer/components/playlist-info/playlist-info.vue @@ -135,6 +135,20 @@ theme="secondary" @click="toggleCopyVideosPrompt" /> + + { + return video.videoId === this.id + }) + }, + quickBookmarkIconText: function () { + if (!this.isQuickBookmarkEnabled) { return false } + + const translationProperties = { + playlistName: this.quickBookmarkPlaylist.playlistName, + } + return this.isInQuickBookmarkPlaylist + ? this.$t('User Playlists.Remove from Favorites', translationProperties) + : this.$t('User Playlists.Add to Favorites', translationProperties) + }, + quickBookmarkIconTheme: function () { + return this.isInQuickBookmarkPlaylist ? 'base favorite' : 'base' + }, }, mounted: function () { if ('mediaSession' in navigator) { @@ -311,10 +341,59 @@ export default defineComponent({ this.showAddToPlaylistPromptForManyVideos({ videos: [videoData] }) }, + toggleQuickBookmarked() { + if (!this.isQuickBookmarkEnabled) { + // This should be prevented by UI + return + } + + if (this.isInQuickBookmarkPlaylist) { + this.removeFromQuickBookmarkPlaylist() + } else { + this.addToQuickBookmarkPlaylist() + } + }, + addToQuickBookmarkPlaylist() { + const videoData = { + videoId: this.id, + title: this.title, + author: this.channelName, + authorId: this.channelId, + description: this.description, + viewCount: this.viewCount, + lengthSeconds: this.lengthSeconds, + } + + this.addVideos({ + _id: this.quickBookmarkPlaylist._id, + videos: [videoData], + }) + // Update playlist's `lastUpdatedAt` + this.updatePlaylist({ _id: this.quickBookmarkPlaylist._id }) + + // TODO: Maybe show playlist name + showToast(this.$t('Video.Video has been saved')) + }, + removeFromQuickBookmarkPlaylist() { + this.removeVideo({ + _id: this.quickBookmarkPlaylist._id, + // Remove all playlist items with same videoId + videoId: this.id, + }) + // Update playlist's `lastUpdatedAt` + this.updatePlaylist({ _id: this.quickBookmarkPlaylist._id }) + + // TODO: Maybe show playlist name + showToast(this.$t('Video.Video has been removed from your saved list')) + }, + ...mapActions([ 'openInExternalPlayer', 'downloadMedia', 'showAddToPlaylistPromptForManyVideos', + 'addVideos', + 'updatePlaylist', + 'removeVideo', ]) } }) diff --git a/src/renderer/components/watch-video-info/watch-video-info.vue b/src/renderer/components/watch-video-info/watch-video-info.vue index af56b5c905a90..8d1017c996c56 100644 --- a/src/renderer/components/watch-video-info/watch-video-info.vue +++ b/src/renderer/components/watch-video-info/watch-video-info.vue @@ -89,6 +89,17 @@ theme="base" @click="togglePlaylistPrompt" /> + playlist._id === payload._id) + removeVideo(state, { _id, videoId, playlistItemId }) { + const playlist = state.playlists.find(playlist => playlist._id === _id) if (playlist) { - playlist.videos = playlist.videos.filter(video => video.playlistItemId !== payload.playlistItemId) + if (playlistItemId != null) { + playlist.videos = playlist.videos.filter(video => video.playlistItemId !== playlistItemId) + } else if (videoId != null) { + playlist.videos = playlist.videos.filter(video => video.videoId !== videoId) + } } }, - removeVideos(state, payload) { - const playlist = state.playlists.find(playlist => playlist._id === payload.playlistId) + removeVideos(state, { _id, videoId }) { + const playlist = state.playlists.find(playlist => playlist._id === _id) if (playlist) { - playlist.videos = playlist.videos.filter(video => payload.videoId.indexOf(video) === -1) + playlist.videos = playlist.videos.filter(video => videoId.indexOf(video) === -1) } }, diff --git a/src/renderer/store/modules/settings.js b/src/renderer/store/modules/settings.js index 0ec0058770ef5..12847e3df888c 100644 --- a/src/renderer/store/modules/settings.js +++ b/src/renderer/store/modules/settings.js @@ -301,7 +301,10 @@ const state = { commentAutoLoadEnabled: false, useDeArrowTitles: false, useDeArrowThumbnails: false, - deArrowThumbnailGeneratorUrl: 'https://dearrow-thumb.ajay.app' + deArrowThumbnailGeneratorUrl: 'https://dearrow-thumb.ajay.app', + // This makes the `favorites` playlist uses as quick bookmark target + // If the playlist is removed quick bookmark is disabled + quickBookmarkTargetPlaylistId: 'favorites', } const stateWithSideEffects = { diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js index f2708a23b2228..826f034494ec3 100644 --- a/src/renderer/views/Playlist/Playlist.js +++ b/src/renderer/views/Playlist/Playlist.js @@ -108,6 +108,15 @@ export default defineComponent({ isUserPlaylistRequested: function () { return this.$route.query.playlistType === 'user' }, + + quickBookmarkPlaylistId() { + return this.$store.getters.getQuickBookmarkTargetPlaylistId + }, + quickBookmarkButtonEnabled() { + if (this.selectedUserPlaylist == null) { return true } + + return this.selectedUserPlaylist?._id !== this.quickBookmarkPlaylistId + }, }, watch: { $route () { diff --git a/src/renderer/views/Playlist/Playlist.vue b/src/renderer/views/Playlist/Playlist.vue index 7b4e995aea300..d3d0ab79456fd 100644 --- a/src/renderer/views/Playlist/Playlist.vue +++ b/src/renderer/views/Playlist/Playlist.vue @@ -62,6 +62,7 @@ appearance="result" force-list-type="list" :always-show-add-to-playlist-button="true" + :quick-bookmark-button-enabled="quickBookmarkButtonEnabled" :can-move-video-up="index > 0" :can-move-video-down="index < playlistItems.length - 1" :can-remove-from-playlist="true" diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index 28c578d703327..b18dc697596d6 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -147,6 +147,9 @@ User Playlists: Create New Playlist: Create New Playlist Add to Playlist: Add to Playlist + Add to Favorites: Add to {playlistName} + Remove from Favorites: Remove from {playlistName} + Move Video Up: Move Video Up Move Video Down: Move Video Down Remove from Playlist: Remove from Playlist @@ -159,6 +162,8 @@ User Playlists: Edit Playlist Info: Edit Playlist Info Copy Playlist: Copy Playlist Remove Watched Videos: Remove Watched Videos + Enable Quick Bookmark With This Playlist: Enable Quick Bookmark With This Playlist + Disable Quick Bookmark: Disable Quick Bookmark Are you sure you want to remove all watched videos from this playlist? This cannot be undone: Are you sure you want to remove all watched videos from this playlist? This cannot be undone. Delete Playlist: Delete Playlist Are you sure you want to delete this playlist? This cannot be undone: Are you sure you want to delete this playlist? This cannot be undone. @@ -185,6 +190,11 @@ User Playlists: Video has been removed: Video has been removed There was a problem with removing this video: There was a problem with removing this video + This playlist is now used for quick bookmark: This playlist is now used for quick bookmark + Quick bookmark disabled: Quick bookmark disabled + This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo + Reverted to use {oldPlaylistName} for quick bookmark: Reverted to use {oldPlaylistName} for quick bookmark + Some videos in the playlist are not loaded yet. Click here to copy anyway.: Some videos in the playlist are not loaded yet. Click here to copy anyway. Playlist name cannot be empty. Please input a name.: Playlist name cannot be empty. Please input a name. Playlist has been updated.: Playlist has been updated.