{
return {
@@ -28,7 +34,7 @@ export default defineComponent({
isArrowBackwardDisabled: true,
isArrowForwardDisabled: true,
searchSuggestionsDataList: [],
- lastSuggestionQuery: ''
+ lastSuggestionQuery: '',
}
},
computed: {
@@ -92,8 +98,81 @@ export default defineComponent({
newWindowText: function () {
return this.$t('Open New Window')
+ },
+
+ selectVideosText: function () {
+ return this.isSelectionModeEnabled
+ ? this.$t('Disable Video Selection Mode')
+ : this.$t('Enable Video Selection Mode')
+ },
+
+ hideSharingActions: function() {
+ return this.$store.getters.getHideSharingActions
+ },
+
+ isSelectionModeEnabled: function () {
+ return this.$store.getters.getIsSelectionModeEnabled
+ },
+
+ selectionModeSelections: function () {
+ return this.$store.getters.getSelectionModeSelections
+ },
+
+ selectionModeSelectionValues: function() {
+ return Object.values(this.selectionModeSelections.selections)
+ },
+
+ videoDropdownOptionArguments: function() {
+ const videoComponents = this.selectionModeSelectionValues
+ const count = videoComponents.length
+ const videosWithChannelIdsCount = videoComponents.filter((videoComponent) => videoComponent.channelId !== null).length
+
+ const watchedVideosCount = videoComponents.filter((videoComponent) => videoComponent.watched).length
+ const unwatchedVideosCount = count - watchedVideosCount
+
+ const savedVideosCount = videoComponents.filter((videoComponent) => videoComponent.inFavoritesPlaylist).length
+ const unsavedVideosCount = count - savedVideosCount
+
+ const channelsHidden = JSON.parse(this.$store.getters.getChannelsHidden).map((ch) => {
+ // Legacy support
+ if (typeof ch === 'string') {
+ return { name: ch, preferredName: '', icon: '' }
+ }
+ return ch
+ })
+
+ const hiddenChannels = new Set()
+ const unhiddenChannels = new Set()
+ videoComponents.forEach((videoComponent) => {
+ const channelIdentifier = channelsHidden.find(ch => ch.name === videoComponent.channelId)?.name ??
+ channelsHidden.find(ch => ch.name === videoComponent.channelName)?.name
+ if (channelIdentifier) {
+ if (!hiddenChannels.has(channelIdentifier)) {
+ hiddenChannels.add(channelIdentifier)
+ }
+ } else if (!unhiddenChannels.has(videoComponent.channelId)) {
+ unhiddenChannels.add(videoComponent.channelId)
+ }
+ })
+
+ return [
+ count,
+ watchedVideosCount,
+ unwatchedVideosCount,
+ savedVideosCount,
+ unsavedVideosCount,
+ videosWithChannelIdsCount,
+ hiddenChannels,
+ unhiddenChannels,
+ videoComponents
+ ]
+ },
+
+ dropdownOptions: function () {
+ return getVideoDropdownOptions(...this.videoDropdownOptionArguments)
}
},
+
mounted: function () {
let previousWidth = window.innerWidth
if (window.innerWidth <= 680) {
@@ -308,6 +387,8 @@ export default defineComponent({
} else {
this.isForwardOrBack = false
}
+
+ this.setSelectionMode(false)
},
historyBack: function () {
@@ -349,17 +430,47 @@ export default defineComponent({
// Web placeholder
}
},
+
+ handleOptionsClick: function (option) {
+ handleVideoDropdownOptionsClick(option, ...this.videoDropdownOptionArguments,
+ (channelsHidden) => this.updateChannelsHidden(channelsHidden))
+ },
+
+ toggleSelectionMode: function () {
+ this.setSelectionMode(!this.isSelectionModeEnabled)
+ },
+
+ setSelectionMode: function (value) {
+ if (this.isSelectionModeEnabled === value) {
+ return
+ }
+
+ // prevents all page elements from being highlighted if Ctrl+A was used in Selection Mode
+ if (this.isSelectionModeEnabled && window.getSelection) {
+ window.getSelection().empty()
+ }
+
+ this.$store.commit('setSelectionMode', value)
+
+ if (!value) {
+ this.clearSelectionModeSelections()
+ }
+ },
+
navigate: function (route) {
this.$router.push('/' + route)
},
hideFilters: function () {
this.showFilters = false
},
+
updateSearchInputText: function (text) {
this.$refs.searchInput.updateInputData(text)
},
...mapActions([
'getYoutubeUrlInfo',
+ 'clearSelectionModeSelections',
+ 'updateChannelsHidden'
])
}
})
diff --git a/src/renderer/components/top-nav/top-nav.scss b/src/renderer/components/top-nav/top-nav.scss
index 943ffce8b3a3b..650215f751c86 100644
--- a/src/renderer/components/top-nav/top-nav.scss
+++ b/src/renderer/components/top-nav/top-nav.scss
@@ -42,6 +42,14 @@
}
}
+:deep {
+ .ftIconButton.navIcon:has(svg) {
+ padding: 0;
+ block-size: fit-content;
+ inline-size: fit-content;
+ }
+}
+
.navIcon {
border-radius: 50%;
color: var(--primary-text-color);
@@ -56,13 +64,12 @@
color: var(--text-with-main-color);
}
- &.arrowBackwardDisabled,
- &.arrowForwardDisabled {
- color: gray;
- opacity: 0.5;
+ &.navIconDisabled {
pointer-events: none;
-webkit-user-select: none;
user-select: none;
+ color: gray;
+ opacity: 0.5;
}
&:hover {
@@ -86,6 +93,22 @@
}
}
+.selectionModeToggleButton {
+ &.selectionModeEnabled {
+ $effect-distance: 5px;
+ box-shadow: 0 0 $effect-distance var(--primary-color);
+
+ @include top-nav-is-colored {
+ box-shadow: 0 0 $effect-distance var(--text-with-main-color);
+ }
+ }
+}
+
+.ftIconButton.selectionModeOptionsButton {
+ line-height: normal;
+ block-size: normal;
+}
+
.navFilterIcon {
$effect-distance: 10px;
diff --git a/src/renderer/components/top-nav/top-nav.vue b/src/renderer/components/top-nav/top-nav.vue
index 69a54ab70355b..ec9787b7cde92 100644
--- a/src/renderer/components/top-nav/top-nav.vue
+++ b/src/renderer/components/top-nav/top-nav.vue
@@ -16,7 +16,7 @@
+
+
1) {
+ options.push(
+ {
+ label: i18n.tc('Video.Clear Selection', count, { count: count }),
+ value: 'clear'
+ },
+ {
+ type: 'divider'
+ }
+ )
+
+ if (savedVideosCount === count) {
+ options.push({
+ label: i18n.tc('Video.Unsave Video', count, { count: count }),
+ value: 'unsave'
+ })
+ } else if (unsavedVideosCount === count) {
+ options.push({
+ label: i18n.tc('Video.Save Video', count, { count: count }),
+ value: 'save'
+ })
+ } else {
+ options.push(
+ {
+ label: i18n.tc('Video.Unsave Video', savedVideosCount, { count: savedVideosCount }),
+ value: 'unsave'
+ },
+ {
+ label: i18n.tc('Video.Save Video', unsavedVideosCount, { count: unsavedVideosCount }),
+ value: 'save'
+ }
+ )
+ }
+ }
+
+ if (watchedVideosCount === count) {
+ options.push({
+ label: i18n.tc('Video.Remove From History', count, { count: count }),
+ value: 'history-remove'
+ })
+ } else if (unwatchedVideosCount === count) {
+ options.push({
+ label: i18n.tc('Video.Mark As Watched', count, { count: count }),
+ value: 'history-add'
+ })
+ } else {
+ options.push(
+ {
+ label: i18n.tc('Video.Remove From History', watchedVideosCount, { count: watchedVideosCount }),
+ value: 'history-remove'
+ },
+ {
+ label: i18n.tc('Video.Mark As Watched', unwatchedVideosCount, { count: unwatchedVideosCount }),
+ value: 'history-add'
+ },
+ )
+ }
+
+ if (!hideSharingActions()) {
+ options.push(
+ {
+ type: 'divider'
+ },
+ {
+ label: i18n.tc('Video.Copy YouTube Link', count, { count: count }),
+ value: 'copyYoutube'
+ },
+ {
+ label: i18n.tc('Video.Copy YouTube Embedded Player Link', count, { count: count }),
+ value: 'copyYoutubeEmbed'
+ },
+ {
+ label: i18n.tc('Video.Copy Invidious Link', count, { count: count }),
+ value: 'copyInvidious'
+ }
+ )
+
+ if (count <= MAX_NEW_TABS) {
+ options.push(
+ {
+ type: 'divider'
+ },
+ {
+ label: i18n.tc('Video.Open in YouTube', count, { count: count }),
+ value: 'openYoutube'
+ },
+ {
+ label: i18n.tc('Video.Open YouTube Embedded Player', count, { count: count }),
+ value: 'openYoutubeEmbed'
+ },
+ {
+ label: i18n.tc('Video.Open in Invidious', count, { count: count }),
+ value: 'openInvidious'
+ }
+ )
+ }
+
+ if (videosWithChannelIdsCount) {
+ options.push(
+ {
+ type: 'divider'
+ },
+ {
+ label: i18n.tc('Video.Copy YouTube Channel Link', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }),
+ value: 'copyYoutubeChannel'
+ },
+ {
+ label: i18n.tc('Video.Copy Invidious Channel Link', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }),
+ value: 'copyInvidiousChannel'
+ }
+ )
+
+ if (videosWithChannelIdsCount <= MAX_NEW_TABS) {
+ options.push(
+ {
+ type: 'divider'
+ },
+ {
+ label: i18n.tc('Video.Open Channel in YouTube', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }),
+ value: 'openYoutubeChannel'
+ },
+ {
+ label: i18n.tc('Video.Open Channel in Invidious', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }),
+ value: 'openInvidiousChannel'
+ }
+ )
+ }
+
+ options.push(
+ {
+ type: 'divider'
+ }
+ )
+
+ if (hiddenChannels.size) {
+ options.push({
+ label: i18n.tc('Video.Unhide Channel', hiddenChannels.size, { count: hiddenChannels.size }),
+ value: 'unhideChannel'
+ })
+ }
+
+ if (unhiddenChannels.size) {
+ options.push({
+ label: i18n.tc('Video.Hide Channel', unhiddenChannels.size, { count: unhiddenChannels.size }),
+ value: 'hideChannel'
+ })
+ }
+ }
+ }
+
+ return options
+}
+
+export function handleVideoDropdownOptionsClick(
+ option,
+ count,
+ watchedVideosCount,
+ unwatchedVideosCount,
+ savedVideosCount,
+ unsavedVideosCount,
+ videosWithChannelIdsCount,
+ hiddenChannels,
+ unhiddenChannels,
+ videoComponents,
+ updateChannelsHidden
+) {
+ let channelsHidden = JSON.parse(store.getters.getChannelsHidden)
+ switch (option) {
+ case 'clear':
+ videoComponents[0].clearSelectionModeSelections()
+ break
+ case 'save':
+ videoComponents.forEach((videoComponent) => videoComponent.setSave(true, true))
+ showToast(i18n.tc('Video.Video has been saved', unsavedVideosCount, { count: unsavedVideosCount }))
+ break
+ case 'unsave':
+ videoComponents.forEach((videoComponent) => videoComponent.setSave(false, true))
+ showToast(i18n.tc('Video.Video has been removed from your history', savedVideosCount, { count: savedVideosCount }))
+ break
+ case 'history-add':
+ videoComponents.forEach((videoComponent) => videoComponent.markAsWatched(true))
+ showToast(i18n.tc('Video.Video has been marked as watched', unwatchedVideosCount, { count: unwatchedVideosCount }))
+ break
+ case 'history-remove':
+ videoComponents.forEach((videoComponent) => videoComponent.removeFromWatched(true))
+ showToast(i18n.tc('Video.Video has been removed from your history', watchedVideosCount, { count: watchedVideosCount }))
+ break
+ case 'copyYoutube':
+ copyToClipboard(
+ videoComponents.map(videoComponent => videoComponent.youtubeShareUrl).join('\n'),
+ { messageOnSuccess: i18n.tc('Share.YouTube URL copied to clipboard', count, { count: count }) })
+ break
+ case 'openYoutube':
+ videoComponents.forEach((videoComponent) => openExternalLink(videoComponent.youtubeUrl))
+ break
+ case 'copyYoutubeEmbed':
+ copyToClipboard(
+ videoComponents.map(videoComponent => videoComponent.youtubeEmbedUrl).join('\n'),
+ { messageOnSuccess: i18n.tc('Share.YouTube Embed URL copied to clipboard', count, { count: count }) })
+ break
+ case 'openYoutubeEmbed':
+ videoComponents.forEach((videoComponent) => openExternalLink(videoComponent.youtubeEmbedUrl))
+ break
+ case 'copyInvidious':
+ copyToClipboard(
+ videoComponents.map(videoComponent => videoComponent.invidiousUrl).join('\n'),
+ { messageOnSuccess: i18n.tc('Share.Invidious URL copied to clipboard', count, { count: count }) })
+ break
+ case 'openInvidious':
+ videoComponents.forEach((videoComponent) => openExternalLink(videoComponent.invidiousUrl))
+ break
+ case 'copyYoutubeChannel':
+ copyToClipboard(
+ videoComponents.filter(videoComponent => videoComponent.channelId !== null)
+ .map(videoComponent => videoComponent.youtubeChannelUrl).join('\n'),
+ { messageOnSuccess: i18n.tc('Share.YouTube Channel URL copied to clipboard', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }) })
+ break
+ case 'openYoutubeChannel':
+ videoComponents.filter(videoComponent => videoComponent.channelId !== null)
+ .forEach((videoComponent) => openExternalLink(videoComponent.youtubeChannelUrl))
+ break
+ case 'copyInvidiousChannel':
+ copyToClipboard(
+ videoComponents.filter(videoComponent => videoComponent.channelId !== null)
+ .map(videoComponent => videoComponent.invidiousChannelUrl).join('\n'),
+ { messageOnSuccess: i18n.tc('Share.Invidious Channel URL copied to clipboard', videosWithChannelIdsCount, { count: videosWithChannelIdsCount }) }
+ )
+ break
+ case 'openInvidiousChannel':
+ videoComponents.filter(videoComponent => videoComponent.channelId !== null)
+ .forEach((videoComponent) => openExternalLink(videoComponent.invidiousChannelUrl))
+ break
+ case 'hideChannel':
+ unhiddenChannels.forEach((channelId) => channelsHidden.push(channelId))
+ updateChannelsHidden(JSON.stringify(channelsHidden))
+ showToast(i18n.tc('Video.Channel Hidden', unhiddenChannels.size, { count: unhiddenChannels.size }))
+ break
+ case 'unhideChannel':
+ channelsHidden = channelsHidden.filter((channel) => !hiddenChannels.has(channel.name))
+ updateChannelsHidden(JSON.stringify(channelsHidden))
+ showToast(i18n.tc('Video.Channel Unhidden', hiddenChannels.size, { count: hiddenChannels.size }))
+ break
+ }
+}
+
/**
* Check if the `name` of the error is `TimeoutError` to know if the error was caused by a timeout or something else.
* @param {number} timeoutMs
diff --git a/src/renderer/main.js b/src/renderer/main.js
index 43e30da39ac2e..dce57a1933e44 100644
--- a/src/renderer/main.js
+++ b/src/renderer/main.js
@@ -57,6 +57,7 @@ import {
faShareAlt,
faSlidersH,
faSortDown,
+ faSquareCheck,
faStar,
faStepBackward,
faStepForward,
@@ -130,6 +131,7 @@ library.add(
faShareAlt,
faSlidersH,
faSortDown,
+ faSquareCheck,
faStar,
faStepBackward,
faStepForward,
diff --git a/src/renderer/scss-partials/_ft-list-item.scss b/src/renderer/scss-partials/_ft-list-item.scss
index 6537e99f14d00..5bb64237e8381 100644
--- a/src/renderer/scss-partials/_ft-list-item.scss
+++ b/src/renderer/scss-partials/_ft-list-item.scss
@@ -54,7 +54,7 @@ $watched-transition-duration: 0.5s;
.ft-list-item {
padding: 6px;
- &.watched {
+ &.watched:not(.selectedInSelectionMode) {
@include low-contrast-when-watched(var(--primary-text-color));
background-color: var(--bg-color);
@@ -71,6 +71,25 @@ $watched-transition-duration: 0.5s;
}
}
+ &.selectedInSelectionMode {
+ user-select: none;
+ -webkit-user-select: none;
+ background-color: var(--primary-text-color) !important;
+ transition-duration: $watched-transition-duration;
+
+ &, .info .title, .info div.infoLine, .info div.infoLine .channelName, .info .description {
+ color: var(--card-bg-color) !important;
+ transition-duration: $watched-transition-duration;
+ }
+
+ &:hover, &:focus {
+ transition-duration: $watched-transition-duration;
+ background-color: var(--tertiary-text-color) !important;
+ // & .title, &.infoLine {
+ // }
+ }
+ }
+
.videoThumbnail {
display: grid;
@@ -337,6 +356,16 @@ $watched-transition-duration: 0.5s;
}
}
+ &.isSelectableInSelectionMode * {
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-select: none;
+
+ .ftIconButton {
+ pointer-events: initial;
+ }
+ }
+
.favoritesIcon,
.externalPlayerIcon {
opacity: $thumbnail-overlay-opacity;
diff --git a/src/renderer/store/modules/utils.js b/src/renderer/store/modules/utils.js
index 4ec7a5df6bbad..439bf3cc4dadd 100644
--- a/src/renderer/store/modules/utils.js
+++ b/src/renderer/store/modules/utils.js
@@ -1,7 +1,7 @@
import fs from 'fs/promises'
import path from 'path'
import i18n from '../../i18n/index'
-import { set as vueSet } from 'vue'
+import { set as vueSet, del as vueDel } from 'vue'
import { IpcChannels } from '../../../constants'
import { pathExists } from '../../helpers/filesystem'
@@ -44,7 +44,13 @@ const state = {
externalPlayerNames: [],
externalPlayerNameTranslationKeys: [],
externalPlayerValues: [],
- externalPlayerCmdArguments: {}
+ externalPlayerCmdArguments: {},
+ isSelectionModeEnabled: false,
+ selectAllVideosInSelectionModeKey: 0,
+ unselectAllVideosInSelectionModeKey: 0,
+ // Vuex doesn't support Maps, so we have to use an object here instead
+ // TODO: switch to a Map during the Pinia migration
+ selectionModeSelections: { index: 0, selections: {} },
}
const getters = {
@@ -118,6 +124,26 @@ const getters = {
getExternalPlayerCmdArguments () {
return state.externalPlayerCmdArguments
+ },
+
+ getIsSelectionModeEnabled () {
+ return state.isSelectionModeEnabled
+ },
+
+ getSelectAllInSelectionModeTriggered () {
+ return state.selectAllVideosInSelectionModeKey
+ },
+
+ getUnselectAllInSelectionModeTriggered () {
+ return state.unselectAllVideosInSelectionModeKey
+ },
+
+ getIsIndexSelectedInSelectionMode: (state) => (index) => {
+ return Object.hasOwn(state.selectionModeSelections.selections, index)
+ },
+
+ getSelectionModeSelections () {
+ return state.selectionModeSelections
}
}
@@ -496,6 +522,24 @@ const actions = {
}
},
+ selectAllVideosInSelectionMode({ commit }) {
+ commit('setSelectAllVideosInSelectionMode')
+ },
+
+ clearSelectionModeSelections ({ commit }) {
+ commit('setSelectionModeSelections', { index: 0, selections: {} })
+ },
+
+ addToSelectionModeSelections ({ commit }, selection) {
+ return new Promise((resolve) => {
+ commit('addToSelectionModeSelections', { selection, callback: resolve })
+ })
+ },
+
+ removeFromSelectionModeSelections ({ commit }, selectionIndex) {
+ return commit('removeFromSelectionModeSelections', { selectionIndex })
+ },
+
clearSessionSearchHistory ({ commit }) {
commit('setSessionSearchHistory', [])
},
@@ -758,7 +802,30 @@ const mutations = {
setExternalPlayerCmdArguments (state, value) {
state.externalPlayerCmdArguments = value
- }
+ },
+
+ setSelectionMode (state, value) {
+ state.isSelectionModeEnabled = value
+ },
+
+ setSelectAllVideosInSelectionMode (state) {
+ state.selectAllVideosInSelectionModeKey++
+ },
+
+ setSelectionModeSelections (state, selectionModeSelections) {
+ state.selectionModeSelections = selectionModeSelections
+ state.unselectAllVideosInSelectionModeKey++
+ },
+
+ addToSelectionModeSelections (state, { selection, callback }) {
+ vueSet(state.selectionModeSelections.selections,
+ ++state.selectionModeSelections.index, selection)
+ callback(state.selectionModeSelections.index)
+ },
+
+ removeFromSelectionModeSelections (state, { selectionIndex }) {
+ vueDel(state.selectionModeSelections.selections, selectionIndex)
+ },
}
export default {
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index 1fbe3e902dcd8..50df91bd050b5 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -31,6 +31,9 @@ Close: Close
Back: Back
Forward: Forward
Open New Window: Open New Window
+Enable Video Selection Mode: Enable Video Selection Mode
+Disable Video Selection Mode: Disable Video Selection Mode
+More Options: More Options
Go to page: Go to {page}
Version {versionNumber} is now available! Click for more details: Version {versionNumber} is now available! Click
@@ -618,25 +621,29 @@ Channel:
Reveal Answers: Reveal Answers
Hide Answers: Hide Answers
Video:
- Mark As Watched: Mark As Watched
- Remove From History: Remove From History
- Video has been marked as watched: Video has been marked as watched
- Video has been removed from your history: Video has been removed from your history
- Save Video: Save Video
- Video has been saved: Video has been saved
- Video has been removed from your saved list: Video has been removed from your saved list
- Open in YouTube: Open in YouTube
- Copy YouTube Link: Copy YouTube Link
- Open YouTube Embedded Player: Open YouTube Embedded Player
- Copy YouTube Embedded Player Link: Copy YouTube Embedded Player Link
- Open in Invidious: Open in Invidious
- Copy Invidious Link: Copy Invidious Link
- Open Channel in YouTube: Open Channel in YouTube
- Copy YouTube Channel Link: Copy YouTube Channel Link
- Open Channel in Invidious: Open Channel in Invidious
- Copy Invidious Channel Link: Copy Invidious Channel Link
- Hide Channel: Hide Channel
- Unhide Channel: Show Channel
+ Clear Selection: Clear Selection | Clear {count} Selections
+ Mark As Watched: Mark As Watched | Mark All {count} As Watched
+ Remove From History: Remove From History | Remove All {count} From History
+ Video has been marked as watched: Video has been marked as watched | {count} videos have been marked as watched
+ Video has been removed from your history: Video has been removed from your history | {count} videos have been removed from your history
+ Unsave Video: Unsave Video | Unsave {count} Videos
+ Save Video: Save Video | Save {count} Videos
+ Video has been saved: Video has been saved | {count} videos have been saved
+ Video has been removed from your saved list: Video has been removed from your saved list | {count} videos have been removed from your saved list
+ Open in YouTube: Open in YouTube | Open All {count} in YouTube
+ Copy YouTube Link: Copy YouTube Link | Copy {count} YouTube Links
+ Open YouTube Embedded Player: Open YouTube Embedded Player | Open {count} in YouTube Embedded Player
+ Copy YouTube Embedded Player Link: Copy YouTube Embedded Player Link | Copy {count} YouTube Embedded Player Links
+ Open in Invidious: Open in Invidious | Open All {count} in Invidious
+ Copy Invidious Link: Copy Invidious Link | Copy {count} Invidious Links
+ Open Channel in YouTube: Open Channel in YouTube | Open {count} Channels in YouTube
+ Copy YouTube Channel Link: Copy YouTube Channel Link | Copy {count} YouTube Channel Links
+ Open Channel in Invidious: Open Channel in Invidious | Open {count} Channels in Invidious
+ Copy Invidious Channel Link: Copy Invidious Channel Link | Copy {count} Invidious Channel Links
+ Hide Channel: Hide Channel | Hide {count} Channels
+ Unhide Channel: Show Channel | Show {count} Channels
+ Channel Hidden: Channel added to channel filter | {count} channels added to channel filter
+ Channel Unhidden: Channel removed from channel filter | {count} channels removed from channel filter
Views: Views
Loop Playlist: Loop Playlist
Shuffle Playlist: Shuffle Playlist
@@ -786,12 +793,12 @@ Share:
Copy Embed: Copy Embed
Open Embed: Open Embed
# On Click
- Invidious URL copied to clipboard: Invidious URL copied to clipboard
- Invidious Embed URL copied to clipboard: Invidious Embed URL copied to clipboard
- Invidious Channel URL copied to clipboard: Invidious Channel URL copied to clipboard
- YouTube URL copied to clipboard: YouTube URL copied to clipboard
- YouTube Embed URL copied to clipboard: YouTube Embed URL copied to clipboard
- YouTube Channel URL copied to clipboard: YouTube Channel URL copied to clipboard
+ Invidious URL copied to clipboard: Invidious URL copied to clipboard | {count} Invidious URLs copied to clipboard
+ Invidious Embed URL copied to clipboard: Invidious Embed URL copied to clipboard | {count} Invidious Embed URLs copied to clipboard
+ Invidious Channel URL copied to clipboard: Invidious Channel URL copied to clipboard | {count} Invidious Channel URLs copied to clipboard
+ YouTube URL copied to clipboard: YouTube URL copied to clipboard | {count} YouTube URLs copied to clipboard
+ YouTube Embed URL copied to clipboard: YouTube Embed URL copied to clipboard | {count} YouTube Embed URLs copied to clipboard
+ YouTube Channel URL copied to clipboard: YouTube Channel URL copied to clipboard | {count} YouTube Channel URLs copied to clipboard
Clipboard:
Copy failed: Copy to clipboard failed
Cannot access clipboard without a secure connection: Cannot access clipboard without a secure connection
@@ -934,8 +941,6 @@ Starting download: 'Starting download of "{videoTitle}"'
Downloading failed: 'There was an issue downloading "{videoTitle}"'
Screenshot Success: Saved screenshot as "{filePath}"
Screenshot Error: Screenshot failed. {error}
-Channel Hidden: '{channel} added to channel filter'
-Channel Unhidden: '{channel} removed from channel filter'
Hashtag:
Hashtag: Hashtag