[#414] Added ability to sort Jellyin results.

This also adds a new `FloatingDropdownButton` component.
This commit is contained in:
Fabio Manganiello 2024-09-28 13:22:27 +02:00
parent bf82ad9bf0
commit 1a53c59382
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 227 additions and 6 deletions

View file

@ -59,11 +59,19 @@ export default {
}, },
computed: { computed: {
button() {
const el = this.$refs.button?.$el
if (!el)
return this.$refs.button
return el.querySelector('button')
},
buttonStyle() { buttonStyle() {
if (!this.$refs.button) if (!this.button)
return {} return {}
return getComputedStyle(this.$refs.button) return getComputedStyle(this.button)
}, },
buttonWidth() { buttonWidth() {
@ -140,7 +148,7 @@ export default {
}, },
adjustDropdownPos() { adjustDropdownPos() {
const buttonRect = this.$refs.button.getBoundingClientRect() const buttonRect = this.button.getBoundingClientRect()
const buttonPos = { const buttonPos = {
left: buttonRect.left + window.scrollX, left: buttonRect.left + window.scrollX,
top: buttonRect.top + window.scrollY, top: buttonRect.top + window.scrollY,

View file

@ -0,0 +1,60 @@
<template>
<div class="floating-dropdown-container">
<FloatingButton :disabled="disabled"
:iconClass="iconClass"
:iconUrl="iconUrl"
:glow="glow"
:left="left"
:right="right"
:title="title"
:top="top"
:bottom="bottom"
ref="button"
@click.stop="toggle($event)" />
<div class="body-container hidden" ref="dropdownContainer">
<DropdownBody :id="id"
:keepOpenOnItemClick="keepOpenOnItemClick"
:style="style"
ref="dropdown"
@click="onClick">
<slot />
</DropdownBody>
</div>
</div>
</template>
<script>
import Dropdown from "./Dropdown";
import DropdownBody from "./DropdownBody";
import FloatingButton from "@/components/elements/FloatingButton";
export default {
mixins: [Dropdown, FloatingButton],
emits: ['click'],
components: {
DropdownBody,
FloatingButton,
},
}
</script>
<style lang="scss" scoped>
.floating-dropdown-container {
.body-container {
position: relative;
display: inline-flex;
flex-direction: column;
button {
background: none;
border: 0;
padding: 0.5em;
&:hover {
color: $default-hover-fg;
}
}
}
}
</style>

View file

@ -24,7 +24,7 @@ import Index from "./Jellyfin/Index";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import MediaNav from "./Nav"; import MediaNav from "./Nav";
import MediaProvider from "./Mixin"; import MediaProvider from "./Mixin";
import Movies from "./Jellyfin/Collections/Movies/Index"; import Movies from "./Jellyfin/collections/Movies/Index";
export default { export default {
mixins: [MediaProvider], mixins: [MediaProvider],

View file

@ -18,6 +18,8 @@
@remove-from-playlist="$emit('remove-from-playlist', $event)" @remove-from-playlist="$emit('remove-from-playlist', $event)"
@select="selectedResult = $event" @select="selectedResult = $event"
v-else /> v-else />
<SortButton :value="sort" @input="sort = $event" v-if="sortedMovies.length > 0" />
</div> </div>
</template> </template>
@ -26,6 +28,7 @@ import Loading from "@/components/Loading";
import MediaProvider from "@/components/panels/Media/Providers/Mixin"; import MediaProvider from "@/components/panels/Media/Providers/Mixin";
import NoItems from "@/components/elements/NoItems"; import NoItems from "@/components/elements/NoItems";
import Results from "@/components/panels/Media/Results"; import Results from "@/components/panels/Media/Results";
import SortButton from "@/components/panels/Media/Providers/Jellyfin/components/SortButton";
export default { export default {
mixins: [MediaProvider], mixins: [MediaProvider],
@ -33,6 +36,7 @@ export default {
Loading, Loading,
NoItems, NoItems,
Results, Results,
SortButton,
}, },
emits: [ emits: [
@ -52,9 +56,13 @@ export default {
data() { data() {
return { return {
movies: {}, movies: [],
loading_: false, loading_: false,
selectedResult: null, selectedResult: null,
sort: {
attr: 'title',
desc: false,
},
}; };
}, },
@ -64,8 +72,25 @@ export default {
}, },
sortedMovies() { sortedMovies() {
if (!this.movies) {
return []
}
return [...this.movies].sort((a, b) => { return [...this.movies].sort((a, b) => {
return a.title.localeCompare(b.title) const attr = this.sort.attr
const desc = this.sort.desc
let aVal = a[attr]
let bVal = b[attr]
if (typeof aVal === 'number' || typeof bVal === 'number') {
aVal = aVal || 0
bVal = bVal || 0
return desc ? bVal - aVal : aVal - bVal
}
aVal = (aVal || '').toString().toLowerCase()
bVal = (bVal || '').toString().toLowerCase()
return desc ? bVal.localeCompare(aVal) : aVal.localeCompare(bVal)
}).map((movie) => { }).map((movie) => {
return { return {
item_type: movie.type, item_type: movie.type,
@ -113,4 +138,8 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/components/panels/Media/Providers/Jellyfin/common.scss"; @import "@/components/panels/Media/Providers/Jellyfin/common.scss";
.index {
position: relative;
}
</style> </style>

View file

@ -0,0 +1,124 @@
<template>
<div class="sort-buttons">
<Dropdown :icon-class="btnIconClass"
glow right
title="Sort Direction">
<div class="sort-buttons-dropdown-body">
<div class="title">Sort Direction</div>
<DropdownItem text="Ascending"
icon-class="fa fa-arrow-up-short-wide"
:item-class="{ active: !value?.desc }"
@input="onDescChange(false)" />
<DropdownItem text="Descending"
icon-class="fa fa-arrow-down-wide-short"
:item-class="{ active: value?.desc }"
@input="onDescChange(true)" />
<div class="title">Sort By</div>
<DropdownItem text="Name"
icon-class="fa fa-font"
:item-class="{ active: value?.attr === 'title' }"
@input="onAttrChange('title')" />
<DropdownItem text="Release Date"
icon-class="fa fa-calendar"
:item-class="{ active: value?.attr === 'year' }"
@input="onAttrChange('year')" />
<DropdownItem text="Critics Rating"
icon-class="fa fa-star"
:item-class="{ active: value?.attr === 'critic_rating' }"
@input="onAttrChange('critic_rating')" />
<DropdownItem text="Community Rating"
icon-class="fa fa-users"
:item-class="{ active: value?.attr === 'community_rating' }"
@input="onAttrChange('community_rating')" />
</div>
</Dropdown>
</div>
</template>
<script>
import Dropdown from "@/components/elements/FloatingDropdownButton";
import DropdownItem from "@/components/elements/DropdownItem";
import Utils from '@/Utils'
export default {
emits: ['input'],
mixins: [Utils],
components: {
Dropdown,
DropdownItem,
},
props: {
value: {
type: Object,
required: true,
},
},
computed: {
btnIconClass() {
return this.value?.desc ? 'fa fa-arrow-down-wide-short' : 'fa fa-arrow-up-short-wide'
},
},
methods: {
onAttrChange(attr) {
this.$emit('input', { attr, desc: !!this.value?.desc })
},
onDescChange(desc) {
this.$emit('input', { attr: this.value?.attr, desc })
},
},
watch: {
value() {
this.setUrlArgs({
sort: this.value?.attr,
desc: this.value?.desc,
})
},
},
mounted() {
const urlArgs = this.getUrlArgs()
const sortBy = urlArgs.sort
const desc = urlArgs.desc?.toString() === 'true'
if (sortBy || desc) {
this.$emit('input', { attr: sortBy, desc })
}
},
unmounted() {
this.setUrlArgs({
sort: null,
desc: null,
})
},
}
</script>
<style lang="scss">
.sort-buttons {
.floating-btn {
z-index: 100;
}
}
.sort-buttons-dropdown-body {
.title {
font-weight: bold;
text-align: center;
box-shadow: $border-shadow-bottom;
border-top: $default-border-2;
}
.item {
&.active {
color: $selected-fg;
}
}
}
</style>