[web] Refactor search pages

This commit is contained in:
Alain Nussbaumer
2025-05-18 18:35:58 +02:00
parent b5fe530f0d
commit 708370aab9
10 changed files with 319 additions and 312 deletions

View File

@@ -33,6 +33,8 @@ export default {
components: { ListItem, ModalDialogAlbum },
props: {
items: { required: true, type: Object },
load: { default: null, type: Function },
loaded: { default: true, type: Boolean },
mediaKind: { default: '', type: String }
},
emits: ['play-count-changed', 'podcast-deleted'],

View File

@@ -22,7 +22,11 @@ import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
export default {
name: 'ListArtists',
components: { ListItem, ModalDialogArtist },
props: { items: { required: true, type: Object } },
props: {
items: { required: true, type: Object },
load: { default: null, type: Function },
loaded: { default: true, type: Boolean }
},
data() {
return { selectedItem: {}, showDetailsModal: false }
},

View File

@@ -22,7 +22,11 @@ import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
export default {
name: 'ListComposers',
components: { ListItem, ModalDialogComposer },
props: { items: { required: true, type: Object } },
props: {
items: { required: true, type: Object },
load: { default: null, type: Function },
loaded: { default: true, type: Boolean }
},
data() {
return { selectedItem: {}, showDetailsModal: false }
},

View File

@@ -23,7 +23,11 @@ import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
export default {
name: 'ListPlaylists',
components: { ListItem, ModalDialogPlaylist },
props: { items: { required: true, type: Object } },
props: {
items: { required: true, type: Object },
load: { default: null, type: Function },
loaded: { default: true, type: Boolean }
},
data() {
return { selectedItem: {}, showDetailsModal: false }
},

View File

@@ -31,6 +31,8 @@ export default {
expression: { default: '', type: String },
items: { default: null, type: Object },
icon: { default: null, type: String },
load: { default: null, type: Function },
loaded: { default: true, type: Boolean },
showProgress: { default: false, type: Boolean },
uris: { default: '', type: String }
},

View File

@@ -1,89 +1,46 @@
<template>
<section class="section pb-0">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form @submit.prevent="search">
<div class="field">
<div class="control has-icons-left">
<input
v-model="searchStore.query"
class="input is-rounded"
type="text"
:placeholder="$t('page.search.placeholder')"
autocomplete="off"
/>
<mdicon class="icon is-left" name="magnify" size="16" />
</div>
<i18n-t
tag="p"
class="help has-text-centered"
keypath="page.search.help"
scope="global"
>
<template #query>
<code>query:</code>
</template>
<template #help>
<a
href="https://owntone.github.io/owntone-server/smart-playlists/"
target="_blank"
v-text="$t('page.search.expression')"
/>
</template>
</i18n-t>
</div>
</form>
<div class="field is-grouped is-grouped-multiline mt-4">
<div v-for="item in history" :key="item" class="control">
<div class="tags has-addons">
<a class="tag" @click="openSearch(item)" v-text="item" />
<a class="tag is-delete" @click="searchStore.remove(item)" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<tabs-search @search-library="search" @search-spotify="searchSpotify" />
<content-with-heading v-for="[type, items] in results" :key="type">
<template #heading>
<pane-title :content="{ title: $t(`page.search.${type}s`) }" />
<content-with-search
:components="components"
:expanded="expanded"
:get-items="getItems"
:history="history"
:results="results"
@search="search"
@search-library="search"
@search-query="openSearch"
@search-spotify="searchSpotify"
@expand="expand"
>
<template #help>
<i18n-t
tag="p"
class="help has-text-centered"
keypath="page.search.help"
scope="global"
>
<template #query>
<code>query:</code>
</template>
<template #help>
<a
href="https://owntone.github.io/owntone-server/smart-playlists/"
target="_blank"
v-text="$t('page.search.expression')"
/>
</template>
</i18n-t>
</template>
<template #content>
<component :is="components[type]" :items="items" />
</template>
<template v-if="!expanded" #footer>
<control-button
v-if="showAllButton(items)"
:button="{
handler: () => expand(type),
title: $t(
`page.search.show-${type}s`,
{ count: $n(items.total) },
items.total
)
}"
/>
<div v-if="!items.total" class="has-text-centered-mobile">
<i v-text="$t('page.search.no-results')" />
</div>
</template>
</content-with-heading>
</content-with-search>
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlButton from '@/components/ControlButton.vue'
import ContentWithSearch from '@/templates/ContentWithSearch.vue'
import { GroupedList } from '@/lib/GroupedList'
import ListAlbums from '@/components/ListAlbums.vue'
import ListArtists from '@/components/ListArtists.vue'
import ListComposers from '@/components/ListComposers.vue'
import ListPlaylists from '@/components/ListPlaylists.vue'
import ListTracks from '@/components/ListTracks.vue'
import PaneTitle from '@/components/PaneTitle.vue'
import TabsSearch from '@/components/TabsSearch.vue'
import library from '@/api/library'
import { useSearchStore } from '@/stores/search'
@@ -100,31 +57,21 @@ const PAGE_SIZE = 3,
export default {
name: 'PageSearchLibrary',
components: {
ContentWithHeading,
ControlButton,
ListAlbums,
ListArtists,
ListComposers,
ListPlaylists,
ListTracks,
PaneTitle,
TabsSearch
},
components: { ContentWithSearch },
setup() {
return { searchStore: useSearchStore() }
return {
components: {
album: ListAlbums,
artist: ListArtists,
composer: ListComposers,
playlist: ListPlaylists,
track: ListTracks
},
searchStore: useSearchStore()
}
},
data() {
return {
components: {
album: ListAlbums.name,
artist: ListArtists.name,
audiobook: ListAlbums.name,
composer: ListComposers.name,
playlist: ListPlaylists.name,
podcast: ListAlbums.name,
track: ListTracks.name
},
results: new Map(),
limit: {},
types: SEARCH_TYPES
@@ -149,6 +96,9 @@ export default {
this.limit = -1
this.search()
},
getItems(items) {
return items
},
openSearch(query) {
this.searchStore.query = query
this.types = SEARCH_TYPES
@@ -196,12 +146,6 @@ export default {
},
searchSpotify() {
this.$router.push({ name: 'search-spotify' })
},
show(type) {
return this.types.includes(type)
},
showAllButton(items) {
return items.total > items.items.length
}
}
}

View File

@@ -1,76 +1,25 @@
<template>
<section class="section pb-0">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form @submit.prevent="search">
<div class="field">
<div class="control has-icons-left">
<input
v-model="searchStore.query"
class="input is-rounded"
type="text"
:placeholder="$t('page.search.placeholder')"
autocomplete="off"
/>
<mdicon class="icon is-left" name="magnify" size="16" />
</div>
</div>
</form>
<div class="field is-grouped is-grouped-multiline mt-4">
<div v-for="item in history" :key="item" class="control">
<div class="tags has-addons">
<a class="tag" @click="openSearch(item)" v-text="item" />
<a class="tag is-delete" @click="searchStore.remove(item)" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<tabs-search @search-library="searchLibrary" @search-spotify="search" />
<content-with-heading v-for="[type, items] in results" :key="type">
<template #heading>
<pane-title :content="{ title: $t(`page.search.${type}s`) }" />
</template>
<template #content>
<component
:is="components[type]"
:items="items.items"
:load="searchNext"
:loaded="!expanded"
/>
</template>
<template v-if="!expanded" #footer>
<control-button
v-if="showAllButton(items)"
:button="{
handler: () => expand(type),
title: $t(
`page.search.show-${type}s`,
{ count: $n(items.total) },
items.total
)
}"
/>
<div v-if="!items.total" class="has-text-centered-mobile">
<i v-text="$t('page.search.no-results')" />
</div>
</template>
</content-with-heading>
<content-with-search
:components="components"
:expanded="expanded"
:get-items="getItems"
:history="history"
:results="results"
@search="search"
@search-library="searchLibrary"
@search-query="openSearch"
@search-spotify="search"
@expand="expand"
/>
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlButton from '@/components/ControlButton.vue'
import ContentWithSearch from '@/templates/ContentWithSearch.vue'
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
import ListArtistsSpotify from '@/components/ListArtistsSpotify.vue'
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import TabsSearch from '@/components/TabsSearch.vue'
import services from '@/api/services'
import { useSearchStore } from '@/stores/search'
@@ -80,27 +29,20 @@ const PAGE_SIZE = 3,
export default {
name: 'PageSearchSpotify',
components: {
ControlButton,
ContentWithHeading,
ListAlbumsSpotify,
ListArtistsSpotify,
ListPlaylistsSpotify,
ListTracksSpotify,
PaneTitle,
TabsSearch
},
components: { ContentWithSearch },
setup() {
return { searchStore: useSearchStore() }
return {
components: {
album: ListAlbumsSpotify,
artist: ListArtistsSpotify,
playlist: ListPlaylistsSpotify,
track: ListTracksSpotify
},
searchStore: useSearchStore()
}
},
data() {
return {
components: {
album: ListAlbumsSpotify.name,
artist: ListArtistsSpotify.name,
playlist: ListPlaylistsSpotify.name,
track: ListTracksSpotify.name
},
results: new Map(),
parameters: {},
types: SEARCH_TYPES
@@ -128,6 +70,9 @@ export default {
this.parameters.offset = 0
this.search()
},
getItems(items) {
return items.items
},
openSearch(query) {
this.searchStore.query = query
this.types = SEARCH_TYPES
@@ -186,12 +131,6 @@ export default {
this.parameters.offset += next.limit
loaded(next.items.length, PAGE_SIZE_EXPANDED)
})
},
show(type) {
return this.types.includes(type)
},
showAllButton(items) {
return items.total > items.items.length
}
}
}

View File

@@ -0,0 +1,108 @@
<template>
<section class="section pb-0">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<form @submit.prevent="$emit('search')">
<div class="field">
<div class="control has-icons-left">
<input
v-model="searchStore.query"
class="input is-rounded"
type="text"
:placeholder="$t('page.search.placeholder')"
autocomplete="off"
/>
<mdicon class="icon is-left" name="magnify" size="16" />
</div>
<slot name="help" />
</div>
</form>
<div class="field is-grouped is-grouped-multiline mt-4">
<div v-for="item in history" :key="item" class="control">
<div class="tags has-addons">
<a
class="tag"
@click="$emit('search-query', item)"
v-text="item"
/>
<a class="tag is-delete" @click="searchStore.remove(item)" />
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<tabs-search
@search-library="$emit('search-library')"
@search-spotify="$emit('search-spotify')"
/>
<content-with-heading v-for="[type, items] in results" :key="type">
<template #heading>
<pane-title :content="{ title: $t(`page.search.${type}s`) }" />
</template>
<template #content>
<component
:is="components[type]"
:items="getItems(items)"
:load="load"
:loaded="!expanded"
/>
</template>
<template v-if="!expanded" #footer>
<control-button
v-if="showAllButton(items)"
:button="{
handler: () => $emit('expand', type),
title: $t(
`page.search.show-${type}s`,
{ count: $n(items.total) },
items.total
)
}"
/>
<div v-if="!items.total" class="has-text-centered-mobile">
<i v-text="$t('page.search.no-results')" />
</div>
</template>
</content-with-heading>
</template>
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlButton from '@/components/ControlButton.vue'
import PaneTitle from '@/components/PaneTitle.vue'
import TabsSearch from '@/components/TabsSearch.vue'
import { useSearchStore } from '@/stores/search'
export default {
name: 'ContentWithSearch',
components: { ContentWithHeading, ControlButton, PaneTitle, TabsSearch },
props: {
components: { default: null, type: Object },
expanded: { default: false, type: Boolean },
getItems: { default: null, type: Function },
history: { default: null, type: Array },
load: { default: null, type: Function },
results: { default: null, type: Object }
},
emits: [
'expand',
'search',
'search-library',
'search-query',
'search-spotify'
],
setup() {
return {
searchStore: useSearchStore()
}
},
methods: {
showAllButton(items) {
return items.total > items.items.length
}
}
}
</script>