From 1c88c0a796c2c22a605432dac8470dc6088105d8 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 18 Aug 2021 20:18:44 -0500 Subject: [PATCH] Sorting and filtering --- client/.nuxt/components/index.js | 4 + client/.nuxt/components/plugin.js | 2 + client/.nuxt/components/readme.md | 2 + client/.nuxt/vetur/tags.json | 6 + client/assets/fastSort.js | 158 ++++++++++++++++++++ client/components/app/BookShelf.vue | 42 +++--- client/components/app/BookShelfToolbar.vue | 44 ++++-- client/components/controls/FilterSelect.vue | 79 ++++++++++ client/components/controls/OrderSelect.vue | 101 +++++++++++++ client/package.json | 2 +- client/store/audiobooks.js | 18 ++- client/store/settings.js | 29 +++- client/tailwind.config.js | 3 + package.json | 2 +- 14 files changed, 450 insertions(+), 42 deletions(-) create mode 100644 client/assets/fastSort.js create mode 100644 client/components/controls/FilterSelect.vue create mode 100644 client/components/controls/OrderSelect.vue diff --git a/client/.nuxt/components/index.js b/client/.nuxt/components/index.js index 51e7786b..275852ce 100644 --- a/client/.nuxt/components/index.js +++ b/client/.nuxt/components/index.js @@ -7,6 +7,8 @@ export { default as AppBookShelfToolbar } from '../..\\components\\app\\BookShel export { default as AppStreamContainer } from '../..\\components\\app\\StreamContainer.vue' export { default as CardsBookCard } from '../..\\components\\cards\\BookCard.vue' export { default as CardsBookCover } from '../..\\components\\cards\\BookCover.vue' +export { default as ControlsFilterSelect } from '../..\\components\\controls\\FilterSelect.vue' +export { default as ControlsOrderSelect } from '../..\\components\\controls\\OrderSelect.vue' export { default as ControlsVolumeControl } from '../..\\components\\controls\\VolumeControl.vue' export { default as ModalsEditModal } from '../..\\components\\modals\\EditModal.vue' export { default as ModalsModal } from '../..\\components\\modals\\Modal.vue' @@ -34,6 +36,8 @@ export const LazyAppBookShelfToolbar = import('../..\\components\\app\\BookShelf export const LazyAppStreamContainer = import('../..\\components\\app\\StreamContainer.vue' /* webpackChunkName: "components/app-stream-container" */).then(c => wrapFunctional(c.default || c)) export const LazyCardsBookCard = import('../..\\components\\cards\\BookCard.vue' /* webpackChunkName: "components/cards-book-card" */).then(c => wrapFunctional(c.default || c)) export const LazyCardsBookCover = import('../..\\components\\cards\\BookCover.vue' /* webpackChunkName: "components/cards-book-cover" */).then(c => wrapFunctional(c.default || c)) +export const LazyControlsFilterSelect = import('../..\\components\\controls\\FilterSelect.vue' /* webpackChunkName: "components/controls-filter-select" */).then(c => wrapFunctional(c.default || c)) +export const LazyControlsOrderSelect = import('../..\\components\\controls\\OrderSelect.vue' /* webpackChunkName: "components/controls-order-select" */).then(c => wrapFunctional(c.default || c)) export const LazyControlsVolumeControl = import('../..\\components\\controls\\VolumeControl.vue' /* webpackChunkName: "components/controls-volume-control" */).then(c => wrapFunctional(c.default || c)) export const LazyModalsEditModal = import('../..\\components\\modals\\EditModal.vue' /* webpackChunkName: "components/modals-edit-modal" */).then(c => wrapFunctional(c.default || c)) export const LazyModalsModal = import('../..\\components\\modals\\Modal.vue' /* webpackChunkName: "components/modals-modal" */).then(c => wrapFunctional(c.default || c)) diff --git a/client/.nuxt/components/plugin.js b/client/.nuxt/components/plugin.js index f9823b55..1f66578c 100644 --- a/client/.nuxt/components/plugin.js +++ b/client/.nuxt/components/plugin.js @@ -9,6 +9,8 @@ const components = { AppStreamContainer: () => import('../..\\components\\app\\StreamContainer.vue' /* webpackChunkName: "components/app-stream-container" */).then(c => wrapFunctional(c.default || c)), CardsBookCard: () => import('../..\\components\\cards\\BookCard.vue' /* webpackChunkName: "components/cards-book-card" */).then(c => wrapFunctional(c.default || c)), CardsBookCover: () => import('../..\\components\\cards\\BookCover.vue' /* webpackChunkName: "components/cards-book-cover" */).then(c => wrapFunctional(c.default || c)), + ControlsFilterSelect: () => import('../..\\components\\controls\\FilterSelect.vue' /* webpackChunkName: "components/controls-filter-select" */).then(c => wrapFunctional(c.default || c)), + ControlsOrderSelect: () => import('../..\\components\\controls\\OrderSelect.vue' /* webpackChunkName: "components/controls-order-select" */).then(c => wrapFunctional(c.default || c)), ControlsVolumeControl: () => import('../..\\components\\controls\\VolumeControl.vue' /* webpackChunkName: "components/controls-volume-control" */).then(c => wrapFunctional(c.default || c)), ModalsEditModal: () => import('../..\\components\\modals\\EditModal.vue' /* webpackChunkName: "components/modals-edit-modal" */).then(c => wrapFunctional(c.default || c)), ModalsModal: () => import('../..\\components\\modals\\Modal.vue' /* webpackChunkName: "components/modals-modal" */).then(c => wrapFunctional(c.default || c)), diff --git a/client/.nuxt/components/readme.md b/client/.nuxt/components/readme.md index 7095248b..242bf4f0 100644 --- a/client/.nuxt/components/readme.md +++ b/client/.nuxt/components/readme.md @@ -13,6 +13,8 @@ You can directly use them in pages and other components without the need to impo - `` | `` (components/app/StreamContainer.vue) - `` | `` (components/cards/BookCard.vue) - `` | `` (components/cards/BookCover.vue) +- `` | `` (components/controls/FilterSelect.vue) +- `` | `` (components/controls/OrderSelect.vue) - `` | `` (components/controls/VolumeControl.vue) - `` | `` (components/modals/EditModal.vue) - `` | `` (components/modals/Modal.vue) diff --git a/client/.nuxt/vetur/tags.json b/client/.nuxt/vetur/tags.json index 37a9c618..cd181bc1 100644 --- a/client/.nuxt/vetur/tags.json +++ b/client/.nuxt/vetur/tags.json @@ -20,6 +20,12 @@ "CardsBookCover": { "description": "Auto imported from components/cards/BookCover.vue" }, + "ControlsFilterSelect": { + "description": "Auto imported from components/controls/FilterSelect.vue" + }, + "ControlsOrderSelect": { + "description": "Auto imported from components/controls/OrderSelect.vue" + }, "ControlsVolumeControl": { "description": "Auto imported from components/controls/VolumeControl.vue" }, diff --git a/client/assets/fastSort.js b/client/assets/fastSort.js new file mode 100644 index 00000000..f8337da4 --- /dev/null +++ b/client/assets/fastSort.js @@ -0,0 +1,158 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['fast-sort'] = {})); +}(this, (function (exports) { + 'use strict'; + + // >>> INTERFACES <<< + // >>> HELPERS <<< + var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; }; + var throwInvalidConfigErrorIfTrue = function (condition, context) { + if (condition) + throw Error("Invalid sort config: " + context); + }; + var unpackObjectSorter = function (sortByObj) { + var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc; + var order = asc ? 1 : -1; + var sortBy = (asc || desc); + // Validate object config + throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property'); + throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties'); + var comparer = sortByObj.comparer && castComparer(sortByObj.comparer); + return { order: order, sortBy: sortBy, comparer: comparer }; + }; + // >>> SORTERS <<< + var multiPropertySorterProvider = function (defaultComparer) { + return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) { + var valA; + var valB; + if (typeof sortBy === 'string') { + valA = a[sortBy]; + valB = b[sortBy]; + } + else if (typeof sortBy === 'function') { + valA = sortBy(a); + valB = sortBy(b); + } + else { + var objectSorterConfig = unpackObjectSorter(sortBy); + return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b); + } + var equality = comparer(valA, valB, order); + if ((equality === 0 || (valA == null && valB == null)) && + sortByArr.length > depth) { + return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b); + } + return equality; + }; + }; + function getSortStrategy(sortBy, comparer, order) { + // Flat array sorter + if (sortBy === undefined || sortBy === true) { + return function (a, b) { return comparer(a, b, order); }; + } + // Sort list of objects by single object key + if (typeof sortBy === 'string') { + throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.'); + return function (a, b) { return comparer(a[sortBy], b[sortBy], order); }; + } + // Sort list of objects by single function sorter + if (typeof sortBy === 'function') { + return function (a, b) { return comparer(sortBy(a), sortBy(b), order); }; + } + // Sort by multiple properties + if (Array.isArray(sortBy)) { + var multiPropSorter_1 = multiPropertySorterProvider(comparer); + return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); }; + } + // Unpack object config to get actual sorter strategy + var objectSorterConfig = unpackObjectSorter(sortBy); + return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order); + } + var sortArray = function (order, ctx, sortBy, comparer) { + var _a; + if (!Array.isArray(ctx)) { + return ctx; + } + // Unwrap sortBy if array with only 1 value to get faster sort strategy + if (Array.isArray(sortBy) && sortBy.length < 2) { + _a = sortBy, sortBy = _a[0]; + } + return ctx.sort(getSortStrategy(sortBy, comparer, order)); + }; + // >>> Public <<< + var createNewSortInstance = function (opts) { + var comparer = castComparer(opts.comparer); + return function (_ctx) { + var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting + ? _ctx.slice() + : _ctx; + return { + /** + * Sort array in ascending order. + * @example + * sort([3, 1, 4]).asc(); + * sort(users).asc(u => u.firstName); + * sort(users).asc([ + * U => u.firstName + * u => u.lastName, + * ]); + */ + asc: function (sortBy) { + return sortArray(1, ctx, sortBy, comparer); + }, + /** + * Sort array in descending order. + * @example + * sort([3, 1, 4]).desc(); + * sort(users).desc(u => u.firstName); + * sort(users).desc([ + * U => u.firstName + * u => u.lastName, + * ]); + */ + desc: function (sortBy) { + return sortArray(-1, ctx, sortBy, comparer); + }, + /** + * Sort array in ascending or descending order. It allows sorting on multiple props + * in different order for each of them. + * @example + * sort(users).by([ + * { asc: u => u.score } + * { desc: u => u.age } + * ]); + */ + by: function (sortBy) { + return sortArray(1, ctx, sortBy, comparer); + }, + }; + }; + }; + var defaultComparer = function (a, b, order) { + if (a == null) + return order; + if (b == null) + return -order; + if (a < b) + return -1; + if (a === b) + return 0; + return 1; + }; + var sort = createNewSortInstance({ + comparer: defaultComparer, + }); + var inPlaceSort = createNewSortInstance({ + comparer: defaultComparer, + inPlaceSorting: true, + }); + + exports.createNewSortInstance = createNewSortInstance; + exports.inPlaceSort = inPlaceSort; + exports.sort = sort; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); \ No newline at end of file diff --git a/client/components/app/BookShelf.vue b/client/components/app/BookShelf.vue index d2ab1b1b..6fe032d6 100644 --- a/client/components/app/BookShelf.vue +++ b/client/components/app/BookShelf.vue @@ -20,13 +20,17 @@ diff --git a/client/components/controls/FilterSelect.vue b/client/components/controls/FilterSelect.vue new file mode 100644 index 00000000..81b9c5b4 --- /dev/null +++ b/client/components/controls/FilterSelect.vue @@ -0,0 +1,79 @@ + + + \ No newline at end of file diff --git a/client/components/controls/OrderSelect.vue b/client/components/controls/OrderSelect.vue new file mode 100644 index 00000000..ddc6d2d2 --- /dev/null +++ b/client/components/controls/OrderSelect.vue @@ -0,0 +1,101 @@ + + + \ No newline at end of file diff --git a/client/package.json b/client/package.json index bfc19e1b..c73940c9 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "0.9.4", + "version": "0.9.5", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js index e27b88df..11cdccc6 100644 --- a/client/store/audiobooks.js +++ b/client/store/audiobooks.js @@ -1,3 +1,4 @@ +import { sort } from '@/assets/fastSort' export const state = () => ({ audiobooks: [], @@ -5,7 +6,21 @@ export const state = () => ({ }) export const getters = { + getFiltered: (state, getters, rootState) => () => { + var filtered = state.audiobooks + // TODO: Add filters + return filtered + }, + getFilteredAndSorted: (state, getters, rootState) => () => { + var settings = rootState.settings.settings + var direction = settings.orderDesc ? 'desc' : 'asc' + var filtered = getters.getFiltered() + return sort(filtered)[direction]((ab) => { + // Supports dot notation strings i.e. "book.title" + return settings.orderBy.split('.').reduce((a, b) => a[b], ab) + }) + } } export const actions = { @@ -19,7 +34,8 @@ export const actions = { console.error('Failed', error) commit('set', []) }) - } + }, + } export const mutations = { diff --git a/client/store/settings.js b/client/store/settings.js index 72ce11ca..d27f739c 100644 --- a/client/store/settings.js +++ b/client/store/settings.js @@ -1,11 +1,18 @@ export const state = () => ({ - orderBy: 'title', - orderDesc: false + settings: { + orderBy: 'book.title', + orderDesc: false, + filterBy: 'all' + }, + + listeners: [] }) export const getters = { - + getFilterOrderKey: (state) => { + return Object.values(state.settings).join('-') + } } export const actions = { @@ -14,7 +21,19 @@ export const actions = { export const mutations = { setSettings(state, settings) { - state.orderBy = settings.orderBy - state.orderDesc = settings.orderDesc + state.settings = { + ...settings + } + state.listeners.forEach((listener) => { + listener.meth() + }) + }, + addListener(state, listener) { + var index = state.listeners.findIndex(l => l.id === listener.id) + if (index >= 0) state.listeners.splice(index, 1, listener) + else state.listeners.push(listener) + }, + removeListener(state, listenerId) { + state.listeners = state.listeners.filter(l => l.id !== listenerId) } } \ No newline at end of file diff --git a/client/tailwind.config.js b/client/tailwind.config.js index b9a0634b..570f53ae 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -11,6 +11,9 @@ module.exports = { darkMode: false, theme: { extend: { + height: { + '7.5': '1.75rem' + }, colors: { bg: '#373838', primary: '#262626', diff --git a/package.json b/package.json index d93181da..9ac39b42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "0.9.4", + "version": "0.9.5", "description": "", "main": "index.js", "scripts": {