fix(style) [WIP] Library search pages

This commit is contained in:
ArneBo 2025-01-05 18:30:55 +01:00 committed by upsiflu
parent 17bed3e9b4
commit 42805171fe
4 changed files with 281 additions and 326 deletions

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import type { OrderingProps } from '~/composables/navigation/useOrdering'
import type { Album, BackendResponse } from '~/types'
import type { Artist, BackendResponse } from '~/types'
import type { RouteRecordName } from 'vue-router'
import type { OrderingField } from '~/store/ui'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, ref, watch, onMounted } from 'vue'
import { useRouteQuery } from '@vueuse/router'
import { useI18n } from 'vue-i18n'
import { syncRef } from '@vueuse/core'
@ -12,12 +12,19 @@ import { sortedUniq } from 'lodash-es'
import { useStore } from '~/store'
import axios from 'axios'
import $ from 'jquery'
import TagsSelector from '~/components/library/TagsSelector.vue'
import AlbumCard from '~/components/album/Card.vue'
import Pagination from '~/components/vui/Pagination.vue'
import Pagination from '~/components/ui/Pagination.vue'
import Link from '~/components/ui/Link.vue'
import Card from '~/components/ui/Card.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
import Toggle from '~/components/ui/Toggle.vue'
import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Pills from '~/components/ui/Pills.vue'
import Loader from '~/components/ui/Loader.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useOrdering from '~/composables/navigation/useOrdering'
@ -41,6 +48,11 @@ const page = usePage()
const tags = useRouteQuery<string[]>('tag', [])
const tagList = computed(() => ({
current: tags.value,
others: []
}))
const q = useRouteQuery('query', '')
const query = ref(q.value)
syncRef(q, query, { direction: 'ltr' })
@ -107,7 +119,7 @@ onOrderingUpdate(() => {
fetchData()
})
onMounted(() => $('.ui.dropdown').dropdown())
// onMounted(() => $('.ui.dropdown').dropdown())
const { t } = useI18n()
const labels = computed(() => ({
@ -115,139 +127,109 @@ const labels = computed(() => ({
title: t('components.library.Albums.title')
}))
const paginateOptions = computed(() => sortedUniq([12, 25, 50, paginateBy.value].sort((a, b) => a - b)))
const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value].sort((a, b) => a - b)))
</script>
<template>
<main v-title="labels.title">
<section class="ui vertical stripe segment">
/front/src/components/library/Albums.vue
<h2 class="ui header">
{{ t('components.library.Albums.header.browse') }}
</h2>
<form
<Layout stack main v-title="labels.title">
<h2>{{ t('components.library.Albums.header.browse') }}</h2>
<Layout form flex
:class="['ui', {'loading': isLoading}, 'form']"
@submit.prevent="search"
>
<div class="fields">
<div class="field">
<label for="albums-search">
{{ t('components.library.Albums.label.search') }}
</label>
<div class="ui action input">
<input
id="albums-search"
v-model="query"
type="text"
name="search"
:placeholder="labels.searchPlaceholder"
>
<Button
type="submit"
:aria-label="t('components.library.Albums.button.search')"
>
<i class="search icon" />
</Button>
</div>
</div>
<div class="field">
<label for="tags-search">{{ t('components.library.Albums.label.tags') }}</label>
<tags-selector v-model="tags" />
</div>
<div class="field">
<label for="album-ordering">{{ t('components.library.Albums.ordering.label') }}</label>
<select
id="album-ordering"
v-model="ordering"
class="ui dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
</div>
<div class="field">
<label for="album-ordering-direction">{{ t('components.library.Albums.ordering.direction.label') }}</label>
<select
id="album-ordering-direction"
v-model="orderingDirection"
class="ui dropdown"
>
<option value="+">
{{ t('components.library.Albums.ordering.direction.ascending') }}
</option>
<option value="-">
{{ t('components.library.Albums.ordering.direction.descending') }}
</option>
</select>
</div>
<div class="field">
<label for="album-results">{{ t('components.library.Albums.pagination.results') }}</label>
<select
id="album-results"
v-model="paginateBy"
class="ui dropdown"
>
<option
v-for="opt in paginateOptions"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</div>
</div>
</form>
<div
v-if="result"
transition-duration="0"
item-selector=".column"
percent-position="true"
stagger="0"
class=""
>
<div
v-if="result.results.length > 0"
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
<Input search
id="album-search"
v-model="query"
name="search"
:label="t('components.library.Albums.label.search')"
autofocus
:placeholder="labels.searchPlaceholder"
>
<album-card
v-for="album in result.results"
:key="album.id"
:album="album"
/>
</div>
<div
v-else
class="ui placeholder segment sixteen wide column"
style="text-align: center; display: flex; align-items: center"
>
<div class="ui icon header">
<i class="compact disc icon" />
{{ t('components.library.Albums.empty.noResults') }}
</div>
<router-link
v-if="store.state.auth.authenticated"
:to="{name: 'content.index'}"
class="ui success button labeled icon"
>
<i class="upload icon" />
{{ t('components.library.Albums.link.addMusic') }}
</router-link>
</div>
</div>
<div class="ui center aligned basic segment">
<pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
:paginate-by="paginateBy"
:total="result.count"
</Input>
<Pills
v-model="tagList"
:label="t('components.library.Albums.label.tags')"
style="max-width: 150px;"
/>
</div>
</section>
</main>
<select
id="album-ordering"
:label="t('components.library.Albums.ordering.label')"
v-model="ordering"
class="dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
<select
:label="t('components.library.Albums.ordering.direction.label')"
id="album-ordering-direction"
v-model="orderingDirection"
class="ui dropdown"
>
<option value="+">
{{ t('components.library.Albums.ordering.direction.ascending') }}
</option>
<option value="-">
{{ t('components.library.Albums.ordering.direction.descending') }}
</option>
</select>
<select
:label="t('components.library.Albums.pagination.results')"
id="album-results"
v-model="paginateBy"
class="ui dropdown"
>
<option
v-for="opt in paginateOptions"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</Layout>
<Layout grid
v-if="result && result.results.length > 0"
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
>
<Loader v-if="isLoading"/>
<album-card
v-for="album in result.results"
:key="album.id"
:album="album"
/>
</Layout>
<Layout
stack
v-else-if="result && result.results.length === 0"
>
<Alert yellow>
<i class="compact disc icon" />
{{ t('components.library.Albums.empty.noResults') }}
</Alert>
<Card
v-if="store.state.auth.authenticated"
:title="t('components.library.Albums.link.addMusic')"
solid
small
primary
:to="{name: 'content.index'}"
>
<template #image>
<i class="bi bi-upload" style="font-size: 100px; position: relative; top: 50px;" />
</template>
</Card>
</Layout>
<Spacer grow />
<Pagination
v-if="result && result.count > paginateBy"
:page="page"
:pages="Math.ceil((result?.results.length || 0)/paginateBy)"
/>
</Layout>
</template>

