diff --git a/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.css b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.css
new file mode 100644
index 0000000000000..20e216423f167
--- /dev/null
+++ b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.css
@@ -0,0 +1,17 @@
+/*
+ Set a height to invisible/unloaded elements, so that lazy loading actually works.
+ If we don't set a height, they all get a height of 0px (because they have no content),
+ so they all bunch up together and end up loading all of them in one go.
+ */
+.placeholder {
+ block-size: 40px;
+}
+
+.videoIndex {
+ color: var(--tertiary-text-color);
+ text-align: center;
+}
+
+.videoIndexIcon {
+ font-size: 14px;
+}
diff --git a/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.js b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.js
new file mode 100644
index 0000000000000..9ed32bb8e9a89
--- /dev/null
+++ b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.js
@@ -0,0 +1,124 @@
+import { defineComponent } from 'vue'
+import FtListVideo from '../ft-list-video/ft-list-video.vue'
+
+export default defineComponent({
+ name: 'FtListVideoNumbered',
+ components: {
+ 'ft-list-video': FtListVideo
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true
+ },
+ playlistId: {
+ type: String,
+ default: null
+ },
+ playlistType: {
+ type: String,
+ default: null
+ },
+ playlistIndex: {
+ type: Number,
+ default: null
+ },
+ playlistReverse: {
+ type: Boolean,
+ default: false
+ },
+ playlistShuffle: {
+ type: Boolean,
+ default: false
+ },
+ playlistLoop: {
+ type: Boolean,
+ default: false
+ },
+ playlistItemId: {
+ type: String,
+ default: null,
+ },
+ appearance: {
+ type: String,
+ required: true
+ },
+ initialVisibleState: {
+ type: Boolean,
+ default: false,
+ },
+ alwaysShowAddToPlaylistButton: {
+ type: Boolean,
+ default: false,
+ },
+ quickBookmarkButtonEnabled: {
+ type: Boolean,
+ default: true,
+ },
+ canMoveVideoUp: {
+ type: Boolean,
+ default: false,
+ },
+ canMoveVideoDown: {
+ type: Boolean,
+ default: false,
+ },
+ canRemoveFromPlaylist: {
+ type: Boolean,
+ default: false,
+ },
+ videoIndex: {
+ type: Number,
+ default: -1
+ },
+ isCurrentVideo: {
+ type: Boolean,
+ default: false
+ },
+ useChannelsHiddenPreference: {
+ type: Boolean,
+ default: false,
+ }
+ },
+ data: function () {
+ return {
+ visible: false,
+ show: true
+ }
+ },
+ computed: {
+ channelsHidden() {
+ // Some component users like channel view will have this disabled
+ if (!this.useChannelsHiddenPreference) { return [] }
+
+ return JSON.parse(this.$store.getters.getChannelsHidden).map((ch) => {
+ // Legacy support
+ if (typeof ch === 'string') {
+ return { name: ch, preferredName: '', icon: '' }
+ }
+ return ch
+ })
+ },
+
+ // As we only use this component in Playlist and watch-video-playlist,
+ // where title filtering is never desired, we don't have any title filtering logic here,
+ // like we do in ft-list-video-lazy
+
+ shouldBeVisible() {
+ return !(this.channelsHidden.some(ch => ch.name === this.data.authorId) ||
+ this.channelsHidden.some(ch => ch.name === this.data.author))
+ }
+ },
+ created() {
+ this.visible = this.initialVisibleState
+ },
+ methods: {
+ onVisibilityChanged: function (visible) {
+ if (visible && this.shouldBeVisible) {
+ this.visible = visible
+ } else if (visible) {
+ this.show = false
+ }
+ }
+ }
+})
diff --git a/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.vue b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.vue
new file mode 100644
index 0000000000000..745a704fe84c5
--- /dev/null
+++ b/src/renderer/components/ft-list-video-numbered/ft-list-video-numbered.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ {{ videoIndex + 1 }}
+
+
+
+
+
+
+
+
+
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 11c35f95475db..1befb4403e3d2 100644
--- a/src/renderer/components/ft-list-video/ft-list-video.js
+++ b/src/renderer/components/ft-list-video/ft-list-video.js
@@ -97,7 +97,6 @@ export default defineComponent({
lengthSeconds: 0,
duration: '',
description: '',
- watched: false,
watchProgress: 0,
publishedText: '',
isLive: false,
@@ -223,7 +222,7 @@ export default defineComponent({
dropdownOptions: function () {
const options = [
{
- label: this.watched
+ label: this.historyEntryExists
? this.$t('Video.Remove From History')
: this.$t('Video.Mark As Watched'),
value: 'history'
@@ -343,7 +342,7 @@ export default defineComponent({
},
addWatchedStyle: function () {
- return this.watched && !this.inHistory
+ return this.historyEntryExists && !this.inHistory
},
externalPlayer: function () {
@@ -576,7 +575,7 @@ export default defineComponent({
}
this.openInExternalPlayer(payload)
- if (this.saveWatchedProgress && !this.watched) {
+ if (this.saveWatchedProgress && !this.historyEntryExists) {
this.markAsWatched()
}
},
@@ -584,7 +583,7 @@ export default defineComponent({
handleOptionsClick: function (option) {
switch (option) {
case 'history':
- if (this.watched) {
+ if (this.historyEntryExists) {
this.removeFromWatched()
} else {
this.markAsWatched()
@@ -727,8 +726,6 @@ export default defineComponent({
checkIfWatched: function () {
if (this.historyEntryExists) {
- this.watched = true
-
const historyEntry = this.historyEntry
if (this.saveWatchedProgress) {
@@ -744,7 +741,6 @@ export default defineComponent({
this.publishedText = ''
}
} else {
- this.watched = false
this.watchProgress = 0
}
},
@@ -766,8 +762,6 @@ export default defineComponent({
}
this.updateHistory(videoData)
showToast(this.$t('Video.Video has been marked as watched'))
-
- this.watched = true
},
removeFromWatched: function () {
@@ -775,7 +769,6 @@ export default defineComponent({
showToast(this.$t('Video.Video has been removed from your history'))
- this.watched = false
this.watchProgress = 0
},
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 c87cb45c605cc..d8e373d2bc18b 100644
--- a/src/renderer/components/ft-list-video/ft-list-video.vue
+++ b/src/renderer/components/ft-list-video/ft-list-video.vue
@@ -104,7 +104,7 @@
{{ $t("Video.Watched") }}
@@ -129,12 +129,13 @@
{{ channelName }}
-
-
- •
- {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
-
-
+
+ •
+ {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
+
{
this.toggleCopyVideosPrompt(true)
})
@@ -364,6 +381,25 @@ export default defineComponent({
showToast(this.$t('User Playlists.SinglePlaylistView.Toast.Quick bookmark disabled'))
},
+ updateQuery(query) {
+ this.query = query
+ this.$emit('search-video-query-change', query)
+ },
+ enableVideoSearchMode() {
+ this.searchVideoMode = true
+ this.$emit('search-video-mode-on')
+
+ nextTick(() => {
+ // Some elements only present after rendering update
+ this.$refs.searchInput.focus()
+ })
+ },
+ disableVideoSearchMode() {
+ this.searchVideoMode = false
+ this.updateQuery('')
+ this.$emit('search-video-mode-off')
+ },
+
...mapActions([
'showAddToPlaylistPromptForManyVideos',
'updatePlaylist',
diff --git a/src/renderer/components/playlist-info/playlist-info.scss b/src/renderer/components/playlist-info/playlist-info.scss
index 3fb13c0c0fe8a..be8766bfa89c2 100644
--- a/src/renderer/components/playlist-info/playlist-info.scss
+++ b/src/renderer/components/playlist-info/playlist-info.scss
@@ -72,3 +72,13 @@
column-gap: 8px;
justify-content: flex-end;
}
+
+.searchInputsRow {
+ margin-block-start: 8px;
+
+ display: grid;
+
+ /* 2 columns */
+ grid-template-columns: 1fr auto;
+ column-gap: 8px;
+}
diff --git a/src/renderer/components/playlist-info/playlist-info.vue b/src/renderer/components/playlist-info/playlist-info.vue
index 4dd1f1e95bb01..68277fb44430b 100644
--- a/src/renderer/components/playlist-info/playlist-info.vue
+++ b/src/renderer/components/playlist-info/playlist-info.vue
@@ -47,9 +47,9 @@
{{ title }}
- {{ videoCount }} {{ $t("Playlist.Videos") }}
+ {{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }}
- - {{ viewCount }} {{ $t("Playlist.Views") }}
+ - {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
-
@@ -76,6 +76,7 @@
+
+
+
+ updateQueryDebounce(input)"
+ @clear="updateQueryDebounce('')"
+ />
+
+
diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.css b/src/renderer/components/watch-video-playlist/watch-video-playlist.css
index e859e7d676a28..53a8753a8f1ae 100644
--- a/src/renderer/components/watch-video-playlist/watch-video-playlist.css
+++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.css
@@ -85,19 +85,6 @@
transition: background 0.2s ease-in;
}
-.videoIndexContainer {
- text-align: center;
-}
-
-.videoIndex {
- color: var(--tertiary-text-color);
-}
-
-.videoIndexIcon {
- font-size: 14px;
- color: var(--tertiary-text-color);
-}
-
.videoInfo {
margin-inline-start: 30px;
position: relative;
diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.js b/src/renderer/components/watch-video-playlist/watch-video-playlist.js
index a68df182ec136..e5244e2c04364 100644
--- a/src/renderer/components/watch-video-playlist/watch-video-playlist.js
+++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.js
@@ -2,7 +2,7 @@ import { defineComponent, nextTick } from 'vue'
import { mapMutations } from 'vuex'
import FtLoader from '../ft-loader/ft-loader.vue'
import FtCard from '../ft-card/ft-card.vue'
-import FtListVideoLazy from '../ft-list-video-lazy/ft-list-video-lazy.vue'
+import FtListVideoNumbered from '../ft-list-video-numbered/ft-list-video-numbered.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import {
getLocalPlaylist,
@@ -16,7 +16,7 @@ export default defineComponent({
components: {
'ft-loader': FtLoader,
'ft-card': FtCard,
- 'ft-list-video-lazy': FtListVideoLazy,
+ 'ft-list-video-numbered': FtListVideoNumbered
},
props: {
playlistId: {
@@ -473,14 +473,13 @@ export default defineComponent({
parseUserPlaylist: function (playlist, { allowPlayingVideoRemoval = true } = {}) {
this.playlistTitle = playlist.playlistName
this.channelName = ''
- this.channelThumbnail = ''
this.channelId = ''
if (this.playlistItems.length === 0 || allowPlayingVideoRemoval) {
this.playlistItems = playlist.videos
} else {
// `this.currentVideo` relies on `playlistItems`
- const latestPlaylistContainsCurrentVideo = playlist.videos.find(v => v.playlistItemId === this.playlistItemId) != null
+ const latestPlaylistContainsCurrentVideo = playlist.videos.some(v => v.playlistItemId === this.playlistItemId)
// Only update list of videos if latest video list still contains currently playing video
if (latestPlaylistContainsCurrentVideo) {
this.playlistItems = playlist.videos
@@ -513,7 +512,7 @@ export default defineComponent({
const currentVideoItem = (this.$refs.currentVideoItem || [])[0]
if (container != null && currentVideoItem != null) {
// Watch view can be ready sooner than this component
- container.scrollTop = currentVideoItem.offsetTop - container.offsetTop
+ container.scrollTop = currentVideoItem.$el.offsetTop - container.offsetTop
}
},
diff --git a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
index 1b4c611b9446d..2fd2f60f5035d 100644
--- a/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
+++ b/src/renderer/components/watch-video-playlist/watch-video-playlist.vue
@@ -119,41 +119,25 @@
ref="playlistItems"
class="playlistItems"
>
-
-
-
-
- {{ index + 1 }}
-
-
-
-
+ :data="item"
+ :playlist-id="playlistId"
+ :playlist-type="playlistType"
+ :playlist-index="reversePlaylist ? playlistItems.length - index - 1 : index"
+ :playlist-item-id="item.playlistItemId"
+ :playlist-reverse="reversePlaylist"
+ :playlist-shuffle="shuffleEnabled"
+ :playlist-loop="loopEnabled"
+ :video-index="index"
+ :is-current-video="currentVideoIndexZeroBased === index"
+ appearance="watchPlaylistItem"
+ :initial-visible-state="index < (currentVideoIndexZeroBased + 4) && index > (currentVideoIndexZeroBased - 4)"
+ @pause-player="$emit('pause-player')"
+ />
diff --git a/src/renderer/store/modules/playlists.js b/src/renderer/store/modules/playlists.js
index 704d6ebb58887..2d24234a8bd6b 100644
--- a/src/renderer/store/modules/playlists.js
+++ b/src/renderer/store/modules/playlists.js
@@ -63,10 +63,13 @@ const actions = {
payload.createdAt = Date.now()
payload.lastUpdatedAt = Date.now()
// Ensure all videos has required attributes
+
+ const currentTime = new Date().getTime()
+
if (Array.isArray(payload.videos)) {
payload.videos.forEach(videoData => {
if (videoData.timeAdded == null) {
- videoData.timeAdded = new Date().getTime()
+ videoData.timeAdded = currentTime
}
if (videoData.playlistItemId == null) {
videoData.playlistItemId = generateRandomUniqueId()
@@ -149,11 +152,14 @@ const actions = {
// Since this action will ensure uniqueness of `playlistItemId` of added video entries
try {
const { _id, videos } = payload
+
+ const currentTime = new Date().getTime()
+
const newVideoObjects = videos.map((video) => {
// Create a new object to prevent changing existing values outside
const videoData = Object.assign({}, video)
if (videoData.timeAdded == null) {
- videoData.timeAdded = new Date().getTime()
+ videoData.timeAdded = currentTime
}
videoData.playlistItemId = generateRandomUniqueId()
// For backward compatibility
@@ -188,6 +194,9 @@ const actions = {
dispatch('addPlaylist', playlist)
})
} else {
+ const dateNow = Date.now()
+ const currentTime = new Date().getTime()
+
payload.forEach((playlist) => {
let anythingUpdated = false
// Assign generated playlist ID in case DB data corrupted
@@ -205,19 +214,19 @@ const actions = {
// Assign current time as created time in case DB data corrupted
if (playlist.createdAt == null) {
// Time now in unix time, in ms
- playlist.createdAt = Date.now()
+ playlist.createdAt = dateNow
anythingUpdated = true
}
// Assign current time as last updated time in case DB data corrupted
if (playlist.lastUpdatedAt == null) {
// Time now in unix time, in ms
- playlist.lastUpdatedAt = Date.now()
+ playlist.lastUpdatedAt = dateNow
anythingUpdated = true
}
playlist.videos.forEach((v) => {
// Ensure all videos has `timeAdded` property
if (v.timeAdded == null) {
- v.timeAdded = new Date().getTime()
+ v.timeAdded = currentTime
anythingUpdated = true
}
@@ -257,8 +266,9 @@ const actions = {
return playlist.playlistName === 'Watch Later' || playlist._id === 'watchLater'
})
- const defaultFavoritesPlaylist = state.defaultPlaylists.find((e) => e._id === 'favorites')
if (favoritesPlaylist != null) {
+ const defaultFavoritesPlaylist = state.defaultPlaylists.find((e) => e._id === 'favorites')
+
// Update existing matching playlist only if it exists
if (favoritesPlaylist._id !== defaultFavoritesPlaylist._id || favoritesPlaylist.protected !== defaultFavoritesPlaylist.protected) {
const oldId = favoritesPlaylist._id
@@ -277,8 +287,9 @@ const actions = {
}
}
- const defaultWatchLaterPlaylist = state.defaultPlaylists.find((e) => e._id === 'watchLater')
if (watchLaterPlaylist != null) {
+ const defaultWatchLaterPlaylist = state.defaultPlaylists.find((e) => e._id === 'watchLater')
+
// Update existing matching playlist only if it exists
if (watchLaterPlaylist._id !== defaultWatchLaterPlaylist._id || watchLaterPlaylist.protected !== defaultWatchLaterPlaylist.protected) {
const oldId = watchLaterPlaylist._id
@@ -394,7 +405,7 @@ const mutations = {
addVideos(state, payload) {
const playlist = state.playlists.find(playlist => playlist._id === payload._id)
if (playlist) {
- playlist.videos = [].concat(playlist.videos).concat(payload.videos)
+ playlist.videos = [].concat(playlist.videos, payload.videos)
}
},
diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js
index 826f034494ec3..31e7532dc9d39 100644
--- a/src/renderer/views/Playlist/Playlist.js
+++ b/src/renderer/views/Playlist/Playlist.js
@@ -4,7 +4,7 @@ import debounce from 'lodash.debounce'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue'
import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
-import FtListVideoLazy from '../../components/ft-list-video-lazy/ft-list-video-lazy.vue'
+import FtListVideoNumbered from '../../components/ft-list-video-numbered/ft-list-video-numbered.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import {
@@ -21,12 +21,12 @@ export default defineComponent({
'ft-loader': FtLoader,
'ft-card': FtCard,
'playlist-info': PlaylistInfo,
- 'ft-list-video-lazy': FtListVideoLazy,
+ 'ft-list-video-numbered': FtListVideoNumbered,
'ft-flex-box': FtFlexBox,
'ft-button': FtButton
},
beforeRouteLeave(to, from, next) {
- if (!this.isLoading && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
+ if (!this.isLoading && !this.isUserPlaylistRequested && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
this.setCachedPlaylist({
id: this.playlistId,
title: this.playlistTitle,
@@ -54,11 +54,15 @@ export default defineComponent({
channelId: '',
infoSource: 'local',
playlistItems: [],
+ userPlaylistVisibleLimit: 100,
continuationData: null,
isLoadingMore: false,
getPlaylistInfoDebounce: function() {},
playlistInEditMode: false,
+ playlistInVideoSearchMode: false,
+ videoSearchQuery: '',
+
promptOpen: false,
}
},
@@ -102,7 +106,11 @@ export default defineComponent({
},
moreVideoDataAvailable() {
- return this.continuationData !== null
+ if (this.isUserPlaylistRequested) {
+ return this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length
+ } else {
+ return this.continuationData !== null
+ }
},
isUserPlaylistRequested: function () {
@@ -117,6 +125,30 @@ export default defineComponent({
return this.selectedUserPlaylist?._id !== this.quickBookmarkPlaylistId
},
+
+ sometimesFilteredUserPlaylistItems() {
+ if (!this.isUserPlaylistRequested) { return this.playlistItems }
+ if (this.processedVideoSearchQuery === '') { return this.playlistItems }
+
+ return this.playlistItems.filter((v) => {
+ return v.title.toLowerCase().includes(this.processedVideoSearchQuery)
+ })
+ },
+ visiblePlaylistItems: function () {
+ if (!this.isUserPlaylistRequested) {
+ // No filtering for non user playlists yet
+ return this.playlistItems
+ }
+
+ if (this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length) {
+ return this.sometimesFilteredUserPlaylistItems.slice(0, this.userPlaylistVisibleLimit)
+ } else {
+ return this.sometimesFilteredUserPlaylistItems
+ }
+ },
+ processedVideoSearchQuery() {
+ return this.videoSearchQuery.trim().toLowerCase()
+ },
},
watch: {
$route () {
@@ -147,8 +179,10 @@ export default defineComponent({
this.getPlaylistInfoDebounce()
},
},
- mounted: function () {
+ created: function () {
this.getPlaylistInfoDebounce = debounce(this.getPlaylistInfo, 100)
+ },
+ mounted: function () {
this.getPlaylistInfoDebounce()
},
methods: {
@@ -250,7 +284,7 @@ export default defineComponent({
const dateString = new Date(result.updated * 1000)
this.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' })
- this.playlistItems = this.playlistItems.concat(result.videos)
+ this.allPlaylistItems = result.videos
this.isLoading = false
}).catch((err) => {
@@ -298,6 +332,20 @@ export default defineComponent({
case 'local':
this.getNextPageLocal()
break
+ case 'user':
+ // Stop users from spamming the load more button, by replacing it with a loading symbol until the newly added items are renderered
+ this.isLoadingMore = true
+
+ setTimeout(() => {
+ if (this.userPlaylistVisibleLimit + 100 < this.videoCount) {
+ this.userPlaylistVisibleLimit += 100
+ } else {
+ this.userPlaylistVisibleLimit = this.videoCount
+ }
+
+ this.isLoadingMore = false
+ })
+ break
case 'invidious':
console.error('Playlist pagination is not currently supported when the Invidious backend is selected.')
break
diff --git a/src/renderer/views/Playlist/Playlist.scss b/src/renderer/views/Playlist/Playlist.scss
index 13fd09464488b..e8c2b7681d952 100644
--- a/src/renderer/views/Playlist/Playlist.scss
+++ b/src/renderer/views/Playlist/Playlist.scss
@@ -62,11 +62,6 @@
transform: translate(calc(10% * var(--horizontal-directionality-coefficient)));
}
-.videoIndex {
- color: var(--tertiary-text-color);
- text-align: center;
-}
-
.loadNextPageWrapper {
/* about the same height as the button */
max-block-size: 7vh;
diff --git a/src/renderer/views/Playlist/Playlist.vue b/src/renderer/views/Playlist/Playlist.vue
index ef3efef0ce00e..40c6ae5682673 100644
--- a/src/renderer/views/Playlist/Playlist.vue
+++ b/src/renderer/views/Playlist/Playlist.vue
@@ -28,6 +28,9 @@
}"
@enter-edit-mode="playlistInEditMode = true"
@exit-edit-mode="playlistInEditMode = false"
+ @search-video-mode-on="playlistInVideoSearchMode = true"
+ @search-video-mode-off="playlistInVideoSearchMode = false"
+ @search-video-query-change="(v) => videoSearchQuery = v"
@prompt-open="promptOpen = true"
@prompt-close="promptOpen = false"
/>
@@ -36,42 +39,34 @@
v-if="!isLoading"
class="playlistItems"
>
-
-
-
- {{ index + 1 }}
-
-
-
+ :data="item"
+ :playlist-id="playlistId"
+ :playlist-type="infoSource"
+ :playlist-index="index"
+ :playlist-item-id="item.playlistItemId"
+ appearance="result"
+ :always-show-add-to-playlist-button="true"
+ :quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
+ :can-move-video-up="index > 0 && !playlistInVideoSearchMode"
+ :can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode"
+ :can-remove-from-playlist="true"
+ :video-index="index"
+ :initial-visible-state="index < 10"
+ @move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
+ @move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
+ @remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
+ />
-
+
diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml
index b8fc52b81a3b5..78a7ad475be6a 100644
--- a/static/locales/en-US.yaml
+++ b/static/locales/en-US.yaml
@@ -186,6 +186,8 @@ User Playlists:
EarliestPlayedFirst: 'Earliest Played'
SinglePlaylistView:
+ Search for Videos: Search for Videos
+
Toast:
This video cannot be moved up.: This video cannot be moved up.
This video cannot be moved down.: This video cannot be moved down.
diff --git a/static/locales/hr.yaml b/static/locales/hr.yaml
index e9dbfe204615e..a57585b8ac84d 100644
--- a/static/locales/hr.yaml
+++ b/static/locales/hr.yaml
@@ -548,6 +548,7 @@ Settings:
provjeri točnost ID-a.
Hide Videos and Playlists Containing Text Placeholder: Riječ, fragment riječi
ili fraza
+ Hide Videos and Playlists Containing Text: Sakrij videa i zbirke koji sadrže tekst
The app needs to restart for changes to take effect. Restart and apply change?: Promjene
će se primijeniti nakon ponovnog pokeretanja programa. Ponovo pokrenuti program?
Proxy Settings:
@@ -1117,6 +1118,10 @@ Tooltips:
se mora potpuno poklapati i razlikuje velika i mala slova.
Hide Subscriptions Live: Ovu postavku nadjačava aplikacijska postavka „{appWideSetting}”,
u odjeljku „{subsection}” u „{settingsSection}”
+ Hide Videos and Playlists Containing Text: Upiši riječ, fragment riječi ili izraz
+ (ne razlikuje velika i mala slova) za skrivanje svih videa i zbirki s tim sadržajem
+ u njihovim izvornim naslovima u cijelom FreeTubeu, isključujući samo povijest,
+ tvoje zbirke i videa unutar zbirki.
SponsorBlock Settings:
UseDeArrowTitles: Zamijeni naslove videa koje su poslali korisnici s DeArrow naslovima.
UseDeArrowThumbnails: Zamijeni minijature videa s DeArrow minijaturama.