Skip to content

Commit 672803d

Browse files
authored
Add video search in user playlist feature (#4622)
* * Update single playlist view for user playlists to add search video function * ! Fix load more button appears when searching & visible items under pagination limit * * Show message when search returns no result * * Make search button focused after existing search mode * * Make search result show search result show original playlist item indexes * * Make search button only appear with video count > 0
1 parent f54c45a commit 672803d

File tree

8 files changed

+154
-44
lines changed

8 files changed

+154
-44
lines changed

src/renderer/components/ft-icon-button/ft-icon-button.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ export default defineComponent({
128128

129129
handleResize: function () {
130130
this.useModal = window.innerWidth <= 900
131-
}
131+
},
132+
133+
focus() {
134+
// To be called by parent components
135+
this.$refs.iconButton.focus()
136+
},
132137
}
133138
})

src/renderer/components/ft-icon-button/ft-icon-button.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div class="ftIconButton">
33
<font-awesome-icon
4+
ref="iconButton"
45
class="iconButton"
56
:title="title"
67
:icon="icon"

src/renderer/components/playlist-info/playlist-info.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
55
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
66
import FtInput from '../ft-input/ft-input.vue'
77
import FtPrompt from '../ft-prompt/ft-prompt.vue'
8+
import FtButton from '../ft-button/ft-button.vue'
89
import {
910
formatNumber,
1011
showToast,
1112
} from '../../helpers/utils'
13+
import debounce from 'lodash.debounce'
1214