View File

@ -25,6 +25,7 @@ import Toggle from '~/components/ui/Toggle.vue'
import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Pills from '~/components/ui/Pills.vue'
import Loader from '~/components/ui/Loader.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useOrdering from '~/composables/navigation/useOrdering'
@ -48,6 +49,11 @@ const page = usePage()
const tags = useRouteQuery<string[]>('tag', [])
const tagList = computed(() => ({
current: tags.value,
others: []
}))
const q = useRouteQuery('query', '')
const query = ref(q.value)
syncRef(q, query, { direction: 'ltr' })
@ -123,11 +129,6 @@ const labels = computed(() => ({
title: t('components.library.Artists.title')
}))
const tagList = computed(() => ({
current: tags.value,
others: []
}))
const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value].sort((a, b) => a - b)))
</script>
@ -208,20 +209,16 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
v-if="result && result.results.length > 0"
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
>
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<Loader v-if="isLoading"/>
<ArtistCard
v-for="artist in result.results"
:key="artist.id"
:artist="artist"
/>
</Layout>
<Layout stack
v-else-if="!isLoading"
<Layout
stack
v-else-if="result && result.results.length === 0"
>
<Alert yellow>
<i class="compact disc icon" />

View File

@ -4,7 +4,7 @@ import type { Artist, BackendResponse } from '~/types'
import type { RouteRecordName } from 'vue-router'
import type { OrderingField } from '~/store/ui'
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, onMounted } from 'vue'
import { useRouteQuery } from '@vueuse/router'
import { useI18n } from 'vue-i18n'
import { syncRef } from '@vueuse/core'
@ -17,10 +17,13 @@ import TagsSelector from '~/components/library/TagsSelector.vue'
import RemoteSearchForm from '~/components/RemoteSearchForm.vue'
import Modal from '~/components/ui/Modal.vue'
import ArtistCard from '~/components/artist/Card.vue'
import Pagination from '~/components/vui/Pagination.vue'
import Popover from "~/components/ui/Popover.vue"
import PopoverItem from "~/components/ui/popover/PopoverItem.vue"
import Pagination from '~/components/ui/Pagination.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Pills from '~/components/ui/Pills.vue'
import useSharedLabels from '~/composables/locale/useSharedLabels'
import useOrdering from '~/composables/navigation/useOrdering'
@ -44,11 +47,16 @@ const page = usePage()
const tags = useRouteQuery<string[]>('tag', [])
const tagList = computed(() => ({
current: tags.value,
others: []
}))
const q = useRouteQuery('query', '')
const query = ref(q.value)
syncRef(q, query, { direction: 'ltr' })
const result = ref<BackendResponse<Artist>>()
const result = ref<BackendResponse<Playlist>>()
const showSubscribeModal = ref(false)
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
@ -112,6 +120,8 @@ onOrderingUpdate(() => {
fetchData()
})
// onMounted(() => $('.ui.dropdown').dropdown())
const { t } = useI18n()
const labels = computed(() => ({
searchPlaceholder: t('components.library.Podcasts.placeholder.search'),
@ -122,169 +132,141 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
</script>
<template>
<main v-title="labels.title">
<section class="ui vertical stripe segment">
<h2 class="ui header">
{{ t('components.library.Podcasts.header.browse') }}
</h2>
<form
:class="['ui', {'loading': isLoading}, 'form']"
@submit.prevent="search"
>
<div class="fields">
<div class="field">
<label for="artist-search">
{{ t('components.library.Podcasts.label.search') }}
</label>
<div class="ui action input">
<input
id="artist-search"
v-model="query"
type="text"
name="search"
:placeholder="labels.searchPlaceholder"
>
<Button
icon="bi-search"
type="submit"
:aria-label="t('components.library.Podcasts.button.search')"
>
</Button>
</div>
</div>
<div class="field">
<label for="tags-search">{{ t('components.library.Podcasts.label.tags') }}</label>
<tags-selector v-model="tags" />
</div>
<div class="field">
<label for="artist-ordering">{{ t('components.library.Podcasts.ordering.label') }}</label>
<Popover v-model:open="artistOrdering">
<OptionsButton @click="artistOrdering = !artistOrdering" id="artist-ordering" v-model="ordering"/>
<PopoverItem
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</PopoverItem>
</Popover>
</div>
<div class="field">
<label for="artist-ordering-direction">{{ t('components.library.Podcasts.ordering.direction.label') }}</label>
<select
id="artist-ordering-direction"
v-model="orderingDirection"
class="ui dropdown"
>
<option value="+">
{{ t('components.library.Podcasts.ordering.direction.ascending') }}
</option>
<option value="-">
{{ t('components.library.Podcasts.ordering.direction.descending') }}
</option>
</select>
</div>
<div class="field">
<label for="artist-results">{{ t('components.library.Podcasts.pagination.results') }}</label>
<select
id="artist-results"
v-model="paginateBy"
class="ui dropdown"
>
<option
v-for="opt in paginateOptions"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</div>
</div>
</form>
<div class="ui hidden divider" />
<div
v-if="result && result.results.length > 0"
class="ui five app-cards cards"
>
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<artist-card
v-for="artist in result.results"
:key="artist.id"
:artist="artist"
/>
</div>
<div
v-else-if="!isLoading"
class="ui placeholder segment sixteen wide column"
style="text-align: center; display: flex; align-items: center"
>
<div class="ui icon header">
<i class="podcast icon" />
{{ t('components.library.Podcasts.empty.noResults') }}
</div>
<router-link
v-if="store.state.auth.authenticated"
:to="{name: 'content.index'}"
class="ui success button labeled icon"
>
<i class="upload icon" />
{{ t('components.library.Podcasts.button.channel') }}
</router-link>
<h1
v-if="store.state.auth.authenticated"
class="ui with-actions header"
>
<div class="actions">
<a @click.stop.prevent="showSubscribeModal = true">
<i class="plus icon" />
{{ t('components.library.Podcasts.button.feed') }}
</a>
</div>
</h1>
</div>
<div class="ui center aligned basic segment">
<pagination
v-if="result && result.count > paginateBy"
v-model:current="page"
:paginate-by="paginateBy"
:total="result.count"
/>
</div>
</section>
<Modal
v-model:show="showSubscribeModal"
title="t('components.library.Podcasts.modal.subscription.header')"
<Layout stack main>
<h1>{{ t('components.library.Podcasts.header.browse') }}</h1>
<Layout form flex
:class="['ui', {'loading': isLoading}, 'form']"
@submit.prevent="search"
>
<div
ref="modalContent"
class="scrolling content"
<Input search
id="artist-search"
v-model="query"
name="search"
:label="t('components.library.Podcasts.label.search')"
autofocus
:placeholder="labels.searchPlaceholder"
>
<remote-search-form
initial-type="both"
:show-submit="false"
:standalone="false"
:redirect="true"
@subscribed="showSubscribeModal = false; fetchData()"
/>
</div>
<div class="actions">
<Button color="secondary">
{{ t('components.library.Podcasts.button.cancel') }}
</Input>
<Pills
v-model="tagList"
:label="t('components.library.Podcasts.label.tags')"
style="max-width: 150px;"
/>
<select
id="artist-ordering"
:label="t('components.library.Podcasts.ordering.label')"
v-model="ordering"
class="dropdown"
>
<option
v-for="(option, key) in orderingOptions"
:key="key"
:value="option[0]"
>
{{ sharedLabels.filters[option[1]] }}
</option>
</select>
<select
:label="t('components.library.Podcasts.ordering.direction.label')"
id="artist-ordering-direction"
v-model="orderingDirection"
class="ui dropdown"
>
<option value="+">
{{ t('components.library.Podcasts.ordering.direction.ascending') }}
</option>
<option value="-">
{{ t('components.library.Podcasts.ordering.direction.descending') }}
</option>
</select>
<select
:label="t('components.library.Podcasts.pagination.results')"
id="artist-results"
v-model="paginateBy"
class="ui dropdown"
>
<option
v-for="opt in paginateOptions"
:key="opt"
:value="opt"
>
{{ opt }}
</option>
</select>
</Layout>
<Layout grid
v-if="result && result.results.length > 0"
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
>
<Loader v-if="isLoading"/>
<artist-card
v-for="artist in result.results"
:key="artist.id"
:artist="artist"
/>
</Layout>
<Layout
stack
v-else-if="result && result.results.length === 0"
>
<Alert yellow>
{{ t('components.library.Podcasts.empty.noResults') }}
</Alert>
<Layout flex>
<Button
v-if="store.state.auth.authenticated"
icon="bi-plus"
primary
@click.stop.prevent="showSubscribeModal = true"
>
{{ t('components.library.Podcasts.button.feed') }}
</Button>
<Button
form="remote-search"
type="submit"
primary
v-if="store.state.auth.authenticated"
icon="bi-upload"
:to="{name: 'content.index'}"
>
<i class="bookmark icon" />
{{ t('components.library.Podcasts.button.subscribe') }}
{{ t('components.library.Podcasts.button.channel') }}
</Button>
</div>
</Modal>
</main>
</Layout>
</Layout>
<Spacer grow />
<Pagination
v-if="result && result.count > paginateBy"
:page="page"
:pages="Math.ceil((result?.results.length || 0)/paginateBy)"
/>
<Modal
v-model:show="showSubscribeModal"
title="t('components.library.Podcasts.modal.subscription.header')"
>
<div
ref="modalContent"
class="scrolling content"
>
<remote-search-form
initial-type="both"
:show-submit="false"
:standalone="false"
:redirect="true"
@subscribed="showSubscribeModal = false; fetchData()"
/>
</div>
<div class="actions">
<Button color="secondary">
{{ t('components.library.Podcasts.button.cancel') }}
</Button>
<Button
form="remote-search"
type="submit"
>
<i class="bookmark icon" />
{{ t('components.library.Podcasts.button.subscribe') }}
</Button>
</div>
</Modal>
</Layout>
</template>

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import ArtistCard from '~/components/artist/Card.vue'
import type { OrderingProps } from '~/composables/navigation/useOrdering'
import type { Artist, BackendResponse } from '~/types'
import type { Playlist, BackendResponse } from '~/types'
import type { RouteRecordName } from 'vue-router'
import type { OrderingField } from '~/store/ui'
@ -14,6 +13,7 @@ import { useStore } from '~/store'
import axios from 'axios'
import PlaylistCardList from '~/components/playlists/CardList.vue'
import Pagination from '~/components/ui/Pagination.vue'
import Layout from '~/components/ui/Layout.vue'
import Button from '~/components/ui/Button.vue'
@ -188,34 +188,28 @@ const paginateOptions = computed(() => sortedUniq([12, 30, 50, paginateBy.value]
v-if="result && result.results.length > 0"
style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;"
>
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<Loader v-if="isLoading"/>
<playlist-card-list
v-if="result && result.results.length > 0"
:playlists="result.results"
/>
<div
</Layout>
<Layout
stack
v-else-if="result && result.results.length === 0"
class="ui placeholder segment sixteen wide column"
style="text-align: center; display: flex; align-items: center"
>
<div class="ui icon header">
<i class="list icon" />
<Alert yellow>
{{ t('views.playlists.List.empty.noResults') }}
</div>
</Alert>
<Button
v-if="store.state.auth.authenticated"
icon="bi-list"
primary
@click="store.commit('playlists/showModal', true)"
>
{{ t('views.playlists.List.button.create') }}
</Button>
</div>
</Layout>
</Layout>
<Spacer grow />
<Pagination
v-if="result && result.count > paginateBy"