mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-01 19:07:02 -04:00 
			
		
		
		
	Add podcast episode sorting and saving sort order
This commit is contained in:
		
							parent
							
								
									8ce9b55969
								
							
						
					
					
						commit
						1152e5513e
					
				
							
								
								
									
										94
									
								
								client/components/controls/EpisodeSortSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								client/components/controls/EpisodeSortSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | ||||
| <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-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="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> | ||||
|     </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"> | ||||
|       <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" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)"> | ||||
|           <div class="flex items-center"> | ||||
|             <span class="font-normal ml-3 block truncate text-xs">{{ 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: 'Current', | ||||
|           value: 'index' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Title', | ||||
|           value: 'title' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Episode', | ||||
|           value: 'episode' | ||||
|         }, | ||||
|         { | ||||
|           text: 'Pub Date', | ||||
|           value: 'pubDate' | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   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 _selected = this.selected | ||||
|       if (!_selected) return '' | ||||
|       var _sel = this.items.find((i) => i.value === _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,7 +1,7 @@ | ||||
| <template> | ||||
|   <div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||
|     <div v-if="episode" class="flex items-center h-24"> | ||||
|       <div class="w-12 min-w-12 max-w-16 h-full"> | ||||
|       <div v-show="userCanUpdate" class="w-12 min-w-12 max-w-16 h-full"> | ||||
|         <div class="flex h-full items-center justify-center"> | ||||
|           <span class="material-icons drag-handle text-lg text-white text-opacity-50 hover:text-opacity-100">menu</span> | ||||
|         </div> | ||||
|  | ||||
| @ -1,6 +1,13 @@ | ||||
| <template> | ||||
|   <div class="w-full py-6"> | ||||
|     <p class="text-lg mb-0 font-semibold">Episodes</p> | ||||
|     <div class="flex items-center mb-4"> | ||||
|       <p class="text-lg mb-0 font-semibold">Episodes</p> | ||||
|       <div class="flex-grow" /> | ||||
|       <controls-episode-sort-select v-model="sortKey" :descending.sync="sortDesc" class="w-36 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="changeSort" /> | ||||
|       <div v-if="userCanUpdate" class="w-12"> | ||||
|         <ui-icon-btn v-if="orderChanged" :loading="savingOrder" icon="save" bg-color="primary" class="ml-auto" @click="saveOrder" /> | ||||
|       </div> | ||||
|     </div> | ||||
|     <p v-if="!episodes.length" class="py-4 text-center text-lg">No Episodes</p> | ||||
|     <draggable v-model="episodesCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate"> | ||||
|       <transition-group type="transition" :name="!drag ? 'episode' : null"> | ||||
| @ -29,15 +36,14 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       sortKey: 'index', | ||||
|       sortDesc: true, | ||||
|       drag: false, | ||||
|       dragOptions: { | ||||
|         animation: 200, | ||||
|         group: 'description', | ||||
|         ghostClass: 'ghost' | ||||
|       }, | ||||
|       episodesCopy: [], | ||||
|       selectedEpisode: null, | ||||
|       showEditEpisodeModal: false | ||||
|       showEditEpisodeModal: false, | ||||
|       orderChanged: false, | ||||
|       savingOrder: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -48,6 +54,17 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     dragOptions() { | ||||
|       return { | ||||
|         animation: 200, | ||||
|         group: 'description', | ||||
|         ghostClass: 'ghost', | ||||
|         disabled: !this.userCanUpdate | ||||
|       } | ||||
|     }, | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     media() { | ||||
|       return this.libraryItem.media || {} | ||||
|     }, | ||||
| @ -59,23 +76,53 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     changeSort() { | ||||
|       this.episodesCopy.sort((a, b) => { | ||||
|         if (this.sortDesc) { | ||||
|           return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' }) | ||||
|         } | ||||
|         return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' }) | ||||
|       }) | ||||
| 
 | ||||
|       this.orderChanged = this.checkHasOrderChanged() | ||||
|     }, | ||||
|     checkHasOrderChanged() { | ||||
|       for (let i = 0; i < this.episodesCopy.length; i++) { | ||||
|         var epc = this.episodesCopy[i] | ||||
|         var ep = this.episodes[i] | ||||
|         if (epc.index != ep.index) { | ||||
|           return true | ||||
|         } | ||||
|       } | ||||
|       return false | ||||
|     }, | ||||
|     editEpisode(episode) { | ||||
|       this.selectedEpisode = episode | ||||
|       this.showEditEpisodeModal = true | ||||
|     }, | ||||
|     draggableUpdate() { | ||||
|       this.orderChanged = this.checkHasOrderChanged() | ||||
|     }, | ||||
|     async saveOrder() { | ||||
|       if (!this.userCanUpdate) return | ||||
| 
 | ||||
|       this.savingOrder = true | ||||
| 
 | ||||
|       var episodesUpdate = { | ||||
|         episodes: this.episodesCopy.map((b) => b.id) | ||||
|       } | ||||
|       this.$axios | ||||
|       await this.$axios | ||||
|         .$patch(`/api/items/${this.libraryItem.id}/episodes`, episodesUpdate) | ||||
|         .then((podcast) => { | ||||
|           console.log('Podcast updated', podcast) | ||||
|           this.$toast.success('Saved episode order') | ||||
|           this.orderChanged = false | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('Failed to update podcast', error) | ||||
|           this.$toast.error('Failed to save podcast episode order') | ||||
|         }) | ||||
|       this.savingOrder = false | ||||
|     }, | ||||
|     init() { | ||||
|       this.episodesCopy = this.episodes.map((ep) => { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user