Add search playlists with matching videos function (#4537)

* * Update user playlists page to add search playlists with matching videos function

* * Update add videos to playlists prompt to add search playlists with matching videos function

* * Update UI & label text

* * Click on playlist link with search matching video enabled now also search for video when view switched

* * Only auto enable search video mode for playlists with video(s)

* * Make new toggle vertically align center

* * Make new toggle vertically align center
This commit is contained in:
PikachuEXE 2024-03-15 05:16:15 +08:00 committed by GitHub
parent 68c74ea2b6
commit 65a5b0c045
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 217 additions and 48 deletions

View File

@ -33,7 +33,12 @@ export default defineComponent({
hideForbiddenTitles: { hideForbiddenTitles: {
type: Boolean, type: Boolean,
default: true default: true
} },
searchQueryText: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
listType: function () { listType: function () {

View File

@ -13,6 +13,7 @@
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist" :show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
:use-channels-hidden-preference="useChannelsHiddenPreference" :use-channels-hidden-preference="useChannelsHiddenPreference"
:hide-forbidden-titles="hideForbiddenTitles" :hide-forbidden-titles="hideForbiddenTitles"
:search-query-text="searchQueryText"
/> />
</ft-auto-grid> </ft-auto-grid>
</template> </template>

View File

@ -47,6 +47,11 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true default: true
}, },
searchQueryText: {
type: String,
required: false,
default: '',
},
}, },
data: function () { data: function () {
return { return {

View File

@ -28,6 +28,7 @@
v-else-if="finalDataType === 'playlist'" v-else-if="finalDataType === 'playlist'"
:appearance="appearance" :appearance="appearance"
:data="data" :data="data"
:search-query-text="searchQueryText"
/> />
<ft-community-post <ft-community-post
v-else-if="finalDataType === 'community'" v-else-if="finalDataType === 'community'"

View File

@ -15,7 +15,12 @@ export default defineComponent({
appearance: { appearance: {
type: String, type: String,
required: true required: true
} },
searchQueryText: {
type: String,
required: false,
default: '',
},
}, },
data: function () { data: function () {
return { return {
@ -79,6 +84,7 @@ export default defineComponent({
path: `/playlist/${this.playlistId}`, path: `/playlist/${this.playlistId}`,
query: { query: {
playlistType: this.isUserPlaylist ? 'user' : '', playlistType: this.isUserPlaylist ? 'user' : '',
searchQueryText: this.searchQueryText,
}, },
} }
}, },

View File

@ -16,6 +16,36 @@
flex-direction: column; flex-direction: column;
} }
.searchInputsRow {
display: grid;
/* 2 columns */
grid-template-columns: 1fr auto;
column-gap: 16px;
}
@media only screen and (max-width: 800px) {
.searchInputsRow {
/* Switch to 2 rows from 2 columns */
grid-template-columns: auto;
grid-template-rows: auto auto;
}
}
.optionsRow {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr;
align-items: center;
}
@media only screen and (max-width: 800px) {
.optionsRow {
/* Switch to 2 rows from 2 columns */
grid-template-columns: auto;
grid-template-rows: auto auto;
align-items: stretch;
}
}
.sortSelect { .sortSelect {
/* Put it on the right */ /* Put it on the right */
margin-inline-start: auto; margin-inline-start: auto;

View File

@ -7,6 +7,7 @@ import FtButton from '../ft-button/ft-button.vue'
import FtPlaylistSelector from '../ft-playlist-selector/ft-playlist-selector.vue' import FtPlaylistSelector from '../ft-playlist-selector/ft-playlist-selector.vue'
import FtInput from '../../components/ft-input/ft-input.vue' import FtInput from '../../components/ft-input/ft-input.vue'
import FtSelect from '../../components/ft-select/ft-select.vue' import FtSelect from '../../components/ft-select/ft-select.vue'
import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue'
import { import {
showToast, showToast,
} from '../../helpers/utils' } from '../../helpers/utils'
@ -31,12 +32,14 @@ export default defineComponent({
'ft-playlist-selector': FtPlaylistSelector, 'ft-playlist-selector': FtPlaylistSelector,
'ft-input': FtInput, 'ft-input': FtInput,
'ft-select': FtSelect, 'ft-select': FtSelect,
'ft-toggle-switch': FtToggleSwitch,
}, },
data: function () { data: function () {
return { return {
selectedPlaylistIdList: [], selectedPlaylistIdList: [],
createdSincePromptShownPlaylistIdList: [], createdSincePromptShownPlaylistIdList: [],
query: '', query: '',
doSearchPlaylistsWithMatchingVideos: false,
updateQueryDebounce: function() {}, updateQueryDebounce: function() {},
lastShownAt: Date.now(), lastShownAt: Date.now(),
lastActiveElement: null, lastActiveElement: null,
@ -115,6 +118,12 @@ export default defineComponent({
return this.allPlaylists.filter((playlist) => { return this.allPlaylists.filter((playlist) => {
if (typeof (playlist.playlistName) !== 'string') { return false } if (typeof (playlist.playlistName) !== 'string') { return false }
if (this.doSearchPlaylistsWithMatchingVideos) {
if (playlist.videos.some((v) => v.title.toLowerCase().includes(this.processedQuery))) {
return true
}
}
return playlist.playlistName.toLowerCase().includes(this.processedQuery) return playlist.playlistName.toLowerCase().includes(this.processedQuery)
}) })
}, },

View File

@ -12,23 +12,37 @@
playlistCount: selectedPlaylistCount, playlistCount: selectedPlaylistCount,
}) }} }) }}
</p> </p>
<ft-input <div
ref="searchBar" class="searchInputsRow"
:placeholder="$t('User Playlists.AddVideoPrompt.Search in Playlists')" >
:show-clear-text-button="true" <ft-input
:show-action-button="false" ref="searchBar"
@input="(input) => updateQueryDebounce(input)" :placeholder="$t('User Playlists.AddVideoPrompt.Search in Playlists')"
@clear="updateQueryDebounce('')" :show-clear-text-button="true"
/> :show-action-button="false"
<ft-select @input="(input) => updateQueryDebounce(input)"
v-if="allPlaylists.length > 1" @clear="updateQueryDebounce('')"
class="sortSelect" />
:value="sortBy" </div>
:select-names="sortBySelectNames" <div
:select-values="sortBySelectValues" class="optionsRow"
:placeholder="$t('User Playlists.Sort By.Sort By')" >
@change="sortBy = $event" <ft-toggle-switch
/> :label="$t('User Playlists.Playlists with Matching Videos')"
:compact="true"
:default-value="doSearchPlaylistsWithMatchingVideos"
@change="doSearchPlaylistsWithMatchingVideos = !doSearchPlaylistsWithMatchingVideos"
/>
<ft-select
v-if="allPlaylists.length > 1"
class="sortSelect"
:value="sortBy"
:select-names="sortBySelectNames"
:select-values="sortBySelectValues"
:placeholder="$t('User Playlists.Sort By.Sort By')"
@change="sortBy = $event"
/>
</div>
<div class="playlists-container"> <div class="playlists-container">
<ft-flex-box> <ft-flex-box>
<div <div

View File

@ -83,6 +83,18 @@ export default defineComponent({
type: Boolean, type: Boolean,
required: true, required: true,
}, },
searchVideoModeAllowed: {
type: Boolean,
required: true,
},
searchVideoModeEnabled: {
type: Boolean,
required: true,
},
searchQueryText: {
type: String,
required: true,
},
}, },
data: function () { data: function () {
return { return {
@ -239,6 +251,12 @@ export default defineComponent({
this.newTitle = this.title this.newTitle = this.title
this.newDescription = this.description this.newDescription = this.description
if (this.videoCount > 0) {
// Only enable search video mode when viewing non empty playlists
this.searchVideoMode = this.searchVideoModeEnabled
this.query = this.searchQueryText
}
this.updateQueryDebounce = debounce(this.updateQuery, 500) this.updateQueryDebounce = debounce(this.updateQuery, 500)
}, },
methods: { methods: {

View File

@ -108,7 +108,7 @@
<div class="playlistOptions"> <div class="playlistOptions">
<ft-icon-button <ft-icon-button
v-if="isUserPlaylist && videoCount > 0 && !editMode" v-if="searchVideoModeAllowed && videoCount > 0 && !editMode"
ref="enableSearchModeButton" ref="enableSearchModeButton"
:title="$t('User Playlists.SinglePlaylistView.Search for Videos')" :title="$t('User Playlists.SinglePlaylistView.Search for Videos')"
:icon="['fas', 'search']" :icon="['fas', 'search']"
@ -198,7 +198,7 @@
</div> </div>
<div <div
v-if="isUserPlaylist && searchVideoMode" v-if="searchVideoModeAllowed && searchVideoMode"
class="searchInputsRow" class="searchInputsRow"
> >
<ft-input <ft-input
@ -207,11 +207,11 @@
:placeholder="$t('User Playlists.SinglePlaylistView.Search for Videos')" :placeholder="$t('User Playlists.SinglePlaylistView.Search for Videos')"
:show-clear-text-button="true" :show-clear-text-button="true"
:show-action-button="false" :show-action-button="false"
:value="query"
@input="(input) => updateQueryDebounce(input)" @input="(input) => updateQueryDebounce(input)"
@clear="updateQueryDebounce('')" @clear="updateQueryDebounce('')"
/> />
<ft-icon-button <ft-icon-button
v-if="isUserPlaylist && searchVideoMode"
:title="$t('User Playlists.Cancel')" :title="$t('User Playlists.Cancel')"
:icon="['fas', 'times']" :icon="['fas', 'times']"
theme="secondary" theme="secondary"

View File

@ -113,6 +113,17 @@ export default defineComponent({
} }
}, },
searchVideoModeAllowed() {
return this.isUserPlaylistRequested
},
searchQueryTextRequested() {
return this.$route.query.searchQueryText
},
searchQueryTextPresent() {
const searchQueryText = this.searchQueryTextRequested
return typeof searchQueryText === 'string' && searchQueryText !== ''
},
isUserPlaylistRequested: function () { isUserPlaylistRequested: function () {
return this.$route.query.playlistType === 'user' return this.$route.query.playlistType === 'user'
}, },
@ -181,6 +192,11 @@ export default defineComponent({
}, },
created: function () { created: function () {
this.getPlaylistInfoDebounce = debounce(this.getPlaylistInfo, 100) this.getPlaylistInfoDebounce = debounce(this.getPlaylistInfo, 100)
if (this.searchVideoModeAllowed && this.searchQueryTextPresent) {
this.playlistInVideoSearchMode = true
this.videoSearchQuery = this.searchQueryTextRequested
}
}, },
mounted: function () { mounted: function () {
this.getPlaylistInfoDebounce() this.getPlaylistInfoDebounce()

View File

@ -22,6 +22,9 @@
:view-count="viewCount" :view-count="viewCount"
:info-source="infoSource" :info-source="infoSource"
:more-video-data-available="moreVideoDataAvailable" :more-video-data-available="moreVideoDataAvailable"
:search-video-mode-allowed="searchVideoModeAllowed"
:search-video-mode-enabled="playlistInVideoSearchMode"
:search-query-text="searchQueryTextRequested"
class="playlistInfo" class="playlistInfo"
:class="{ :class="{
promptOpen, promptOpen,

View File

@ -16,6 +16,36 @@
vertical-align: middle; vertical-align: middle;
} }
.searchInputsRow {
display: grid;
/* 2 columns */
grid-template-columns: 1fr auto;
column-gap: 16px;
}
@media only screen and (max-width: 800px) {
.searchInputsRow {
/* Switch to 2 rows from 2 columns */
grid-template-columns: auto;
grid-template-rows: auto auto;
}
}
.optionsRow {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr;
align-items: center;
}
@media only screen and (max-width: 800px) {
.optionsRow {
/* Switch to 2 rows from 2 columns */
grid-template-columns: auto;
grid-template-rows: auto auto;
align-items: stretch;
}
}
.sortSelect { .sortSelect {
/* Put it on the right */ /* Put it on the right */
margin-inline-start: auto; margin-inline-start: auto;

View File

@ -10,6 +10,7 @@ import FtSelect from '../../components/ft-select/ft-select.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtInput from '../../components/ft-input/ft-input.vue' import FtInput from '../../components/ft-input/ft-input.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue' import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import FtToggleSwitch from '../../components/ft-toggle-switch/ft-toggle-switch.vue'
const SORT_BY_VALUES = { const SORT_BY_VALUES = {
NameAscending: 'name_ascending', NameAscending: 'name_ascending',
@ -37,6 +38,7 @@ export default defineComponent({
'ft-element-list': FtElementList, 'ft-element-list': FtElementList,
'ft-icon-button': FtIconButton, 'ft-icon-button': FtIconButton,
'ft-input': FtInput, 'ft-input': FtInput,
'ft-toggle-switch': FtToggleSwitch,
}, },
data: function () { data: function () {
return { return {
@ -45,6 +47,7 @@ export default defineComponent({
searchDataLimit: 100, searchDataLimit: 100,
showLoadMoreButton: false, showLoadMoreButton: false,
query: '', query: '',
doSearchPlaylistsWithMatchingVideos: false,
activeData: [], activeData: [],
sortBy: SORT_BY_VALUES.LatestPlayedFirst, sortBy: SORT_BY_VALUES.LatestPlayedFirst,
} }
@ -165,6 +168,10 @@ export default defineComponent({
this.searchDataLimit = 100 this.searchDataLimit = 100
this.filterPlaylistAsync() this.filterPlaylistAsync()
}, },
doSearchPlaylistsWithMatchingVideos() {
this.searchDataLimit = 100
this.filterPlaylistAsync()
},
fullData() { fullData() {
this.activeData = this.fullData this.activeData = this.fullData
this.filterPlaylist() this.filterPlaylist()
@ -209,15 +216,22 @@ export default defineComponent({
if (this.lowerCaseQuery === '') { if (this.lowerCaseQuery === '') {
this.activeData = this.fullData this.activeData = this.fullData
this.showLoadMoreButton = this.allPlaylists.length > this.activeData.length this.showLoadMoreButton = this.allPlaylists.length > this.activeData.length
} else { return
const filteredPlaylists = this.allPlaylists.filter((playlist) => {
if (typeof (playlist.playlistName) !== 'string') { return false }
return playlist.playlistName.toLowerCase().includes(this.lowerCaseQuery)
})
this.showLoadMoreButton = filteredPlaylists.length > this.searchDataLimit
this.activeData = filteredPlaylists.length < this.searchDataLimit ? filteredPlaylists : filteredPlaylists.slice(0, this.searchDataLimit)
} }
const filteredPlaylists = this.allPlaylists.filter((playlist) => {
if (typeof (playlist.playlistName) !== 'string') { return false }
if (this.doSearchPlaylistsWithMatchingVideos) {
if (playlist.videos.some((v) => v.title.toLowerCase().includes(this.lowerCaseQuery))) {
return true
}
}
return playlist.playlistName.toLowerCase().includes(this.lowerCaseQuery)
})
this.showLoadMoreButton = filteredPlaylists.length > this.searchDataLimit
this.activeData = filteredPlaylists.length < this.searchDataLimit ? filteredPlaylists : filteredPlaylists.slice(0, this.searchDataLimit)
}, },
createNewPlaylist: function () { createNewPlaylist: function () {

View File

@ -19,24 +19,39 @@
class="newPlaylistButton" class="newPlaylistButton"
@click="createNewPlaylist" @click="createNewPlaylist"
/> />
<ft-input <div
v-if="fullData.length > 1" v-if="fullData.length > 1"
ref="searchBar" class="searchInputsRow"
:placeholder="$t('User Playlists.Search bar placeholder')" >
:show-clear-text-button="true" <ft-input
:show-action-button="false" ref="searchBar"
@input="(input) => query = input" :placeholder="$t('User Playlists.Search bar placeholder')"
@clear="query = ''" :show-clear-text-button="true"
/> :show-action-button="false"
<ft-select @input="(input) => query = input"
v-if="fullData.length > 1" @clear="query = ''"
class="sortSelect" />
:value="sortBy" </div>
:select-names="sortBySelectNames" <div
:select-values="sortBySelectValues" class="optionsRow"
:placeholder="$t('User Playlists.Sort By.Sort By')" >
@change="sortBy = $event" <ft-toggle-switch
/> v-if="fullData.length > 1"
:label="$t('User Playlists.Playlists with Matching Videos')"
:compact="true"
:default-value="doSearchPlaylistsWithMatchingVideos"
@change="doSearchPlaylistsWithMatchingVideos = !doSearchPlaylistsWithMatchingVideos"
/>
<ft-select
v-if="fullData.length > 1"
class="sortSelect"
:value="sortBy"
:select-names="sortBySelectNames"
:select-values="sortBySelectValues"
:placeholder="$t('User Playlists.Sort By.Sort By')"
@change="sortBy = $event"
/>
</div>
</div> </div>
<ft-flex-box <ft-flex-box
v-if="fullData.length === 0" v-if="fullData.length === 0"
@ -56,6 +71,7 @@
v-else-if="activeData.length > 0 && !isLoading" v-else-if="activeData.length > 0 && !isLoading"
:data="activeData" :data="activeData"
:data-type="'playlist'" :data-type="'playlist'"
:search-query-text="doSearchPlaylistsWithMatchingVideos ? lowerCaseQuery : ''"
:use-channels-hidden-preference="false" :use-channels-hidden-preference="false"
:hide-forbidden-titles="false" :hide-forbidden-titles="false"
/> />

View File

@ -143,7 +143,8 @@ User Playlists:
it listed here it listed here
You have no playlists. Click on the create new playlist button to create a new one.: You have no playlists. Click on the create new playlist button to create a new one. You have no playlists. Click on the create new playlist button to create a new one.: You have no playlists. Click on the create new playlist button to create a new one.
Empty Search Message: There are no videos in this playlist that matches your search Empty Search Message: There are no videos in this playlist that matches your search
Search bar placeholder: Search in Playlist Search bar placeholder: Search for Playlists
Playlists with Matching Videos: Playlists with Matching Videos
This playlist currently has no videos.: This playlist currently has no videos. This playlist currently has no videos.: This playlist currently has no videos.