mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-29 00:23:23 -05:00
a1046f3913
Albums, artists, composers, genres, and any other items listed on page where they appear sorted by names are now all displayed properly.
223 lines
6.0 KiB
JavaScript
223 lines
6.0 KiB
JavaScript
import i18n from '@/i18n'
|
|
|
|
const { t, locale } = i18n.global
|
|
const GROUP_KEY_NONE = 'GROUP_KEY_NONE'
|
|
|
|
export function noop() {
|
|
return {
|
|
compareFn: null,
|
|
groupKeyFn: (item) => GROUP_KEY_NONE
|
|
}
|
|
}
|
|
|
|
export function byName(field, keepSortOrder = false, defaultValue = '_') {
|
|
return {
|
|
compareFn: keepSortOrder
|
|
? null
|
|
: (a, b) => {
|
|
const fieldA = a[field] || defaultValue
|
|
const fieldB = b[field] || defaultValue
|
|
return fieldA.localeCompare(fieldB, locale.value)
|
|
},
|
|
|
|
groupKeyFn: (item) => {
|
|
const value = (item[field] || defaultValue).charAt(0)
|
|
if (value.match(/\p{Letter}/gu)) {
|
|
return value.toUpperCase()
|
|
} else if (value.match(/\p{Number}/gu)) {
|
|
return '#'
|
|
} else {
|
|
return '⌘'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function byRating(field, { direction = 'asc', defaultValue = 0 }) {
|
|
return {
|
|
compareFn: (a, b) => {
|
|
const fieldA = a[field] || defaultValue
|
|
const fieldB = b[field] || defaultValue
|
|
const result = fieldA - fieldB
|
|
return direction === 'asc' ? result : result * -1
|
|
},
|
|
|
|
groupKeyFn: (item) => {
|
|
const fieldValue = item[field] || defaultValue
|
|
return Math.floor(fieldValue / 10)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function byYear(field, { direction = 'asc', defaultValue = '0000' }) {
|
|
return {
|
|
compareFn: (a, b) => {
|
|
const fieldA = a[field] || defaultValue
|
|
const fieldB = b[field] || defaultValue
|
|
const result = fieldA.localeCompare(fieldB, locale.value)
|
|
return direction === 'asc' ? result : result * -1
|
|
},
|
|
|
|
groupKeyFn: (item) => {
|
|
const fieldValue = item[field] || defaultValue
|
|
return fieldValue.substring(0, 4)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function byDateSinceToday(field, defaultValue = '0000') {
|
|
return {
|
|
compareFn: (a, b) => {
|
|
const fieldA = a[field] || defaultValue
|
|
const fieldB = b[field] || defaultValue
|
|
return fieldB.localeCompare(fieldA, locale.value)
|
|
},
|
|
|
|
groupKeyFn: (item) => {
|
|
const fieldValue = item[field]
|
|
if (!fieldValue) {
|
|
return defaultValue
|
|
}
|
|
const diff = new Date().getTime() - new Date(fieldValue).getTime()
|
|
if (diff < 86400000) {
|
|
// 24h
|
|
return t('group-by-list.today')
|
|
} else if (diff < 604800000) {
|
|
// 7 days
|
|
return t('group-by-list.last-week')
|
|
} else if (diff < 2592000000) {
|
|
// 30 days
|
|
return t('group-by-list.last-month')
|
|
}
|
|
return fieldValue.substring(0, 4)
|
|
}
|
|
}
|
|
}
|
|
|
|
export class GroupByList {
|
|
constructor({ items = [], total = 0, offset = 0, limit = -1 } = {}) {
|
|
this.items = items
|
|
this.total = total
|
|
this.offset = offset
|
|
this.limit = limit
|
|
|
|
this.count = items.length
|
|
this.indexList = []
|
|
this.group(noop())
|
|
}
|
|
|
|
get() {
|
|
return this.itemsByGroup
|
|
}
|
|
|
|
isEmpty() {
|
|
return !this.items || this.items.length <= 0
|
|
}
|
|
|
|
group(options, filterFns = []) {
|
|
const itemsFiltered = filterFns
|
|
? this.items.filter((item) => filterFns.every((fn) => fn(item)))
|
|
: this.items
|
|
this.count = itemsFiltered.length
|
|
|
|
// Sort item list
|
|
let itemsSorted = options.compareFn
|
|
? [...itemsFiltered].sort(options.compareFn)
|
|
: itemsFiltered
|
|
|
|
// Create index list
|
|
this.indexList = [...new Set(itemsSorted.map(options.groupKeyFn))]
|
|
|
|
// Group item list
|
|
this.itemsByGroup = itemsSorted.reduce((r, item) => {
|
|
const groupKey = options.groupKeyFn(item)
|
|
r[groupKey] = [...(r[groupKey] || []), item]
|
|
return r
|
|
}, {})
|
|
}
|
|
|
|
[Symbol.iterator]() {
|
|
// Use a new index for each iterator. This makes multiple
|
|
// iterations over the iterable safe for non-trivial cases,
|
|
// such as use of break or nested looping over the same iterable.
|
|
let groupIndex = -1
|
|
let itemIndex = -1
|
|
|
|
return {
|
|
next: () => {
|
|
if (this.isEmpty()) {
|
|
return { done: true }
|
|
} else if (groupIndex >= this.indexList.length) {
|
|
// We reached the end of all groups and items
|
|
//
|
|
// This should never happen, as the we already
|
|
// return "done" after we reached the last item
|
|
// of the last group
|
|
return { done: true }
|
|
} else if (groupIndex < 0) {
|
|
// We start iterating
|
|
//
|
|
// Return the first group title as the next item
|
|
++groupIndex
|
|
itemIndex = 0
|
|
|
|
if (this.indexList[groupIndex] !== GROUP_KEY_NONE) {
|
|
// Only return the group, if it is not the "noop" default group
|
|
return {
|
|
value: {
|
|
groupKey: this.indexList[groupIndex],
|
|
itemId: this.indexList[groupIndex],
|
|
isItem: false,
|
|
item: {}
|
|
},
|
|
done: false
|
|
}
|
|
}
|
|
}
|
|
|
|
let currentGroupKey = this.indexList[groupIndex]
|
|
let currentGroupItems = this.itemsByGroup[currentGroupKey]
|
|
|
|
if (itemIndex < currentGroupItems.length) {
|
|
// We are in a group with items left
|
|
//
|
|
// Return the current item and increment the item index
|
|
const currentItem = this.itemsByGroup[currentGroupKey][itemIndex++]
|
|
return {
|
|
value: {
|
|
groupKey: currentGroupKey,
|
|
itemId: currentItem.id,
|
|
isItem: true,
|
|
item: currentItem
|
|
},
|
|
done: false
|
|
}
|
|
} else {
|
|
// We reached the end of the current groups item list
|
|
//
|
|
// Move to the next group and return the group key/title
|
|
// as the next item
|
|
++groupIndex
|
|
itemIndex = 0
|
|
|
|
if (groupIndex < this.indexList.length) {
|
|
currentGroupKey = this.indexList[groupIndex]
|
|
return {
|
|
value: {
|
|
groupKey: currentGroupKey,
|
|
itemId: currentGroupKey,
|
|
isItem: false,
|
|
item: {}
|
|
},
|
|
done: false
|
|
}
|
|
} else {
|
|
// No group left, we are done iterating
|
|
return { done: true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|