mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Sorting and filtering
This commit is contained in:
		
							parent
							
								
									7e482352b1
								
							
						
					
					
						commit
						1c88c0a796
					
				| @ -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)) | ||||
|  | ||||
| @ -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)), | ||||
|  | ||||
| @ -13,6 +13,8 @@ You can directly use them in pages and other components without the need to impo | ||||
| - `<AppStreamContainer>` | `<app-stream-container>` (components/app/StreamContainer.vue) | ||||
| - `<CardsBookCard>` | `<cards-book-card>` (components/cards/BookCard.vue) | ||||
| - `<CardsBookCover>` | `<cards-book-cover>` (components/cards/BookCover.vue) | ||||
| - `<ControlsFilterSelect>` | `<controls-filter-select>` (components/controls/FilterSelect.vue) | ||||
| - `<ControlsOrderSelect>` | `<controls-order-select>` (components/controls/OrderSelect.vue) | ||||
| - `<ControlsVolumeControl>` | `<controls-volume-control>` (components/controls/VolumeControl.vue) | ||||
| - `<ModalsEditModal>` | `<modals-edit-modal>` (components/modals/EditModal.vue) | ||||
| - `<ModalsModal>` | `<modals-modal>` (components/modals/Modal.vue) | ||||
|  | ||||
| @ -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" | ||||
|   }, | ||||
|  | ||||
							
								
								
									
										158
									
								
								client/assets/fastSort.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								client/assets/fastSort.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 }); | ||||
| 
 | ||||
| }))); | ||||
| @ -20,13 +20,17 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { sort } from '@/assets/fastSort' | ||||
| console.log('SORT', sort) | ||||
| 
 | ||||