1315
export default defineComponent({
1416
name: 'PlaylistInfo',
@@ -18,6 +20,7 @@ export default defineComponent({
1820
'ft-icon-button': FtIconButton,
1921
'ft-input': FtInput,
2022
'ft-prompt': FtPrompt,
23+
'ft-button': FtButton,
2124
},
2225
props: {
2326
id: {
@@ -83,6 +86,9 @@ export default defineComponent({
8386
},
8487
data: function () {
8588
return {
89+
searchVideoMode: false,
90+
query: '',
91+
updateQueryDebounce: function() {},
8692
editMode: false,
8793
showDeletePlaylistPrompt: false,
8894
showRemoveVideosOnWatchPrompt: false,
@@ -232,6 +238,8 @@ export default defineComponent({
232238
created: function () {
233239
this.newTitle = this.title
234240
this.newDescription = this.description
241+
242+
this.updateQueryDebounce = debounce(this.updateQuery, 500)
235243
},
236244
methods: {
237245
toggleCopyVideosPrompt: function (force = false) {
@@ -373,6 +381,30 @@ export default defineComponent({
373381
showToast(this.$t('User Playlists.SinglePlaylistView.Toast.Quick bookmark disabled'))
374382
},
375383

384+
updateQuery(query) {
385+
this.query = query
386+
this.$emit('search-video-query-change', query)
387+
},
388+
enableVideoSearchMode() {
389+
this.searchVideoMode = true
390+
this.$emit('search-video-mode-on')
391+
392+
nextTick(() => {
393+
// Some elements only present after rendering update
394+
this.$refs.searchInput.focus()
395+
})
396+
},
397+
disableVideoSearchMode() {
398+
this.searchVideoMode = false
399+
this.updateQuery('')
400+
this.$emit('search-video-mode-off')
401+
402+
nextTick(() => {
403+
// Some elements only present after rendering update
404+
this.$refs.enableSearchModeButton?.focus()
405+
})
406+
},
407+
376408
...mapActions([
377409
'showAddToPlaylistPromptForManyVideos',
378410
'updatePlaylist',

src/renderer/components/playlist-info/playlist-info.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@
7373
justify-content: flex-end;
7474
}
7575

76+
.searchInputsRow {
77+
margin-block-start: 8px;
78+
79+
display: grid;
80+
81+
/* 2 columns */
82+
grid-template-columns: 1fr auto;
83+
column-gap: 8px;
84+
}
85+
7686
@media only screen and (max-width: 1250px) {
7787
:deep(.sharePlaylistIcon .iconDropdown) {
7888
inset-inline-start: auto;

src/renderer/components/playlist-info/playlist-info.vue

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
<hr>
7777

7878
<div
79+
v-if="!searchVideoMode"
7980
class="channelShareWrapper"
8081
>
8182
<router-link
@@ -106,6 +107,14 @@
106107
</div>
107108

108109
<div class="playlistOptions">
110+
<ft-icon-button
111+
v-if="isUserPlaylist && videoCount > 0 && !editMode"
112+
ref="enableSearchModeButton"
113+
:title="$t('User Playlists.SinglePlaylistView.Search for Videos')"
114+
:icon="['fas', 'search']"
115+
theme="secondary"
116+
@click="enableVideoSearchMode"
117+
/>
109118
<ft-icon-button
110119
v-if="editMode"
111120
:title="$t('User Playlists.Save Changes')"
@@ -187,6 +196,28 @@
187196
@click="handleRemoveVideosOnWatchPromptAnswer"
188197
/>
189198
</div>
199+
200+
<div
201+
v-if="isUserPlaylist && searchVideoMode"
202+
class="searchInputsRow"
203+
>
204+
<ft-input
205+
ref="searchInput"
206+
class="searchInput"
207+
:placeholder="$t('User Playlists.SinglePlaylistView.Search for Videos')"
208+
:show-clear-text-button="true"
209+
:show-action-button="false"
210+
@input="(input) => updateQueryDebounce(input)"
211+
@clear="updateQueryDebounce('')"
212+
/>
213+
<ft-icon-button
214+
v-if="isUserPlaylist && searchVideoMode"
215+
:title="$t('User Playlists.Cancel')"
216+
:icon="['fas', 'times']"
217+
theme="secondary"
218+
@click="disableVideoSearchMode"
219+
/>
220+
</div>
190221
</div>
191222
</template>
192223

src/renderer/views/Playlist/Playlist.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export default defineComponent({
6060
getPlaylistInfoDebounce: function() {},
6161
playlistInEditMode: false,
6262

63+
playlistInVideoSearchMode: false,
64+
videoSearchQuery: '',
65+
6366
promptOpen: false,
6467
}
6568
},
@@ -104,7 +107,7 @@ export default defineComponent({
104107

105108
moreVideoDataAvailable() {
106109
if (this.isUserPlaylistRequested) {
107-
return this.userPlaylistVisibleLimit < this.videoCount
110+
return this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length
108111
} else {
109112
return this.continuationData !== null
110113
}
@@ -123,17 +126,29 @@ export default defineComponent({
123126
return this.selectedUserPlaylist?._id !== this.quickBookmarkPlaylistId
124127
},
125128

129+
sometimesFilteredUserPlaylistItems() {
130+
if (!this.isUserPlaylistRequested) { return this.playlistItems }
131+
if (this.processedVideoSearchQuery === '') { return this.playlistItems }
132+
133+
return this.playlistItems.filter((v) => {
134+
return v.title.toLowerCase().includes(this.processedVideoSearchQuery)
135+
})
136+
},
126137
visiblePlaylistItems: function () {
127138
if (!this.isUserPlaylistRequested) {
139+
// No filtering for non user playlists yet
128140
return this.playlistItems
129141
}
130142

131-
if (this.userPlaylistVisibleLimit < this.videoCount) {
132-
return this.playlistItems.slice(0, this.userPlaylistVisibleLimit)
143+
if (this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length) {
144+
return this.sometimesFilteredUserPlaylistItems.slice(0, this.userPlaylistVisibleLimit)
133145
} else {
134-
return this.playlistItems
146+
return this.sometimesFilteredUserPlaylistItems
135147
}
136-
}
148+
},
149+
processedVideoSearchQuery() {
150+
return this.videoSearchQuery.trim().toLowerCase()
151+
},
137152
},
138153
watch: {
139154
$route () {

src/renderer/views/Playlist/Playlist.vue

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
}"
2929
@enter-edit-mode="playlistInEditMode = true"
3030
@exit-edit-mode="playlistInEditMode = false"
31+
@search-video-mode-on="playlistInVideoSearchMode = true"
32+
@search-video-mode-off="playlistInVideoSearchMode = false"
33+
@search-video-query-change="(v) => videoSearchQuery = v"
3134
@prompt-open="promptOpen = true"
3235
@prompt-close="promptOpen = false"
3336
/>
@@ -39,48 +42,59 @@
3942
<template
4043
v-if="playlistItems.length > 0"
4144
>
42-
<transition-group
43-
name="playlistItem"
44-
tag="span"
45+
<template
46+
v-if="visiblePlaylistItems.length > 0"
4547
>
46-
<ft-list-video-numbered
47-
v-for="(item, index) in visiblePlaylistItems"
48-
:key="`${item.videoId}-${item.playlistItemId || index}`"
49-
class="playlistItem"
50-
:data="item"
51-
:playlist-id="playlistId"
52-
:playlist-type="infoSource"
53-
:playlist-index="index"
54-
:playlist-item-id="item.playlistItemId"
55-
appearance="result"
56-
:always-show-add-to-playlist-button="true"
57-
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
58-
:can-move-video-up="index > 0"
59-
:can-move-video-down="index < visiblePlaylistItems.length - 1"
60-
:can-remove-from-playlist="true"
61-
:video-index="index"
62-
:initial-visible-state="index < 10"
63-
@move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
64-
@move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
65-
@remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
66-
/>
67-
</transition-group>
48+
<transition-group
49+
name="playlistItem"
50+
tag="span"
51+
>
52+
<ft-list-video-numbered
53+
v-for="(item, index) in visiblePlaylistItems"
54+
:key="`${item.videoId}-${item.playlistItemId || index}`"
55+
class="playlistItem"
56+
:data="item"
57+
:playlist-id="playlistId"
58+
:playlist-type="infoSource"
59+
:playlist-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
60+
:playlist-item-id="item.playlistItemId"
61+
appearance="result"
62+
:always-show-add-to-playlist-button="true"
63+
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
64+
:can-move-video-up="index > 0 && !playlistInVideoSearchMode"
65+
:can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode"
66+
:can-remove-from-playlist="true"
67+
:video-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
68+
:initial-visible-state="index < 10"
69+
@move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
70+
@move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
71+
@remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
72+
/>
73+
</transition-group>
74+
<ft-flex-box
75+
v-if="moreVideoDataAvailable && !isLoadingMore"
76+
>
77+
<ft-button
78+
:label="$t('Subscriptions.Load More Videos')"
79+
background-color="var(--primary-color)"
80+
text-color="var(--text-with-main-color)"
81+
@click="getNextPage"
82+
/>
83+
</ft-flex-box>
84+
<div
85+
v-if="isLoadingMore"
86+
class="loadNextPageWrapper"
87+
>
88+
<ft-loader />
89+
</div>
90+
</template>
6891
<ft-flex-box
69-
v-if="moreVideoDataAvailable && !isLoadingMore"
92+
v-else
7093
>
71-
<ft-button
72-
:label="$t('Subscriptions.Load More Videos')"
73-
background-color="var(--primary-color)"
74-
text-color="var(--text-with-main-color)"
75-
@click="getNextPage"
76-
/>
94+
<p class="message">
95+
{{ $t("User Playlists['Empty Search Message']") }}
96+
</p>
7797
</ft-flex-box>
78-
<div
79-
v-if="isLoadingMore"
80-
class="loadNextPageWrapper"
81-
>
82-
<ft-loader />
83-
</div>
8498
</template>
8599
<ft-flex-box
86100
v-else

static/locales/en-US.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ User Playlists:
187187
EarliestPlayedFirst: 'Earliest Played'
188188

189189
SinglePlaylistView:
190+
Search for Videos: Search for Videos
191+
190192
Toast:
191193
This video cannot be moved up.: This video cannot be moved up.
192194
This video cannot be moved down.: This video cannot be moved down.

0 commit comments

Comments
 (0)