mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 16:25:03 -05:00
[web] Refactor library search page
This commit is contained in:
parent
17c6454afa
commit
36842dfc04
@ -1,262 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<section class="section pb-0">
|
||||||
<!-- Search field + recent searches -->
|
<div class="container">
|
||||||
<section class="section pb-0">
|
<div class="columns is-centered">
|
||||||
<div class="container">
|
<div class="column is-four-fifths">
|
||||||
<div class="columns is-centered">
|
<form @submit.prevent="new_search">
|
||||||
<div class="column is-four-fifths">
|
<div class="field">
|
||||||
<form @submit.prevent="new_search">
|
<p class="control has-icons-left">
|
||||||
<div class="field">
|
<input
|
||||||
<p class="control has-icons-left">
|
ref="search_field"
|
||||||
<input
|
v-model="search_query"
|
||||||
ref="search_field"
|
class="input is-rounded is-shadowless"
|
||||||
v-model="search_query"
|
type="text"
|
||||||
class="input is-rounded is-shadowless"
|
:placeholder="$t('page.search.placeholder')"
|
||||||
type="text"
|
autocomplete="off"
|
||||||
:placeholder="$t('page.search.placeholder')"
|
/>
|
||||||
autocomplete="off"
|
<mdicon class="icon is-left" name="magnify" size="16" />
|
||||||
/>
|
</p>
|
||||||
<mdicon class="icon is-left" name="magnify" size="16" />
|
<i18n-t
|
||||||
</p>
|
tag="p"
|
||||||
<i18n-t
|
class="help has-text-centered"
|
||||||
tag="p"
|
keypath="page.search.help"
|
||||||
class="help has-text-centered"
|
scope="global"
|
||||||
keypath="page.search.help"
|
>
|
||||||
scope="global"
|
<template #query><code>query:</code></template>
|
||||||
>
|
<template #help
|
||||||
<template #query><code>query:</code></template>
|
><a
|
||||||
<template #help
|
href="https://owntone.github.io/owntone-server/smart-playlists/"
|
||||||
><a
|
target="_blank"
|
||||||
href="https://owntone.github.io/owntone-server/smart-playlists/"
|
v-text="$t('page.search.expression')"
|
||||||
target="_blank"
|
/></template>
|
||||||
v-text="$t('page.search.expression')"
|
</i18n-t>
|
||||||
/></template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div class="tags mt-4">
|
|
||||||
<a
|
|
||||||
v-for="recent_search in recent_searches"
|
|
||||||
:key="recent_search"
|
|
||||||
class="tag"
|
|
||||||
@click="open_recent_search(recent_search)"
|
|
||||||
v-text="recent_search"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="tags mt-4">
|
||||||
|
<a
|
||||||
|
v-for="recent_search in recent_searches"
|
||||||
|
:key="recent_search"
|
||||||
|
class="tag"
|
||||||
|
@click="open_recent_search(recent_search)"
|
||||||
|
v-text="recent_search"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
<tabs-search :query="search_query" />
|
</section>
|
||||||
<!-- Tracks -->
|
<tabs-search :query="search_query" />
|
||||||
<content-with-heading v-if="show('track') && tracks.total" class="pt-0">
|
<template v-for="type in search_types" :key="type">
|
||||||
|
<content-with-heading v-if="show(type)" class="pt-0">
|
||||||
<template #heading-left>
|
<template #heading-left>
|
||||||
<p class="title is-4" v-text="$t('page.search.tracks')" />
|
<p class="title is-4" v-text="$t(`page.search.${type}s`)" />
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<list-tracks :items="tracks" />
|
<component :is="components[type]" :items="results[type]" />
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<nav v-if="show_all_button(tracks)" class="level">
|
<nav v-if="show_all_button(type)" class="level">
|
||||||
<p class="level-item">
|
<p class="level-item">
|
||||||
<a
|
<a
|
||||||
class="button is-light is-small is-rounded"
|
class="button is-light is-small is-rounded"
|
||||||
@click="open_search('track')"
|
@click="open_search(type)"
|
||||||
v-text="
|
v-text="
|
||||||
$t('page.search.show-tracks', tracks.total, {
|
$t(`page.search.show-${type}s`, results[type].total, {
|
||||||
count: $filters.number(tracks.total)
|
count: $filters.number(results[type].total)
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
|
<p v-if="!results[type].total" class="has-text-centered-mobile">
|
||||||
|
<i v-text="$t('page.search.no-tracks')" />
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</content-with-heading>
|
</content-with-heading>
|
||||||
<content-text v-if="show('track') && !tracks.total" class="pt-0">
|
</template>
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-tracks')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
<!-- Artists -->
|
|
||||||
<content-with-heading v-if="show('artist') && artists.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.artists')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-artists :items="artists" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(artists)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('artist')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-artists', artists.total, {
|
|
||||||
count: $filters.number(artists.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('artist') && !artists.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-artists')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
<!-- Albums -->
|
|
||||||
<content-with-heading v-if="show('album') && albums.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.albums')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-albums :items="albums" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(albums)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('album')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-albums', albums.total, {
|
|
||||||
count: $filters.number(albums.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('album') && !albums.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-albums')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
<!-- Composers -->
|
|
||||||
<content-with-heading v-if="show('composer') && composers.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.composers')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-composers :items="composers" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(composers)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('composer')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-composers', composers.total, {
|
|
||||||
count: $filters.number(composers.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('composer') && !composers.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-composers')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
<!-- Playlists -->
|
|
||||||
<content-with-heading v-if="show('playlist') && playlists.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.playlists')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-playlists :items="playlists" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(playlists)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('playlist')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-playlists', playlists.total, {
|
|
||||||
count: $filters.number(playlists.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('playlist') && !playlists.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-playlists')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
<!-- Podcasts -->
|
|
||||||
<content-with-heading v-if="show('podcast') && podcasts.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.podcasts')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-albums :items="podcasts" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(podcasts)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('podcast')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-podcasts', podcasts.total, {
|
|
||||||
count: $filters.number(podcasts.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('podcast') && !podcasts.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-podcasts')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
|
|
||||||
<!-- Audiobooks -->
|
|
||||||
<content-with-heading v-if="show('audiobook') && audiobooks.total">
|
|
||||||
<template #heading-left>
|
|
||||||
<p class="title is-4" v-text="$t('page.search.audiobooks')" />
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<list-albums :items="audiobooks" />
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<nav v-if="show_all_button(audiobooks)" class="level">
|
|
||||||
<p class="level-item">
|
|
||||||
<a
|
|
||||||
class="button is-light is-small is-rounded"
|
|
||||||
@click="open_search('audiobook')"
|
|
||||||
v-text="
|
|
||||||
$t('page.search.show-audiobooks', audiobooks.total, {
|
|
||||||
count: $filters.number(audiobooks.total)
|
|
||||||
})
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
</content-with-heading>
|
|
||||||
<content-text v-if="show('audiobook') && !audiobooks.total">
|
|
||||||
<template #content>
|
|
||||||
<p><i v-text="$t('page.search.no-audiobooks')" /></p>
|
|
||||||
</template>
|
|
||||||
</content-text>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ContentText from '@/templates/ContentText.vue'
|
|
||||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||||
import { GroupedList } from '@/lib/GroupedList'
|
import { GroupedList } from '@/lib/GroupedList'
|
||||||
import ListAlbums from '@/components/ListAlbums.vue'
|
import ListAlbums from '@/components/ListAlbums.vue'
|
||||||
@ -270,7 +90,6 @@ import webapi from '@/webapi'
|
|||||||
export default {
|
export default {
|
||||||
name: 'PageSearchLibrary',
|
name: 'PageSearchLibrary',
|
||||||
components: {
|
components: {
|
||||||
ContentText,
|
|
||||||
ContentWithHeading,
|
ContentWithHeading,
|
||||||
ListAlbums,
|
ListAlbums,
|
||||||
ListArtists,
|
ListArtists,
|
||||||
@ -282,13 +101,34 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
albums: new GroupedList(),
|
components: {
|
||||||
artists: new GroupedList(),
|
album: ListAlbums.name,
|
||||||
audiobooks: new GroupedList(),
|
artist: ListArtists.name,
|
||||||
composers: new GroupedList(),
|
audiobook: ListAlbums.name,
|
||||||
playlists: new GroupedList(),
|
composer: ListComposers.name,
|
||||||
podcasts: new GroupedList(),
|
playlist: ListPlaylists.name,
|
||||||
|
podcast: ListAlbums.name,
|
||||||
|
track: ListTracks.name
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
album: new GroupedList(),
|
||||||
|
artist: new GroupedList(),
|
||||||
|
audiobook: new GroupedList(),
|
||||||
|
composer: new GroupedList(),
|
||||||
|
playlist: new GroupedList(),
|
||||||
|
podcast: new GroupedList(),
|
||||||
|
track: new GroupedList()
|
||||||
|
},
|
||||||
search_query: '',
|
search_query: '',
|
||||||
|
search_types: [
|
||||||
|
'track',
|
||||||
|
'artist',
|
||||||
|
'album',
|
||||||
|
'composer',
|
||||||
|
'playlist',
|
||||||
|
'audiobook',
|
||||||
|
'podcast'
|
||||||
|
],
|
||||||
tracks: new GroupedList()
|
tracks: new GroupedList()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -310,6 +150,31 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
new_search() {
|
||||||
|
if (!this.search_query) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$router.push({
|
||||||
|
name: 'search-library',
|
||||||
|
query: {
|
||||||
|
limit: 3,
|
||||||
|
offset: 0,
|
||||||
|
query: this.search_query,
|
||||||
|
type: this.search_types.join()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$refs.search_field.blur()
|
||||||
|
},
|
||||||
|
open_recent_search(query) {
|
||||||
|
this.search_query = query
|
||||||
|
this.new_search()
|
||||||
|
},
|
||||||
|
open_search(type) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'search-library',
|
||||||
|
query: { query: this.$route.query.query, type }
|
||||||
|
})
|
||||||
|
},
|
||||||
search(route) {
|
search(route) {
|
||||||
this.search_query = route.query.query?.trim()
|
this.search_query = route.query.query?.trim()
|
||||||
if (!this.search_query || !this.search_query.replace(/^query:/u, '')) {
|
if (!this.search_query || !this.search_query.replace(/^query:/u, '')) {
|
||||||
@ -318,11 +183,10 @@ export default {
|
|||||||
}
|
}
|
||||||
route.query.query = this.search_query
|
route.query.query = this.search_query
|
||||||
this.searchMusic(route.query)
|
this.searchMusic(route.query)
|
||||||
this.searchAudiobooks(route.query)
|
this.searchType(route.query, 'audiobook')
|
||||||
this.searchPodcasts(route.query)
|
this.searchType(route.query, 'podcast')
|
||||||
this.$store.dispatch('add_recent_search', this.search_query)
|
this.$store.dispatch('add_recent_search', this.search_query)
|
||||||
},
|
},
|
||||||
|
|
||||||
searchMusic(query) {
|
searchMusic(query) {
|
||||||
if (
|
if (
|
||||||
!query.type.includes('track') &&
|
!query.type.includes('track') &&
|
||||||
@ -333,30 +197,29 @@ export default {
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const searchParams = {
|
const parameters = {
|
||||||
type: query.type
|
type: query.type
|
||||||
}
|
}
|
||||||
if (query.query.startsWith('query:')) {
|
if (query.query.startsWith('query:')) {
|
||||||
searchParams.expression = `(${query.query.replace(/^query:/u, '').trim()}) and media_kind is music`
|
parameters.expression = `(${query.query.replace(/^query:/u, '').trim()}) and media_kind is music`
|
||||||
} else {
|
} else {
|
||||||
searchParams.query = query.query
|
parameters.query = query.query
|
||||||
searchParams.media_kind = 'music'
|
parameters.media_kind = 'music'
|
||||||
}
|
}
|
||||||
if (query.limit) {
|
if (query.limit) {
|
||||||
searchParams.limit = query.limit
|
parameters.limit = query.limit
|
||||||
searchParams.offset = query.offset
|
parameters.offset = query.offset
|
||||||
}
|
}
|
||||||
webapi.search(searchParams).then(({ data }) => {
|
webapi.search(parameters).then(({ data }) => {
|
||||||
this.tracks = new GroupedList(data.tracks)
|
this.results.track = new GroupedList(data.tracks)
|
||||||
this.artists = new GroupedList(data.artists)
|
this.results.artist = new GroupedList(data.artists)
|
||||||
this.albums = new GroupedList(data.albums)
|
this.results.album = new GroupedList(data.albums)
|
||||||
this.composers = new GroupedList(data.composers)
|
this.results.composer = new GroupedList(data.composers)
|
||||||
this.playlists = new GroupedList(data.playlists)
|
this.results.playlist = new GroupedList(data.playlists)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
searchType(query, type) {
|
||||||
searchAudiobooks(query) {
|
if (!query.type.includes(type)) {
|
||||||
if (!query.type.includes('audiobook')) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const parameters = {
|
const parameters = {
|
||||||
@ -367,76 +230,21 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
parameters.expression = `album includes "${query.query}" or artist includes "${query.query}"`
|
parameters.expression = `album includes "${query.query}" or artist includes "${query.query}"`
|
||||||
}
|
}
|
||||||
parameters.expression = `(${parameters.expression}) and media_kind is audiobook`
|
parameters.expression = `(${parameters.expression}) and media_kind is ${type}`
|
||||||
if (query.limit) {
|
if (query.limit) {
|
||||||
parameters.limit = query.limit
|
parameters.limit = query.limit
|
||||||
parameters.offset = query.offset
|
parameters.offset = query.offset
|
||||||
}
|
}
|
||||||
webapi.search(parameters).then(({ data }) => {
|
webapi.search(parameters).then(({ data }) => {
|
||||||
this.audiobooks = new GroupedList(data.albums)
|
this.results[type] = new GroupedList(data.albums)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
searchPodcasts(query) {
|
|
||||||
if (!query.type.includes('podcast')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const parameters = {
|
|
||||||
type: 'album'
|
|
||||||
}
|
|
||||||
if (query.query.startsWith('query:')) {
|
|
||||||
parameters.expression = query.query.replace(/^query:/u, '').trim()
|
|
||||||
} else {
|
|
||||||
parameters.expression = `album includes "${query.query}" or artist includes "${query.query}"`
|
|
||||||
}
|
|
||||||
parameters.expression = `(${parameters.expression}) and media_kind is podcast`
|
|
||||||
if (query.limit) {
|
|
||||||
parameters.limit = query.limit
|
|
||||||
parameters.offset = query.offset
|
|
||||||
}
|
|
||||||
webapi.search(parameters).then(({ data }) => {
|
|
||||||
this.podcasts = new GroupedList(data.albums)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
new_search() {
|
|
||||||
if (!this.search_query) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.push({
|
|
||||||
name: 'search-library',
|
|
||||||
query: {
|
|
||||||
limit: 3,
|
|
||||||
offset: 0,
|
|
||||||
query: this.search_query,
|
|
||||||
type: 'track,artist,album,playlist,audiobook,podcast,composer'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.$refs.search_field.blur()
|
|
||||||
},
|
|
||||||
|
|
||||||
show(type) {
|
show(type) {
|
||||||
return this.$route.query.type?.includes(type) ?? false
|
return this.$route.query.type?.includes(type) ?? false
|
||||||
},
|
},
|
||||||
|
show_all_button(type) {
|
||||||
show_all_button(items) {
|
const items = this.results[type]
|
||||||
return items.total > items.items.length
|
return items.total > items.items.length
|
||||||
},
|
|
||||||
|
|
||||||
open_search(type) {
|
|
||||||
this.$router.push({
|
|
||||||
name: 'search-library',
|
|
||||||
query: {
|
|
||||||
query: this.$route.query.query,
|
|
||||||
type: type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
open_recent_search(query) {
|
|
||||||
this.search_query = query
|
|
||||||
this.new_search()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user