[web] Add the possibility to remove past search queries

This commit is contained in:
Alain Nussbaumer 2024-04-09 15:39:06 +02:00
parent e5e7702fc5
commit 0362896bfb
5 changed files with 212 additions and 257 deletions

View File

@ -10,7 +10,7 @@
'is-active': $route.name === 'search-library'
}"
>
<a @click="search_library">
<a @click="$emit('search-library')">
<mdicon class="icon is-small" name="bookshelf" size="16" />
<span v-text="$t('page.search.tabs.library')" />
</a>
@ -20,7 +20,7 @@
'is-active': $route.name === 'search-spotify'
}"
>
<a @click="search_spotify">
<a @click="$emit('search-spotify')">
<mdicon class="icon is-small" name="spotify" size="16" />
<span v-text="$t('page.search.tabs.spotify')" />
</a>
@ -36,39 +36,12 @@
<script>
export default {
name: 'TabsSearch',
props: { query: { default: '', type: String } },
emits: ['search-library', 'search-spotify'],
computed: {
route_query() {
if (this.query) {
return {
limit: 3,
offset: 0,
query: this.query,
type: 'track,artist,album,composer,playlist,audiobook,podcast'
}
}
return null
},
spotify_enabled() {
return this.$store.state.spotify.webapi_token_valid
}
},
methods: {
search_library() {
this.$router.push({
name: 'search-library',
query: this.route_query
})
},
search_spotify() {
this.$router.push({
name: 'search-spotify',
query: this.route_query
})
}
}
}
</script>

View File

