mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 10:27:01 -04:00 
			
		
		
		
	Added sorting by sequence for series and collapsing series in series view
This commit is contained in:
		
							parent
							
								
									c1035d97e8
								
							
						
					
					
						commit
						b1111912f7
					
				| @ -28,13 +28,19 @@ | |||||||
|             <span class="font-mono">{{ numShowing }}</span> |             <span class="font-mono">{{ numShowing }}</span> | ||||||
|           </div> |           </div> | ||||||
|           <div class="flex-grow" /> |           <div class="flex-grow" /> | ||||||
|           <ui-btn color="primary" small :loading="processingSeries" class="flex items-center" @click="markSeriesFinished"> |           <ui-checkbox v-model="settings.collapseBookSeries" label="Collapse Series" checkbox-bg="bg" | ||||||
|  |             check-color="white" small class="mr-2" @input="updateCollapseBookSeries" /> | ||||||
|  |           <ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4" | ||||||
|  |             @click="markSeriesFinished"> | ||||||
|             <div class="h-5 w-5"> |             <div class="h-5 w-5"> | ||||||
|               <svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)"> |               <svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" | ||||||
|                 <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> |                 fill="rgb(63, 181, 68)"> | ||||||
|  |                 <path | ||||||
|  |                   d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> | ||||||
|               </svg> |               </svg> | ||||||
|               <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> |               <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> | ||||||
|                 <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" /> |                 <path | ||||||
|  |                   d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" /> | ||||||
|               </svg> |               </svg> | ||||||
|             </div> |             </div> | ||||||
|             <span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span> |             <span class="pl-2"> Mark Series {{ isSeriesFinished ? 'Not Finished' : 'Finished' }}</span> | ||||||
| @ -280,6 +286,9 @@ export default { | |||||||
|     updateCollapseSeries() { |     updateCollapseSeries() { | ||||||
|       this.saveSettings() |       this.saveSettings() | ||||||
|     }, |     }, | ||||||
|  |     updateCollapseBookSeries() { | ||||||
|  |       this.saveSettings() | ||||||
|  |     }, | ||||||
|     saveSettings() { |     saveSettings() { | ||||||
|       this.$store.dispatch('user/updateUserSettings', this.settings) |       this.$store.dispatch('user/updateUserSettings', this.settings) | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -122,6 +122,9 @@ export default { | |||||||
|     collapseSeries() { |     collapseSeries() { | ||||||
|       return this.$store.getters['user/getUserSetting']('collapseSeries') |       return this.$store.getters['user/getUserSetting']('collapseSeries') | ||||||
|     }, |     }, | ||||||
|  |     collapseBookSeries() { | ||||||
|  |       return this.$store.getters['user/getUserSetting']('collapseBookSeries') | ||||||
|  |     }, | ||||||
|     coverAspectRatio() { |     coverAspectRatio() { | ||||||
|       return this.$store.getters['libraries/getBookCoverAspectRatio'] |       return this.$store.getters['libraries/getBookCoverAspectRatio'] | ||||||
|     }, |     }, | ||||||
| @ -452,6 +455,9 @@ export default { | |||||||
|         searchParams.set('filter', this.seriesFilterBy) |         searchParams.set('filter', this.seriesFilterBy) | ||||||
|       } else if (this.page === 'series-books') { |       } else if (this.page === 'series-books') { | ||||||
|         searchParams.set('filter', `series.${this.$encode(this.seriesId)}`) |         searchParams.set('filter', `series.${this.$encode(this.seriesId)}`) | ||||||
|  |         if (this.collapseBookSeries) { | ||||||
|  |           searchParams.set('collapseseries', 1) | ||||||
|  |         } | ||||||
|       } else { |       } else { | ||||||
|         if (this.filterBy && this.filterBy !== 'all') { |         if (this.filterBy && this.filterBy !== 'all') { | ||||||
|           searchParams.set('filter', this.filterBy) |           searchParams.set('filter', this.filterBy) | ||||||
| @ -467,8 +473,6 @@ export default { | |||||||
|       return searchParams.toString() |       return searchParams.toString() | ||||||
|     }, |     }, | ||||||
|     checkUpdateSearchParams() { |     checkUpdateSearchParams() { | ||||||
|       if (this.page === 'series-books') return false |  | ||||||
| 
 |  | ||||||
|       var newSearchParams = this.buildSearchParams() |       var newSearchParams = this.buildSearchParams() | ||||||
|       var currentQueryString = window.location.search |       var currentQueryString = window.location.search | ||||||
|       if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1) |       if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1) | ||||||
|  | |||||||
| @ -22,12 +22,16 @@ | |||||||
|       }}</p> |       }}</p> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div v-if="seriesSequenceList" |     <div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" | ||||||
|       class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center bg-black bg-opacity-90 box-shadow-md" |       :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" | ||||||
|       style="background-color: #78350f">#{{ seriesSequenceList }}</div> |       style="background-color: #78350f"> | ||||||
|     <div v-else-if="booksInSeries" |       <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p> | ||||||
|       class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" |     </div> | ||||||
|       style="background-color: #cd9d49dd">{{ booksInSeries }}</div> |     <div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" | ||||||
|  |       :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" | ||||||
|  |       style="background-color: #cd9d49dd"> | ||||||
|  |       <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p> | ||||||
|  |     </div> | ||||||
| 
 | 
 | ||||||
|     <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> |     <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> | ||||||
|       <div v-show="libraryItem && !imageReady" |       <div v-show="libraryItem && !imageReady" | ||||||
|  | |||||||
| @ -1,19 +1,27 @@ | |||||||
| <template> | <template> | ||||||
|   <div ref="wrapper" class="relative" v-click-outside="clickOutside"> |   <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-400 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"> |     <button type="button" | ||||||
|  |       class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 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="flex items-center justify-between"> | ||||||
|         <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> |         <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> | ||||||
|         <span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> |         <span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> | ||||||
|       </span> |       </span> | ||||||
|     </button> |     </button> | ||||||
| 
 | 
 | ||||||
|     <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> |     <ul v-show="showMenu" | ||||||
|  |       class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" | ||||||
|  |       role="listbox" aria-labelledby="listbox-label"> | ||||||
|       <template v-for="item in selectItems"> |       <template v-for="item in selectItems"> | ||||||
|         <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)"> |         <li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" | ||||||
|  |           :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" | ||||||
|  |           @click="clickedOption(item.value)"> | ||||||
|           <div class="flex items-center"> |           <div class="flex items-center"> | ||||||
|             <span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span> |             <span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span> | ||||||
|           </div> |           </div> | ||||||
|           <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> |           <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 class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> | ||||||
|           </span> |           </span> | ||||||
|         </li> |         </li> | ||||||
| @ -29,46 +37,54 @@ export default { | |||||||
|     descending: Boolean |     descending: Boolean | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|  |     const bookItems = [ | ||||||
|  |       { | ||||||
|  |         text: 'Title', | ||||||
|  |         value: 'media.metadata.title' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Author (First Last)', | ||||||
|  |         value: 'media.metadata.authorName' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Author (Last, First)', | ||||||
|  |         value: 'media.metadata.authorNameLF' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Published Year', | ||||||
|  |         value: 'media.metadata.publishedYear' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Added At', | ||||||
|  |         value: 'addedAt' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Size', | ||||||
|  |         value: 'size' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'Duration', | ||||||
|  |         value: 'media.duration' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'File Birthtime', | ||||||
|  |         value: 'birthtimeMs' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         text: 'File Modified', | ||||||
|  |         value: 'mtimeMs' | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     const seriesItems = [...bookItems, { | ||||||
|  |       text: 'Sequence', | ||||||
|  |       value: 'sequence' | ||||||
|  |     }] | ||||||
|  | 
 | ||||||
|     return { |     return { | ||||||
|       showMenu: false, |       showMenu: false, | ||||||
|       bookItems: [ |       bookItems: bookItems, | ||||||
|         { |       seriesItems: seriesItems, | ||||||
|           text: 'Title', |  | ||||||
|           value: 'media.metadata.title' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Author (First Last)', |  | ||||||
|           value: 'media.metadata.authorName' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Author (Last, First)', |  | ||||||
|           value: 'media.metadata.authorNameLF' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Published Year', |  | ||||||
|           value: 'media.metadata.publishedYear' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Added At', |  | ||||||
|           value: 'addedAt' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Size', |  | ||||||
|           value: 'size' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'Duration', |  | ||||||
|           value: 'media.duration' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'File Birthtime', |  | ||||||
|           value: 'birthtimeMs' |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|           text: 'File Modified', |  | ||||||
|           value: 'mtimeMs' |  | ||||||
|         } |  | ||||||
|       ], |  | ||||||
|       podcastItems: [ |       podcastItems: [ | ||||||
|         { |         { | ||||||
|           text: 'Title', |           text: 'Title', | ||||||
| @ -122,8 +138,21 @@ export default { | |||||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' |       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||||
|     }, |     }, | ||||||
|     selectItems() { |     selectItems() { | ||||||
|       if (this.isPodcast) return this.podcastItems |       let items = null | ||||||
|       return this.bookItems |       if (this.isPodcast) { | ||||||
|  |         items = this.podcastItems | ||||||
|  |       } else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) { | ||||||
|  |         items = this.seriesItems | ||||||
|  |       } else { | ||||||
|  |         items = this.bookItems | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (!items.some(i => i.value === this.selected)) { | ||||||
|  |         this.selected = items[0].value | ||||||
|  |         this.selectedDesc = !this.defaultsToAsc(items[0].value) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return items | ||||||
|     }, |     }, | ||||||
|     selectedText() { |     selectedText() { | ||||||
|       var _selected = this.selected |       var _selected = this.selected | ||||||
| @ -143,12 +172,19 @@ export default { | |||||||
|         this.selectedDesc = !this.selectedDesc |         this.selectedDesc = !this.selectedDesc | ||||||
|       } else { |       } else { | ||||||
|         this.selected = val |         this.selected = val | ||||||
|         if (val == 'media.metadata.title' || val == 'media.metadata.author' || val == 'media.metadata.authorName' || val == 'media.metadata.authorNameLF') { |         if (this.defaultsToAsc(val)) this.selectedDesc = false | ||||||
|           this.selectedDesc = false |  | ||||||
|         } |  | ||||||
|       } |       } | ||||||
|       this.showMenu = false |       this.showMenu = false | ||||||
|       this.$nextTick(() => this.$emit('change', val)) |       this.$nextTick(() => this.$emit('change', val)) | ||||||
|  |     }, | ||||||
|  |     defaultsToAsc(val) { | ||||||
|  |       return ( | ||||||
|  |         val == 'media.metadata.title' || | ||||||
|  |         val == 'media.metadata.author' || | ||||||
|  |         val == 'media.metadata.authorName' || | ||||||
|  |         val == 'media.metadata.authorNameLF' || | ||||||
|  |         val == 'sequence' | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -181,7 +181,7 @@ class LibraryController { | |||||||
| 
 | 
 | ||||||
|       if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { |       if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { | ||||||
|         libraryItems = collapsedItems |         libraryItems = collapsedItems | ||||||
|         payload.total = collapsedItems.length |         payload.total = libraryItems.length | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -207,8 +207,10 @@ class LibraryController { | |||||||
|         sortKey += 'IgnorePrefix' |         sortKey += 'IgnorePrefix' | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // If series are collapsed and not sorting by title, sort all collapsed series to the end in alphabetical order
 |       // If series are collapsed and not sorting by title or sequence, 
 | ||||||
|       if (payload.collapseseries && !sortByTitle) { |       // sort all collapsed series to the end in alphabetical order
 | ||||||
|  |       const sortBySequence = filterSeries && (sortKey === 'sequence') | ||||||
|  |       if (payload.collapseseries && !(sortByTitle || sortBySequence)) { | ||||||
|         sortArray.push({ |         sortArray.push({ | ||||||
|           asc: (li) => { |           asc: (li) => { | ||||||
|             if (li.collapsedSeries) { |             if (li.collapsedSeries) { | ||||||
| @ -222,10 +224,13 @@ class LibraryController { | |||||||
|         }) |         }) | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // Sort series based on the sortBy attribute
 | ||||||
|       var direction = payload.sortDesc ? 'desc' : 'asc' |       var direction = payload.sortDesc ? 'desc' : 'asc' | ||||||
|       sortArray.push({ |       sortArray.push({ | ||||||
|         [direction]: (li) => { |         [direction]: (li) => { | ||||||
|           if (mediaIsBook && sortByTitle && li.collapsedSeries) { |           if (mediaIsBook && sortBySequence) { | ||||||
|  |             return li.media.metadata.getSeries(filterSeries).sequence | ||||||
|  |           } else if (mediaIsBook && sortByTitle && li.collapsedSeries) { | ||||||
|             return this.db.serverSettings.sortingIgnorePrefix ? |             return this.db.serverSettings.sortingIgnorePrefix ? | ||||||
|               li.collapsedSeries.nameIgnorePrefix : |               li.collapsedSeries.nameIgnorePrefix : | ||||||
|               li.collapsedSeries.name |               li.collapsedSeries.name | ||||||
| @ -269,24 +274,24 @@ class LibraryController { | |||||||
|         // If collapsing by series and filtering by a series, generate the list of sequences the collapsed
 |         // If collapsing by series and filtering by a series, generate the list of sequences the collapsed
 | ||||||
|         // series represents in the filtered series
 |         // series represents in the filtered series
 | ||||||
|         if (filterSeries) { |         if (filterSeries) { | ||||||
|           json.collapsedSeries.seriesSequenceList = li.collapsedSeries.books |           json.collapsedSeries.seriesSequenceList = | ||||||
|             .map(b => parseFloat(b.filterSeriesSequence)) |             naturalSort(li.collapsedSeries.books.map(b => b.filterSeriesSequence)).asc() | ||||||
|             .filter(s => s) |               .reduce((ranges, currentSequence) => { | ||||||
|             .sort((a, b) => a - b) |                 let lastRange = ranges.at(-1) | ||||||
|             .reduce((ranges, currentSequence) => { |                 let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence) | ||||||
|               let lastRange = ranges.at(-1) |                 if (isNumber) currentSequence = parseFloat(currentSequence) | ||||||
| 
 | 
 | ||||||
|               if (lastRange && ((lastRange.end + 1) == currentSequence)) { |                 if (lastRange && isNumber && lastRange.isNumber && ((lastRange.end + 1) == currentSequence)) { | ||||||
|                 lastRange.end = currentSequence |                   lastRange.end = currentSequence | ||||||
|               } |                 } | ||||||
|               else { |                 else { | ||||||
|                 ranges.push({ start: currentSequence, end: currentSequence }) |                   ranges.push({ start: currentSequence, end: currentSequence, isNumber: isNaN(currentSequence) }) | ||||||
|               } |                 } | ||||||
| 
 | 
 | ||||||
|               return ranges |                 return ranges | ||||||
|             }, []) |               }, []) | ||||||
|             .map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`) |               .map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`) | ||||||
|             .join(', ') |               .join(', ') | ||||||
|         } |         } | ||||||
|       } else if (filterSeries) { |       } else if (filterSeries) { | ||||||
|         // If filtering by series, make sure to include the series metadata
 |         // If filtering by series, make sure to include the series metadata
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user