| export default { | ||||
|   data() { | ||||
|     return { | ||||
|       width: 0, | ||||
|       bookWidth: 176, | ||||
|       booksPerRow: 0, | ||||
|       groupedBooks: [] | ||||
|       groupedBooks: [], | ||||
|       currFilterOrderKey: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -36,34 +40,19 @@ export default { | ||||
|     audiobooks() { | ||||
|       return this.$store.state.audiobooks.audiobooks | ||||
|     }, | ||||
|     orderBy() { | ||||
|       return this.$store.state.settings.orderBy | ||||
|     }, | ||||
|     orderDesc() { | ||||
|       return this.$store.state.settings.orderDesc | ||||
|     filterOrderKey() { | ||||
|       return this.$store.getters['settings/getFilterOrderKey'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     sortAudiobooks() { | ||||
|       var audiobooks = this.audiobooks.map((ab) => ({ ...ab })) | ||||
|       audiobooks.sort((a, b) => { | ||||
|         var bookA = a.book || {} | ||||
|         var bookB = b.book || {} | ||||
|         if (this.orderDesc) { | ||||
|           return bookB[this.orderBy] > bookA[this.orderBy] ? 1 : -1 | ||||
|         } else { | ||||
|           // ASCENDING A -> Z | ||||
|           return bookA[this.orderBy] > bookB[this.orderBy] ? 1 : -1 | ||||
|         } | ||||
|       }) | ||||
|       return audiobooks | ||||
|     }, | ||||
|     setGroupedBooks() { | ||||
|       var groups = [] | ||||
|       var currentRow = 0 | ||||
|       var currentGroup = [] | ||||
|       var audiobooksSorted = this.sortAudiobooks() | ||||
|       console.log('AB SORTED', audiobooksSorted) | ||||
| 
 | ||||
|       var audiobooksSorted = this.$store.getters['audiobooks/getFilteredAndSorted']() | ||||
|       this.currFilterOrderKey = this.filterOrderKey | ||||
| 
 | ||||
|       for (let i = 0; i < audiobooksSorted.length; i++) { | ||||
|         var row = Math.floor(i / this.booksPerRow) | ||||
|         if (row > currentRow) { | ||||
| @ -102,18 +91,27 @@ export default { | ||||
|       console.log('[AudioBookshelf] Audiobooks Updated') | ||||
|       this.setGroupedBooks() | ||||
|     }, | ||||
|     settingsUpdated() { | ||||
|       // var newSortKey = `${this.orderBy}-${this.orderDesc}` | ||||
|       if (this.currFilterOrderKey !== this.filterOrderKey) { | ||||
|         this.setGroupedBooks() | ||||
|       } | ||||
|     }, | ||||
|     scan() { | ||||
|       this.$root.socket.emit('scan') | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated }) | ||||
|     this.$store.commit('settings/addListener', { id: 'bookshelf', meth: this.settingsUpdated }) | ||||
| 
 | ||||
|     this.$store.dispatch('audiobooks/load') | ||||
|     this.init() | ||||
|     window.addEventListener('resize', this.resize) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     this.$store.commit('audiobooks/removeListener', 'bookshelf') | ||||
|     this.$store.commit('settings/removeListener', 'bookshelf') | ||||
|     window.removeEventListener('resize', this.resize) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,13 @@ | ||||
| <template> | ||||
|   <div class="w-full h-10 relative"> | ||||
|     <div id="toolbar" class="absolute top-0 left-0 w-full h-full z-10 flex items-center px-8"> | ||||
|       <p>Order By: {{ orderBy }}</p> | ||||
|       <p class="px-4">Desc: {{ orderDesc ? 'Desc' : 'Asc' }}</p> | ||||
|       <!-- <p>Order By: {{ settings.orderBy }}</p> | ||||
|       <p class="px-4">Desc: {{ settings.orderDesc ? 'Desc' : 'Asc' }}</p> --> | ||||
|       <p class="font-book">{{ numShowing }} Audiobooks</p> | ||||
|       <div class="flex-grow" /> | ||||
|       <controls-filter-select v-model="settings.filterBy" class="w-28 h-7.5" @change="updateFilter" /> | ||||
|       <span class="px-4 text-sm">by</span> | ||||
|       <controls-order-select v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-40 h-7.5" @change="updateOrder" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @ -10,18 +15,33 @@ | ||||
| <script> | ||||
| export default { | ||||
|   data() { | ||||
|     return {} | ||||
|   }, | ||||
|   computed: { | ||||
|     orderBy() { | ||||
|       return this.$store.state.settings.orderBy | ||||
|     }, | ||||
|     orderDesc() { | ||||
|       return this.$store.state.settings.orderDesc | ||||
|     return { | ||||
|       settings: {} | ||||
|     } | ||||
|   }, | ||||
|   methods: {}, | ||||
|   mounted() {} | ||||
|   computed: { | ||||
|     numShowing() { | ||||
|       return this.$store.getters['audiobooks/getFiltered']().length | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     updateOrder() { | ||||
|       this.saveSettings() | ||||
|     }, | ||||
|     updateFilter() { | ||||
|       this.saveSettings() | ||||
|     }, | ||||
|     saveSettings() { | ||||
|       // Send to server | ||||
|       this.$store.commit('settings/setSettings', this.settings) | ||||
|     }, | ||||
|     init() { | ||||
|       this.settings = { ...this.$store.state.settings.settings } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										79
									
								
								client/components/controls/FilterSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								client/components/controls/FilterSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" class="relative" v-click-outside="clickOutside"> | ||||
|     <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> | ||||
|       <span class="flex items-center justify-between"> | ||||
|         <span class="block truncate" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> | ||||
|       </span> | ||||
|       <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> | ||||
|         <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> | ||||
|           <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> | ||||
|         </svg> | ||||
|       </span> | ||||
|     </button> | ||||
| 
 | ||||
|     <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> | ||||
|       <template v-for="item in items"> | ||||
|         <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)"> | ||||
|           <div class="flex items-center"> | ||||
|             <span class="font-normal ml-3 block truncate">{{ item.text }}</span> | ||||
|           </div> | ||||
|         </li> | ||||
|       </template> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: String | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showMenu: false, | ||||
|       items: [ | ||||
|         { | ||||
|           text: 'All', | ||||
|           value: 'all' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Genre', | ||||
|           value: 'genre' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Tag', | ||||
|           value: 'tag' | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     selected: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     selectedText() { | ||||
|       var _sel = this.items.find((i) => i.value === this.selected) | ||||
|       if (!_sel) return '' | ||||
|       return _sel.text | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickOutside() { | ||||
|       this.showMenu = false | ||||
|     }, | ||||
|     clickedOption(val) { | ||||
|       if (this.selected === val) { | ||||
|         return | ||||
|       } | ||||
|       this.selected = val | ||||
|       this.showMenu = false | ||||
|       this.$nextTick(() => this.$emit('change', val)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										101
									
								
								client/components/controls/OrderSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								client/components/controls/OrderSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| <template> | ||||
|   <div ref="wrapper" class="relative" v-click-outside="clickOutside"> | ||||
|     <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> | ||||
|       <span class="flex items-center justify-between"> | ||||
|         <span class="block truncate" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> | ||||
|         <span class="material-icons text-xl text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> | ||||
|       </span> | ||||
|       <!-- <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> | ||||
|         <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> | ||||
|           <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> | ||||
|         </svg> | ||||
|       </span> --> | ||||
|     </button> | ||||
| 
 | ||||
|     <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-labelledby="listbox-label" aria-activedescendant="listbox-option-3"> | ||||
|       <template v-for="item in items"> | ||||
|         <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)"> | ||||
|           <div class="flex items-center"> | ||||
|             <span class="font-normal ml-3 block truncate">{{ item.text }}</span> | ||||
|           </div> | ||||
|           <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> | ||||
|             <span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> | ||||
|           </span> | ||||
|         </li> | ||||
|       </template> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     value: String, | ||||
|     descending: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       showMenu: false, | ||||
|       items: [ | ||||
|         { | ||||
|           text: 'Title', | ||||
|           value: 'book.title' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Author', | ||||
|           value: 'book.author' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Added At', | ||||
|           value: 'addedAt' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Duration', | ||||
|           value: 'duration' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Size', | ||||
|           value: 'size' | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     selected: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     selectedDesc: { | ||||
|       get() { | ||||
|         return this.descending | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('update:descending', val) | ||||
|       } | ||||
|     }, | ||||
|     selectedText() { | ||||
|       var _sel = this.items.find((i) => i.value === this.selected) | ||||
|       if (!_sel) return '' | ||||
|       return _sel.text | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     clickOutside() { | ||||
|       this.showMenu = false | ||||
|     }, | ||||
|     clickedOption(val) { | ||||
|       if (this.selected === val) { | ||||
|         this.selectedDesc = !this.selectedDesc | ||||
|       } else { | ||||
|         this.selected = val | ||||
|       } | ||||
|       this.showMenu = false | ||||
|       this.$nextTick(() => this.$emit('change', val)) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "0.9.4", | ||||
|   "version": "0.9.5", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -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 = { | ||||
|  | ||||
| @ -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) | ||||
|   } | ||||
| } | ||||
| @ -11,6 +11,9 @@ module.exports = { | ||||
|   darkMode: false, | ||||
|   theme: { | ||||
|     extend: { | ||||
|       height: { | ||||
|         '7.5': '1.75rem' | ||||
|       }, | ||||
|       colors: { | ||||
|         bg: '#373838', | ||||
|         primary: '#262626', | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "0.9.4", | ||||
|   "version": "0.9.5", | ||||
|   "description": "", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user