Migrate pagination to v-model and start moving away from mixins
This commit is contained in:
parent
a25f1bbb1f
commit
3ab0435f27
|
@ -1,3 +1,66 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { range, clamp } from 'lodash-es'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
current?: number
|
||||||
|
paginateBy?: number
|
||||||
|
total: number,
|
||||||
|
compact?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
current: 1,
|
||||||
|
paginateBy: 25,
|
||||||
|
compact: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:current', 'pageChanged'])
|
||||||
|
const current = useVModel(props, 'current', emit)
|
||||||
|
|
||||||
|
const RANGE = 2
|
||||||
|
const pages = computed(() => {
|
||||||
|
const start = range(1, 1 + RANGE)
|
||||||
|
const end = range(maxPage.value - RANGE, maxPage.value)
|
||||||
|
const middle = range(
|
||||||
|
clamp(props.current - RANGE + 1, 1, maxPage.value),
|
||||||
|
clamp(props.current + RANGE, 1, maxPage.value)
|
||||||
|
).filter(i => !start.includes(i) && !end.includes(i))
|
||||||
|
|
||||||
|
console.log(middle, end)
|
||||||
|
|
||||||
|
return [
|
||||||
|
...start,
|
||||||
|
middle.length === 0 && 'skip',
|
||||||
|
middle.length !== 0 && start[start.length - 1] + 1 !== middle[0] && 'skip',
|
||||||
|
...middle,
|
||||||
|
middle.length !== 0 && middle[middle.length - 1] + 1 !== end[0] && 'skip',
|
||||||
|
...end
|
||||||
|
].filter(i => i !== false) as Array<'skip' | number>
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxPage = computed(() => Math.ceil(props.total / props.paginateBy))
|
||||||
|
|
||||||
|
const setPage = (page: number) => {
|
||||||
|
if (page > maxPage.value || page < 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
current.value = page
|
||||||
|
// TODO (wvffle): Compat before change to v-model
|
||||||
|
emit('pageChanged', page)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
pagination: $pgettext('Content/*/Hidden text/Noun', 'Pagination'),
|
||||||
|
previousPage: $pgettext('Content/*/Link', 'Previous Page'),
|
||||||
|
nextPage: $pgettext('Content/*/Link', 'Next Page')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="maxPage > 1"
|
v-if="maxPage > 1"
|
||||||
|
@ -6,107 +69,38 @@
|
||||||
:aria-label="labels.pagination"
|
:aria-label="labels.pagination"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href
|
href="#"
|
||||||
:disabled="current - 1 < 1 || null"
|
:disabled="current - 1 < 1 || null"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="labels.previousPage"
|
:aria-label="labels.previousPage"
|
||||||
:class="[{'disabled': current - 1 < 1}, 'item']"
|
:class="[{ 'disabled': current - 1 < 1 }, 'item']"
|
||||||
@click.prevent.stop="selectPage(current - 1)"
|
@click.prevent.stop="setPage(current - 1)"
|
||||||
><i class="angle left icon" /></a>
|
>
|
||||||
|
<i class="angle left icon" />
|
||||||
|
</a>
|
||||||
|
|
||||||
<template v-if="!compact">
|
<template v-if="!compact">
|
||||||
<a
|
<a
|
||||||
v-for="page in pages"
|
v-for="page in pages"
|
||||||
:key="page"
|
:key="page"
|
||||||
href
|
href="#"
|
||||||
:class="[{'active': page === current}, {'disabled': page === 'skip'}, 'item']"
|
:class="[{ active: page === current, disabled: page === 'skip' }, 'item']"
|
||||||
@click.prevent.stop="selectPage(page)"
|
@click.prevent.stop="page !== 'skip' && setPage(page)"
|
||||||
>
|
>
|
||||||
<span v-if="page !== 'skip'">{{ page }}</span>
|
<span v-if="page !== 'skip'">{{ page }}</span>
|
||||||
<span v-else>…</span>
|
<span v-else>…</span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href
|
href="#"
|
||||||
:disabled="current + 1 > maxPage || null"
|
:disabled="current + 1 > maxPage || null"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="labels.nextPage"
|
:aria-label="labels.nextPage"
|
||||||
:class="[{'disabled': current + 1 > maxPage}, 'item']"
|
:class="[{ disabled: current + 1 > maxPage }, 'item']"
|
||||||
@click.prevent.stop="selectPage(current + 1)"
|
@click.prevent.stop="setPage(current + 1)"
|
||||||
><i class="angle right icon" /></a>
|
>
|
||||||
|
<i class="angle right icon" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { range as lodashRange, sortBy, uniq } from 'lodash-es'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
current: { type: Number, default: 1 },
|
|
||||||
paginateBy: { type: Number, default: 25 },
|
|
||||||
total: { type: Number, required: true },
|
|
||||||
compact: { type: Boolean, default: false }
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
pagination: this.$pgettext('Content/*/Hidden text/Noun', 'Pagination'),
|
|
||||||
previousPage: this.$pgettext('Content/*/Link', 'Previous Page'),
|
|
||||||
nextPage: this.$pgettext('Content/*/Link', 'Next Page')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pages: function () {
|
|
||||||
const range = 2
|
|
||||||
const current = this.current
|
|
||||||
const beginning = lodashRange(1, Math.min(this.maxPage, 1 + range))
|
|
||||||
const middle = lodashRange(
|
|
||||||
Math.max(1, current - range + 1),
|
|
||||||
Math.min(this.maxPage, current + range)
|
|
||||||
)
|
|
||||||
const end = lodashRange(this.maxPage, Math.max(1, this.maxPage - range))
|
|
||||||
let allowed = beginning.concat(middle, end)
|
|
||||||
allowed = uniq(allowed)
|
|
||||||
allowed = sortBy(allowed, [
|
|
||||||
e => {
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const final = []
|
|
||||||
allowed.forEach(p => {
|
|
||||||
const last = final.slice(-1)[0]
|
|
||||||
let consecutive = true
|
|
||||||
if (last === 'skip') {
|
|
||||||
consecutive = false
|
|
||||||
} else {
|
|
||||||
if (!last) {
|
|
||||||
consecutive = true
|
|
||||||
} else {
|
|
||||||
consecutive = last + 1 === p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (consecutive) {
|
|
||||||
final.push(p)
|
|
||||||
} else {
|
|
||||||
if (p !== 'skip') {
|
|
||||||
final.push('skip')
|
|
||||||
final.push(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return final
|
|
||||||
},
|
|
||||||
maxPage: function () {
|
|
||||||
return Math.ceil(this.total / this.paginateBy)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
selectPage: function (page) {
|
|
||||||
if (page > this.maxPage || page < 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.current !== page) {
|
|
||||||
this.$emit('page-changed', page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -196,6 +196,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="actions.length > 0">
|
<th v-if="actions.length > 0">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
|
<!-- TODO (wvffle): Check if we don't have to migrate to v-model -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:aria-label="labels.selectAllItems"
|
:aria-label="labels.selectAllItems"
|
||||||
|
@ -217,6 +218,7 @@
|
||||||
v-if="actions.length > 0"
|
v-if="actions.length > 0"
|
||||||
class="collapsing"
|
class="collapsing"
|
||||||
>
|
>
|
||||||
|
<!-- TODO (wvffle): Check if we don't have to migrate to v-model -->
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:aria-label="labels.selectItem"
|
:aria-label="labels.selectItem"
|
||||||
|
|
|
@ -1,245 +1,195 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import axios from 'axios'
|
||||||
|
import $ from 'jquery'
|
||||||
|
import RadioButton from '~/components/radios/Button.vue'
|
||||||
|
import Pagination from '~/components/Pagination.vue'
|
||||||
|
import { checkRedirectToLogin } from '~/utils'
|
||||||
|
import TrackTable from '~/components/audio/track/Table.vue'
|
||||||
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useOrdering from '~/composables/useOrdering'
|
||||||
|
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { Track } from '~/types'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { OrderingField, RouteWithPreferences } from '~/store/ui'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orderingConfigName: RouteWithPreferences | null
|
||||||
|
defaultPage?: number,
|
||||||
|
defaultPaginateBy?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
defaultPage: 1,
|
||||||
|
defaultPaginateBy: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
await checkRedirectToLogin(store, useRouter())
|
||||||
|
|
||||||
|
// TODO (wvffle): Make sure everything is it's own type
|
||||||
|
const page = ref(+props.defaultPage)
|
||||||
|
|
||||||
|
const orderingOptions: [OrderingField, keyof typeof sharedLabels.filters][] = [
|
||||||
|
['creation_date', 'creation_date'],
|
||||||
|
['title', 'track_title'],
|
||||||
|
['album__title', 'album_title'],
|
||||||
|
['artist__name', 'artist_name']
|
||||||
|
]
|
||||||
|
|
||||||
|
const logger = useLogger()
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { onOrderingUpdate, orderingString, paginateBy, ordering, orderingDirection } = useOrdering(props.orderingConfigName)
|
||||||
|
|
||||||
|
const updateQueryString = () => router.replace({
|
||||||
|
query: {
|
||||||
|
page: page.value,
|
||||||
|
paginateBy: paginateBy.value,
|
||||||
|
ordering: orderingString.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const results = reactive<Track[]>([])
|
||||||
|
const nextLink = ref()
|
||||||
|
const previousLink = ref()
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const fetchFavorites = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
favorites: 'true',
|
||||||
|
page: page.value,
|
||||||
|
page_size: paginateBy.value,
|
||||||
|
ordering: orderingString.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.time('Loading user favorites')
|
||||||
|
const response = await axios.get('tracks/', { params: params })
|
||||||
|
|
||||||
|
results.length = 0
|
||||||
|
results.push(...response.data.results)
|
||||||
|
|
||||||
|
for (const track of results) {
|
||||||
|
store.commit('favorites/track', { id: track.id, value: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
count.value = response.data.count
|
||||||
|
nextLink.value = response.data.next
|
||||||
|
previousLink.value = response.data.previous
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
} finally {
|
||||||
|
logger.timeEnd('Loading user favorites')
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(page, updateQueryString)
|
||||||
|
onOrderingUpdate(updateQueryString)
|
||||||
|
onBeforeRouteUpdate(fetchFavorites)
|
||||||
|
fetchFavorites()
|
||||||
|
|
||||||
|
// @ts-expect-error semantic ui
|
||||||
|
onMounted(() => $('.ui.dropdown').dropdown())
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const labels = computed(() => ({
|
||||||
|
title: $pgettext('Head/Favorites/Title', 'Your Favorites')
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main
|
<main v-title="labels.title" class="main pusher">
|
||||||
v-title="labels.title"
|
|
||||||
class="main pusher"
|
|
||||||
>
|
|
||||||
<section class="ui vertical center aligned stripe segment">
|
<section class="ui vertical center aligned stripe segment">
|
||||||
<div :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']">
|
<div :class="['ui', { 'active': isLoading }, 'inverted', 'dimmer']">
|
||||||
<div class="ui text loader">
|
<div class="ui text loader">
|
||||||
<translate translate-context="Content/Favorites/Message">
|
<translate translate-context="Content/Favorites/Message">Loading your favorites…</translate>
|
||||||
Loading your favorites…
|
|
||||||
</translate>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2 v-if="results" class="ui center aligned icon header">
|
||||||
v-if="results"
|
|
||||||
class="ui center aligned icon header"
|
|
||||||
>
|
|
||||||
<i class="circular inverted heart pink icon" />
|
<i class="circular inverted heart pink icon" />
|
||||||
<translate
|
<translate
|
||||||
translate-plural="%{ count } favorites"
|
translate-plural="%{ count } favorites"
|
||||||
:translate-n="$store.state.favorites.count"
|
:translate-n="$store.state.favorites.count"
|
||||||
:translate-params="{count: results.count}"
|
:translate-params="{ count }"
|
||||||
translate-context="Content/Favorites/Title"
|
translate-context="Content/Favorites/Title"
|
||||||
>
|
>%{ count } favorite</translate>
|
||||||
%{ count } favorite
|
|
||||||
</translate>
|
|
||||||
</h2>
|
</h2>
|
||||||
<radio-button
|
<radio-button v-if="$store.state.favorites.count > 0" type="favorites" />
|
||||||
v-if="hasFavorites"
|
|
||||||
type="favorites"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section v-if="$store.state.favorites.count > 0" class="ui vertical stripe segment">
|
||||||
v-if="hasFavorites"
|
<div :class="['ui', { 'loading': isLoading }, 'form']">
|
||||||
class="ui vertical stripe segment"
|
|
||||||
>
|
|
||||||
<div :class="['ui', {'loading': isLoading}, 'form']">
|
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-ordering"><translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate></label>
|
<label for="favorites-ordering">
|
||||||
<select
|
<translate translate-context="Content/Search/Dropdown.Label/Noun">Ordering</translate>
|
||||||
id="favorites-ordering"
|
</label>
|
||||||
v-model="ordering"
|
<select id="favorites-ordering" v-model="ordering" class="ui dropdown">
|
||||||
class="ui dropdown"
|
|
||||||
>
|
|
||||||
<option
|
<option
|
||||||
v-for="option in orderingOptions"
|
v-for="option in orderingOptions"
|
||||||
:key="option[0]"
|
:key="option[0]"
|
||||||
:value="option[0]"
|
:value="option[0]"
|
||||||
>
|
>{{ sharedLabels.filters[option[1]] }}</option>
|
||||||
{{ sharedLabels.filters[option[1]] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-ordering-direction"><translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate></label>
|
<label for="favorites-ordering-direction">
|
||||||
|
<translate translate-context="Content/Search/Dropdown.Label/Noun">Order</translate>
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
id="favorites-ordering-direction"
|
id="favorites-ordering-direction"
|
||||||
v-model="orderingDirection"
|
v-model="orderingDirection"
|
||||||
class="ui dropdown"
|
class="ui dropdown"
|
||||||
>
|
>
|
||||||
<option value="+">
|
<option value="+">
|
||||||
<translate translate-context="Content/Search/Dropdown">
|
<translate translate-context="Content/Search/Dropdown">Ascending</translate>
|
||||||
Ascending
|
|
||||||
</translate>
|
|
||||||
</option>
|
</option>
|
||||||
<option value="-">
|
<option value="-">
|
||||||
<translate translate-context="Content/Search/Dropdown">
|
<translate translate-context="Content/Search/Dropdown">Descending</translate>
|
||||||
Descending
|
|
||||||
</translate>
|
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="favorites-results"><translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate></label>
|
<label for="favorites-results">
|
||||||
<select
|
<translate translate-context="Content/Search/Dropdown.Label/Noun">Results per page</translate>
|
||||||
id="favorites-results"
|
</label>
|
||||||
v-model="paginateBy"
|
<select id="favorites-results" v-model="paginateBy" class="ui dropdown">
|
||||||
class="ui dropdown"
|
<option :value="12">12</option>
|
||||||
>
|
<option :value="25">25</option>
|
||||||
<option :value="parseInt(12)">
|
<option :value="50">50</option>
|
||||||
12
|
|
||||||
</option>
|
|
||||||
<option :value="parseInt(25)">
|
|
||||||
25
|
|
||||||
</option>
|
|
||||||
<option :value="parseInt(50)">
|
|
||||||
50
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<track-table
|
<track-table v-if="results" :show-artist="true" :show-album="true" :tracks="results" />
|
||||||
v-if="results"
|
|
||||||
:show-artist="true"
|
|
||||||
:show-album="true"
|
|
||||||
:tracks="results.results"
|
|
||||||
/>
|
|
||||||
<div class="ui center aligned basic segment">
|
<div class="ui center aligned basic segment">
|
||||||
<pagination
|
<pagination
|
||||||
v-if="results && results.count > paginateBy"
|
v-if="results && count > paginateBy"
|
||||||
:current="page"
|
v-model:current="page"
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total="results.count"
|
:total="count"
|
||||||
@page-changed="selectPage"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div
|
<div v-else class="ui placeholder segment">
|
||||||
v-else
|
|
||||||
class="ui placeholder segment"
|
|
||||||
>
|
|
||||||
<div class="ui icon header">
|
<div class="ui icon header">
|
||||||
<i class="broken heart icon" />
|
<i class="broken heart icon" />
|
||||||
<translate
|
<translate
|
||||||
translate-context="Content/Home/Placeholder"
|
translate-context="Content/Home/Placeholder"
|
||||||
>
|
>No tracks have been added to your favorites yet</translate>
|
||||||
No tracks have been added to your favorites yet
|
|
||||||
</translate>
|
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link :to="'/library'" class="ui success labeled icon button">
|
||||||
:to="'/library'"
|
|
||||||
class="ui success labeled icon button"
|
|
||||||
>
|
|
||||||
<i class="headphones icon" />
|
<i class="headphones icon" />
|
||||||
<translate translate-context="Content/*/Verb">
|
<translate translate-context="Content/*/Verb">Browse the library</translate>
|
||||||
Browse the library
|
|
||||||
</translate>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import $ from 'jquery'
|
|
||||||
import RadioButton from '~/components/radios/Button.vue'
|
|
||||||
import Pagination from '~/components/Pagination.vue'
|
|
||||||
import OrderingMixin from '~/components/mixins/Ordering.vue'
|
|
||||||
import PaginationMixin from '~/components/mixins/Pagination.vue'
|
|
||||||
import { checkRedirectToLogin } from '~/utils'
|
|
||||||
import TrackTable from '~/components/audio/track/Table.vue'
|
|
||||||
import useLogger from '~/composables/useLogger'
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|
||||||
|
|
||||||
const logger = useLogger()
|
|
||||||
|
|
||||||
const FAVORITES_URL = 'tracks/'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
RadioButton,
|
|
||||||
Pagination,
|
|
||||||
TrackTable
|
|
||||||
},
|
|
||||||
mixins: [OrderingMixin, PaginationMixin],
|
|
||||||
setup () {
|
|
||||||
const sharedLabels = useSharedLabels()
|
|
||||||
return { sharedLabels }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
results: null,
|
|
||||||
isLoading: false,
|
|
||||||
nextLink: null,
|
|
||||||
previousLink: null,
|
|
||||||
page: parseInt(this.defaultPage),
|
|
||||||
orderingOptions: [
|
|
||||||
['creation_date', 'creation_date'],
|
|
||||||
['title', 'track_title'],
|
|
||||||
['album__title', 'album_title'],
|
|
||||||
['artist__name', 'artist_name']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
title: this.$pgettext('Head/Favorites/Title', 'Your Favorites')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasFavorites () {
|
|
||||||
return this.$store.state.favorites.count > 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
page: function () {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
paginateBy: function () {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
orderingDirection: function () {
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
ordering: function () {
|
|
||||||
this.updateQueryString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async created () {
|
|
||||||
await checkRedirectToLogin(this.$store, this.$router)
|
|
||||||
this.fetchFavorites(FAVORITES_URL)
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
$('.ui.dropdown').dropdown()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateQueryString: function () {
|
|
||||||
this.$router.replace({
|
|
||||||
query: {
|
|
||||||
page: this.page,
|
|
||||||
paginateBy: this.paginateBy,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.fetchFavorites(FAVORITES_URL)
|
|
||||||
},
|
|
||||||
fetchFavorites (url) {
|
|
||||||
const self = this
|
|
||||||
this.isLoading = true
|
|
||||||
const params = {
|
|
||||||
favorites: 'true',
|
|
||||||
page: this.page,
|
|
||||||
page_size: this.paginateBy,
|
|
||||||
ordering: this.getOrderingAsString()
|
|
||||||
}
|
|
||||||
logger.time('Loading user favorites')
|
|
||||||
axios.get(url, { params: params }).then(response => {
|
|
||||||
self.results = response.data
|
|
||||||
self.nextLink = response.data.next
|
|
||||||
self.previousLink = response.data.previous
|
|
||||||
self.results.results.forEach(track => {
|
|
||||||
self.$store.commit('favorites/track', { id: track.id, value: true })
|
|
||||||
})
|
|
||||||
logger.timeEnd('Loading user favorites')
|
|
||||||
self.isLoading = false
|
|
||||||
})
|
|
||||||
},
|
|
||||||
selectPage: function (page) {
|
|
||||||
this.page = page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
defaultOrdering: { type: String, required: false, default: '' },
|
|
||||||
orderingConfigName: { type: String, required: false, default: '' }
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
orderingConfig () {
|
|
||||||
return this.$store.state.ui.routePreferences[this.orderingConfigName || this.$route.name]
|
|
||||||
},
|
|
||||||
paginateBy: {
|
|
||||||
set (paginateBy) {
|
|
||||||
this.$store.commit('ui/paginateBy', {
|
|
||||||
route: this.$route.name,
|
|
||||||
value: paginateBy
|
|
||||||
})
|
|
||||||
},
|
|
||||||
get () {
|
|
||||||
return this.orderingConfig.paginateBy
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ordering: {
|
|
||||||
set (ordering) {
|
|
||||||
this.$store.commit('ui/ordering', {
|
|
||||||
route: this.$route.name,
|
|
||||||
value: ordering
|
|
||||||
})
|
|
||||||
},
|
|
||||||
get () {
|
|
||||||
return this.orderingConfig.ordering
|
|
||||||
}
|
|
||||||
},
|
|
||||||
orderingDirection: {
|
|
||||||
set (orderingDirection) {
|
|
||||||
this.$store.commit('ui/orderingDirection', {
|
|
||||||
route: this.$route.name,
|
|
||||||
value: orderingDirection
|
|
||||||
})
|
|
||||||
},
|
|
||||||
get () {
|
|
||||||
return this.orderingConfig.orderingDirection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getOrderingFromString (s) {
|
|
||||||
const parts = s.split('-')
|
|
||||||
if (parts.length > 1) {
|
|
||||||
return {
|
|
||||||
direction: '-',
|
|
||||||
field: parts.slice(1).join('-')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
direction: '+',
|
|
||||||
field: s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getOrderingAsString () {
|
|
||||||
let direction = this.orderingDirection
|
|
||||||
if (direction === '+') {
|
|
||||||
direction = ''
|
|
||||||
}
|
|
||||||
return [direction, this.ordering].join('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { MaybeRef, reactiveComputed, toRefs } from '@vueuse/core'
|
||||||
|
import { computed, unref, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
import { OrderingDirection, OrderingField, RouteWithPreferences } from '~/store/ui'
|
||||||
|
|
||||||
|
export default (orderingConfigName: MaybeRef<RouteWithPreferences | null>) => {
|
||||||
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const config = reactiveComputed(() => {
|
||||||
|
const name = unref(orderingConfigName) ?? route.name as RouteWithPreferences
|
||||||
|
return store.state.ui.routePreferences[name]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { paginateBy, ordering, orderingDirection } = toRefs(config)
|
||||||
|
|
||||||
|
const orderingString = computed(() => {
|
||||||
|
if (orderingDirection.value === '-') return `-${ordering.value}`
|
||||||
|
return ordering.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const getOrderingFromString = (str: string) => ({
|
||||||
|
direction: (str[0] === '-' ? '-' : '+') as OrderingDirection,
|
||||||
|
field: (str[0] === '-' || str[0] === '+' ? str.slice(1) : str) as OrderingField
|
||||||
|
})
|
||||||
|
|
||||||
|
const onOrderingUpdate = (fn: () => void) => watch(config, fn)
|
||||||
|
|
||||||
|
return {
|
||||||
|
paginateBy,
|
||||||
|
ordering,
|
||||||
|
orderingDirection,
|
||||||
|
orderingString,
|
||||||
|
getOrderingFromString,
|
||||||
|
onOrderingUpdate
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ Promise.all(modules).finally(() => {
|
||||||
logger.info('Everything loaded!')
|
logger.info('Everything loaded!')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO (wvffle): Rename filters from useSharedLabels to filters from backend
|
||||||
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
|
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
|
||||||
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
|
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
|
||||||
// TODO (wvffle): Find all array watchers and make them deep
|
// TODO (wvffle): Find all array watchers and make them deep
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { availableLanguages } from '~/init/locale'
|
||||||
|
|
||||||
type SupportedExtension = 'flac' | 'ogg' | 'mp3' | 'opus' | 'aac' | 'm4a' | 'aiff' | 'aif'
|
type SupportedExtension = 'flac' | 'ogg' | 'mp3' | 'opus' | 'aac' | 'm4a' | 'aiff' | 'aif'
|
||||||
|
|
||||||
type RouteWithPreferences = 'library.artists.browse' | 'library.podcasts.browse' | 'library.radios.browse'
|
export type RouteWithPreferences = 'library.artists.browse' | 'library.podcasts.browse' | 'library.radios.browse'
|
||||||
| 'library.playlists.browse' | 'library.albums.me' | 'library.artists.me' | 'library.radios.me'
|
| 'library.playlists.browse' | 'library.albums.me' | 'library.artists.me' | 'library.radios.me'
|
||||||
| 'library.playlists.me' | 'content.libraries.files' | 'library.detail.upload' | 'library.detail.edit'
|
| 'library.playlists.me' | 'content.libraries.files' | 'library.detail.upload' | 'library.detail.edit'
|
||||||
| 'library.detail' | 'favorites' | 'manage.channels' | 'manage.library.tags' | 'manage.library.uploads'
|
| 'library.detail' | 'favorites' | 'manage.channels' | 'manage.library.tags' | 'manage.library.uploads'
|
||||||
|
@ -18,12 +18,12 @@ type RouteWithPreferences = 'library.artists.browse' | 'library.podcasts.browse'
|
||||||
export type WebSocketEventName = 'inbox.item_added' | 'import.status_updated' | 'mutation.created' | 'mutation.updated'
|
export type WebSocketEventName = 'inbox.item_added' | 'import.status_updated' | 'mutation.created' | 'mutation.updated'
|
||||||
| 'report.created' | 'user_request.created' | 'Listen'
|
| 'report.created' | 'user_request.created' | 'Listen'
|
||||||
|
|
||||||
type Ordering = 'creation_date'
|
export type OrderingField = 'creation_date' | 'title' | 'album__title' | 'artist__name'
|
||||||
type OrderingDirection = '-'
|
export type OrderingDirection = '-' | '+'
|
||||||
interface RoutePreferences {
|
interface RoutePreferences {
|
||||||
paginateBy: number
|
paginateBy: number
|
||||||
orderingDirection: OrderingDirection
|
orderingDirection: OrderingDirection
|
||||||
ordering: Ordering
|
ordering: OrderingField
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketEvent {
|
interface WebSocketEvent {
|
||||||
|
@ -351,16 +351,6 @@ const store: Module<State, RootState> = {
|
||||||
pageTitle: (state, value) => {
|
pageTitle: (state, value) => {
|
||||||
state.pageTitle = value
|
state.pageTitle = value
|
||||||
},
|
},
|
||||||
paginateBy: (state, { route, value }: { route: RouteWithPreferences, value: number }) => {
|
|
||||||
state.routePreferences[route].paginateBy = value
|
|
||||||
},
|
|
||||||
ordering: (state, { route, value }: { route: RouteWithPreferences, value: Ordering }) => {
|
|
||||||
state.routePreferences[route].ordering = value
|
|
||||||
},
|
|
||||||
orderingDirection: (state, { route, value }: { route: RouteWithPreferences, value: OrderingDirection }) => {
|
|
||||||
state.routePreferences[route].orderingDirection = value
|
|
||||||
},
|
|
||||||
|
|
||||||
window: (state, value) => {
|
window: (state, value) => {
|
||||||
state.window = value
|
state.window = value
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue