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.