@ -3,7 +3,7 @@
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form @submit.prevent="new_search">
<form @submit.prevent="search">
<div class="field">
<p class="control has-icons-left">
<input
@ -32,43 +32,42 @@
</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 class="field is-grouped is-grouped-multiline mt-4">
<div v-for="query in recent_searches" :key="query" class="control">
<div class="tags has-addons">
<a class="tag" @click="open_search(query)" v-text="query" />
<a class="tag is-delete" @click="remove_search(query)"></a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<tabs-search :query="search_query" />
<template v-for="type in search_types" :key="type">
<content-with-heading v-if="show(type)" class="pt-0">
<tabs-search @search-library="search" @search-spotify="search_spotify" />
<template v-for="[type, items] in results" :key="type">
<content-with-heading class="pt-0">
<template #heading-left>
<p class="title is-4" v-text="$t(`page.search.${type}s`)" />
</template>
<template #content>
<component :is="components[type]" :items="results[type]" />
<component :is="components[type]" :items="items" />
</template>
<template #footer>
<nav v-if="show_all_button(type)" class="level">
<template v-if="!expanded" #footer>
<nav v-if="show_all_button(items)" class="level">
<p class="level-item">
<a
class="button is-light is-small is-rounded"
@click="open_search(type)"
@click="expand(type)"
v-text="
$t(`page.search.show-${type}s`, results[type].total, {
count: $filters.number(results[type].total)
$t(`page.search.show-${type}s`, items.total, {
count: $filters.number(items.total)
})
"
/>
</p>
</nav>
<p v-if="!results[type].total" class="has-text-centered-mobile">
<p v-if="!items.total" class="has-text-centered-mobile">
<i v-text="$t('page.search.no-results')" />
</p>
</template>
@ -88,6 +87,17 @@ import ListTracks from '@/components/ListTracks.vue'
import TabsSearch from '@/components/TabsSearch.vue'
import webapi from '@/webapi'
const PAGE_SIZE = 3,
SEARCH_TYPES = [
'track',
'artist',
'album',
'composer',
'playlist',
'audiobook',
'podcast'
]
export default {
name: 'PageSearchLibrary',
components: {
@ -111,139 +121,98 @@ export default {
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()
},
results: new Map(),
search_limit: {},
search_query: '',
search_types: [
'track',
'artist',
'album',
'composer',
'playlist',
'audiobook',
'podcast'
],
tracks: new GroupedList()
search_types: SEARCH_TYPES
}
},
computed: {
expanded() {
return this.search_types.length === 1
},
recent_searches() {
return this.$store.state.recent_searches
}
},
watch: {
$route(to, from) {
this.search(to)
search_query() {
this.$store.commit(types.SEARCH_QUERY, this.search_query)
}
},
mounted() {
this.$store.commit(types.SEARCH_SOURCE, this.$route.name)
this.search_query = this.$store.state.search_query
this.search_limit = PAGE_SIZE
this.search()
},
methods: {
new_search() {
if (!this.search_query) {
return
}
this.$router.push({
query: {
limit: 3,
offset: 0,
query: this.search_query,
type: this.search_types.join()
}
})
this.$refs.search_field.blur()
expand(type) {
this.search_query = this.$store.state.search_query
this.search_types = [type]
this.search_limit = -1
this.search()
},
open_recent_search(query) {
open_search(query) {
this.search_query = query
this.new_search()
this.search_types = SEARCH_TYPES
this.search_limit = PAGE_SIZE
this.search()
},
open_search(type) {
this.$router.push({
query: { query: this.$route.query.query, type }
remove_search(query) {
this.$store.dispatch('remove_recent_search', query)
},
reset() {
this.results.clear()
this.search_types.forEach((type) => {
this.results.set(type, new GroupedList())
})
},
search() {
this.search_query = this.$route.query.query?.trim()
search(event) {
if (event) {
this.search_types = SEARCH_TYPES
this.search_limit = PAGE_SIZE
}
if (!this.search_query || !this.search_query.replace(/^query:/u, '')) {
this.$refs.search_field.focus()
return
}
this.$route.query.query = this.search_query
this.searchMusic(this.$route.query)
this.searchType(this.$route.query, 'audiobook')
this.searchType(this.$route.query, 'podcast')
this.reset()
this.search_types.forEach((type) => {
this.search_items(type)
})
this.$store.dispatch('add_recent_search', this.search_query)
},
searchMusic(query) {
if (
!query.type.includes('track') &&
!query.type.includes('artist') &&
!query.type.includes('album') &&
!query.type.includes('playlist') &&
!query.type.includes('composer')
) {
return
}
const parameters = {
type: query.type
}
if (query.query.startsWith('query:')) {
parameters.expression = `(${query.query.replace(/^query:/u, '').trim()}) and media_kind is music`
search_items(type) {
const music = type !== 'audiobook' && type !== 'podcast',
kind = music ? 'music' : type,
parameters = {
type: music ? type : 'album',
limit: this.search_limit
}
if (this.search_query.startsWith('query:')) {
parameters.expression = `(${this.search_query.replace(/^query:/u, '').trim()}) and media_kind is ${kind}`
} else if (music) {
parameters.query = this.search_query
parameters.media_kind = kind
} else {
parameters.query = query.query
parameters.media_kind = 'music'
}
if (query.limit) {
parameters.limit = query.limit
parameters.offset = query.offset
parameters.expression = `(album includes "${this.search_query}" or artist includes "${this.search_query}") and media_kind is ${kind}`
}
webapi.search(parameters).then(({ data }) => {
this.results.track = new GroupedList(data.tracks)
this.results.artist = new GroupedList(data.artists)
this.results.album = new GroupedList(data.albums)
this.results.composer = new GroupedList(data.composers)
this.results.playlist = new GroupedList(data.playlists)
this.results.set(type, new GroupedList(data[`${parameters.type}s`]))
})
},
searchType(query, type) {
if (!query.type.includes(type)) {
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 ${type}`
if (query.limit) {
parameters.limit = query.limit
parameters.offset = query.offset
}
webapi.search(parameters).then(({ data }) => {
this.results[type] = new GroupedList(data.albums)
})
search_spotify() {
this.$router.push({ name: 'search-spotify' })
},
show(type) {
return this.$route.query.type?.includes(type) ?? false
return this.search_types.includes(type)
},
show_all_button(type) {
const items = this.results[type]
show_all_button(items) {
return items.total > items.items.length
}
}

View File

@ -3,7 +3,7 @@
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form @submit.prevent="new_search">
<form @submit.prevent="search">
<div class="field">
<p class="control has-icons-left">
<input
@ -18,28 +18,27 @@
</p>
</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 class="field is-grouped is-grouped-multiline mt-4">
<div v-for="query in recent_searches" :key="query" class="control">
<div class="tags has-addons">
<a class="tag" @click="open_search(query)" v-text="query" />
<a class="tag is-delete" @click="remove_search(query)"></a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<tabs-search :query="search_query" />
<template v-for="type in search_types" :key="type">
<content-with-heading v-if="show(type)" class="pt-0">
<tabs-search @search-library="search_library" @search-spotify="search" />
<template v-for="[type, items] in results" :key="type">
<content-with-heading class="pt-0">
<template #heading-left>
<p class="title is-4" v-text="$t(`page.spotify.search.${type}s`)" />
</template>
<template #content>
<component :is="components[type]" :items="results[type].items" />
<VueEternalLoading v-if="$route.query.type === type" :load="search_next">
<component :is="components[type]" :items="items.items" />
<VueEternalLoading v-if="expanded" :load="search_next">
<template #loading>
<div class="columns is-centered">
<div class="column has-text-centered">
@ -50,21 +49,21 @@
<template #no-more>&nbsp;</template>
</VueEternalLoading>
</template>
<template #footer>
<nav v-if="show_all_button(type)" class="level">
<template v-if="!expanded" #footer>
<nav v-if="show_all_button(items)" class="level">
<p class="level-item">
<a
class="button is-light is-small is-rounded"
@click="open_search(type)"
@click="expand(type)"
v-text="
$t(`page.spotify.search.show-${type}s`, results[type].total, {
count: $filters.number(results[type].total)
$t(`page.spotify.search.show-${type}s`, items.total, {
count: $filters.number(items.total)
})
"
/>
</p>
</nav>
<p v-if="!results[type].total" class="has-text-centered-mobile">
<p v-if="!items.total" class="has-text-centered-mobile">
<i v-text="$t(`page.spotify.search.no-results`)" />
</p>
</template>
@ -84,7 +83,9 @@ import TabsSearch from '@/components/TabsSearch.vue'
import { VueEternalLoading } from '@ts-pro/vue-eternal-loading'
import webapi from '@/webapi'
const PAGE_SIZE = 50
const PAGE_SIZE = 3,
PAGE_SIZE_EXPANDED = 50,
SEARCH_TYPES = ['track', 'artist', 'album', 'playlist']
export default {
name: 'PageSearchSpotify',
@ -106,114 +107,115 @@ export default {
playlist: ListPlaylistsSpotify.name,
track: ListTracksSpotify.name
},
results: {
album: { items: [], total: 0 },
artist: { items: [], total: 0 },
playlist: { items: [], total: 0 },
track: { items: [], total: 0 }
},
search_param: {},
results: new Map(),
search_parameters: {},
search_query: '',
search_types: ['track', 'artist', 'album', 'playlist']
search_types: SEARCH_TYPES
}
},
computed: {
expanded() {
return this.search_types.length === 1
},
recent_searches() {
return this.$store.state.recent_searches.filter(
(search) => !search.startsWith('query:')
(query) => !query.startsWith('query:')
)
}
},
watch: {
$route(to, from) {
this.search()
search_query() {
this.$store.commit(types.SEARCH_QUERY, this.search_query)
}
},
mounted() {
this.$store.commit(types.SEARCH_SOURCE, this.$route.name)
this.search_query = this.$store.state.search_query
this.search_parameters.limit = PAGE_SIZE
this.search()
},
methods: {
new_search() {
if (!this.search_query) {
return
}
this.$router.push({
query: {
limit: 3,
offset: 0,
query: this.search_query,
type: this.search_types.join()
}
})
this.$refs.search_field.blur()
expand(type) {
this.search_query = this.$store.state.search_query
this.search_types = [type]
this.search_parameters.limit = PAGE_SIZE_EXPANDED
this.search_parameters.offset = 0
this.search()
},
open_recent_search(query) {
open_search(query) {
this.search_query = query
this.new_search()
this.search_types = SEARCH_TYPES
this.search_parameters.limit = PAGE_SIZE
this.search_parameters.offset = 0
this.search()
},
open_search(type) {
this.$router.push({
query: { query: this.$route.query.query, type }
})
remove_search(query) {
this.$store.dispatch('remove_recent_search', query)
},
reset() {
Object.entries(this.results).forEach(
(key) => (this.results[key] = { items: [], total: 0 })
)
this.results.clear()
this.search_types.forEach((type) => {
this.results.set(type, { items: [], total: 0 })
})
},
search() {
this.reset()
this.search_query = this.$route.query.query?.trim()
if (!this.search_query || this.search_query.startsWith('query:')) {
this.search_query = ''
search(event) {
if (event) {
this.search_types = SEARCH_TYPES
this.search_parameters.limit = PAGE_SIZE
}
if (!this.search_query) {
this.$refs.search_field.focus()
return
}
this.$route.query.query = this.search_query
this.search_all()
this.reset()
this.search_items().then((data) => {
this.search_types.forEach((type) => {
this.results.set(type, data[`${type}s`])
})
})
this.$store.dispatch('add_recent_search', this.search_query)
},
search_all() {
this.search_param.limit = this.$route.query.limit ?? PAGE_SIZE
this.search_param.offset = this.$route.query.offset ?? 0
const types = this.$route.query.type
.split(',')
.filter((type) => this.search_types.includes(type))
this.search_spotify(types).then((data) => {
this.results.track = data.tracks ?? { items: [], total: 0 }
this.results.artist = data.artists ?? { items: [], total: 0 }
this.results.album = data.albums ?? { items: [], total: 0 }
this.results.playlist = data.playlists ?? { items: [], total: 0 }
search_items() {
return webapi.spotify().then(({ data }) => {
this.search_parameters.market = data.webapi_country
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token)
return spotifyApi.search(
this.search_query,
this.search_types,
this.search_parameters
)
})
},
search_library() {
this.$router.push({
name: 'search-library'
})
},
search_next({ loaded }) {
const items = this.results[this.$route.query.type]
this.search_spotify([this.$route.query.type]).then((data) => {
const [type] = this.search_types,
items = this.results.get(type)
this.search_parameters.limit = PAGE_SIZE_EXPANDED
this.search_items().then((data) => {
const [next] = Object.values(data)
items.items.push(...next.items)
items.total = next.total
this.search_param.offset += next.limit
loaded(next.items.length, PAGE_SIZE)
})
},
search_spotify(types) {
return webapi.spotify().then(({ data }) => {
this.search_param.market = data.webapi_country
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token)
return spotifyApi.search(this.$route.query.query, types, this.search_param)
if (!this.search_parameters.offset) {
this.search_parameters.offset = 0
}
this.search_parameters.offset += next.limit
loaded(next.items.length, PAGE_SIZE_EXPANDED)
})
},
show(type) {
return this.$route.query.type?.includes(type) ?? false
return this.search_types.includes(type)
},
show_all_button(type) {
const items = this.results[type]
show_all_button(items) {
return items.total > items.items.length
}
}

View File

@ -54,6 +54,7 @@ export default createStore({
},
recent_searches: [],
rss_count: {},
search_query: '',
search_source: 'search-library',
settings: {
categories: []
@ -130,6 +131,9 @@ export default createStore({
[types.UPDATE_PAIRING](state, pairing) {
state.pairing = pairing
},
[types.SEARCH_QUERY](state, query) {
state.search_query = query
},
[types.SEARCH_SOURCE](state, searchSource) {
state.search_source = searchSource
},
@ -200,8 +204,8 @@ export default createStore({
}
},
add_recent_search({ commit, state }, query) {
const index = state.recent_searches.findIndex((elem) => elem === query)
if (index >= 0) {
const index = state.recent_searches.indexOf(query)
if (index !== -1) {
state.recent_searches.splice(index, 1)
}
state.recent_searches.splice(0, 0, query)
@ -209,6 +213,12 @@ export default createStore({
state.recent_searches.pop()
}
},
remove_recent_search({ commit, state }, query) {
const index = state.recent_searches.indexOf(query)
if (index !== -1) {
state.recent_searches.splice(index, 1)
}
},
delete_notification({ commit, state }, notification) {
const index = state.notifications.list.indexOf(notification)
if (index !== -1) {

View File

@ -1,25 +1,26 @@
export const UPDATE_CONFIG = 'UPDATE_CONFIG'
export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'
export const UPDATE_LIBRARY_STATS = 'UPDATE_LIBRARY_STATS'
export const UPDATE_LIBRARY_RSS_COUNT = 'UPDATE_LIBRARY_RSS_COUNT'
export const UPDATE_OUTPUTS = 'UPDATE_OUTPUTS'
export const UPDATE_PLAYER_STATUS = 'UPDATE_PLAYER_STATUS'
export const UPDATE_QUEUE = 'UPDATE_QUEUE'
export const UPDATE_LYRICS = 'UPDATE_LYRICS'
export const UPDATE_LASTFM = 'UPDATE_LASTFM'
export const UPDATE_SPOTIFY = 'UPDATE_SPOTIFY'
export const UPDATE_PAIRING = 'UPDATE_PAIRING'
export const SEARCH_SOURCE = 'SEARCH_SOURCE'
export const COMPOSER_TRACKS_SORT = 'COMPOSER_TRACKS_SORT'
export const GENRE_TRACKS_SORT = 'GENRE_TRACKS_SORT'
export const HIDE_SINGLES = 'HIDE_SINGLES'
export const HIDE_SPOTIFY = 'HIDE_SPOTIFY'
export const ARTISTS_SORT = 'ARTISTS_SORT'
export const ARTIST_ALBUMS_SORT = 'ARTIST_ALBUMS_SORT'
export const ARTIST_TRACKS_SORT = 'ARTIST_TRACKS_SORT'
export const ALBUMS_SORT = 'ALBUMS_SORT'
export const SHOW_ONLY_NEXT_ITEMS = 'SHOW_ONLY_NEXT_ITEMS'
export const SHOW_BURGER_MENU = 'SHOW_BURGER_MENU'
export const SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU'
export const SHOW_UPDATE_DIALOG = 'SHOW_UPDATE_DIALOG'
export const UPDATE_DIALOG_SCAN_KIND = 'UPDATE_DIALOG_SCAN_KIND'
export const ALBUMS_SORT = 'ALBUMS_SORT',
ARTISTS_SORT = 'ARTISTS_SORT',
ARTIST_ALBUMS_SORT = 'ARTIST_ALBUMS_SORT',
ARTIST_TRACKS_SORT = 'ARTIST_TRACKS_SORT',
COMPOSER_TRACKS_SORT = 'COMPOSER_TRACKS_SORT',
GENRE_TRACKS_SORT = 'GENRE_TRACKS_SORT',
HIDE_SINGLES = 'HIDE_SINGLES',
HIDE_SPOTIFY = 'HIDE_SPOTIFY',
SEARCH_QUERY = 'SEARCH_QUERY',
SEARCH_SOURCE = 'SEARCH_SOURCE',
SHOW_BURGER_MENU = 'SHOW_BURGER_MENU',
SHOW_ONLY_NEXT_ITEMS = 'SHOW_ONLY_NEXT_ITEMS',
SHOW_PLAYER_MENU = 'SHOW_PLAYER_MENU',
SHOW_UPDATE_DIALOG = 'SHOW_UPDATE_DIALOG',
UPDATE_CONFIG = 'UPDATE_CONFIG',
UPDATE_DIALOG_SCAN_KIND = 'UPDATE_DIALOG_SCAN_KIND',
UPDATE_LASTFM = 'UPDATE_LASTFM',
UPDATE_LIBRARY_RSS_COUNT = 'UPDATE_LIBRARY_RSS_COUNT',
UPDATE_LIBRARY_STATS = 'UPDATE_LIBRARY_STATS',
UPDATE_LYRICS = 'UPDATE_LYRICS',
UPDATE_OUTPUTS = 'UPDATE_OUTPUTS',
UPDATE_PAIRING = 'UPDATE_PAIRING',
UPDATE_PLAYER_STATUS = 'UPDATE_PLAYER_STATUS',
UPDATE_QUEUE = 'UPDATE_QUEUE',
UPDATE_SETTINGS = 'UPDATE_SETTINGS',
UPDATE_SPOTIFY = 'UPDATE_SPOTIFY'