mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Merge branch 'master' of https://github.com/rasmuslos/audiobookshelf
This commit is contained in:
		
						commit
						da2e65c042
					
				| @ -12,7 +12,7 @@ | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> |         <div v-if="!isPodcast" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> | ||||||
|           <span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> |           <span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
| @ -99,7 +99,8 @@ export default { | |||||||
|       default: () => [] |       default: () => [] | ||||||
|     }, |     }, | ||||||
|     sleepTimerSet: Boolean, |     sleepTimerSet: Boolean, | ||||||
|     sleepTimerRemaining: Number |     sleepTimerRemaining: Number, | ||||||
|  |     isPodcast: Boolean | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|  | |||||||
| @ -1,127 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <div class="outer-container"> |  | ||||||
|     <!-- absolute positioned container --> |  | ||||||
|     <div class="inner-container"> |  | ||||||
|       <div class="relative h-10"> |  | ||||||
|         <div class="table-header" id="headerdiv"> |  | ||||||
|           <table id="headertable" width="100%" cellpadding="0" cellspacing="0"> |  | ||||||
|             <thead> |  | ||||||
|               <tr> |  | ||||||
|                 <th class="header-cell min-w-12 max-w-12"></th> |  | ||||||
|                 <th class="header-cell min-w-6 max-w-6"></th> |  | ||||||
|                 <th class="header-cell min-w-64 max-w-64 px-2">Title</th> |  | ||||||
|                 <th class="header-cell min-w-48 max-w-48 px-2">Author</th> |  | ||||||
|                 <th class="header-cell min-w-48 max-w-48 px-2">Series</th> |  | ||||||
|                 <th class="header-cell min-w-24 max-w-24 px-2">Year</th> |  | ||||||
|                 <th class="header-cell min-w-80 max-w-80 px-2">Description</th> |  | ||||||
|                 <th class="header-cell min-w-48 max-w-48 px-2">Narrator</th> |  | ||||||
|                 <th class="header-cell min-w-48 max-w-48 px-2">Genres</th> |  | ||||||
|                 <th class="header-cell min-w-48 max-w-48 px-2">Tags</th> |  | ||||||
|                 <th class="header-cell min-w-24 max-w-24 px-2"></th> |  | ||||||
|               </tr> |  | ||||||
|             </thead> |  | ||||||
|           </table> |  | ||||||
|         </div> |  | ||||||
|         <div class="absolute top-0 left-0 w-full h-full pointer-events-none" :class="isScrollable ? 'header-shadow' : ''" /> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div ref="tableBody" class="table-body" onscroll="document.getElementById('headerdiv').scrollLeft = this.scrollLeft;" @scroll="tableScrolled"> |  | ||||||
|         <table id="bodytable" width="100%" cellpadding="0" cellspacing="0"> |  | ||||||
|           <tbody> |  | ||||||
|             <template v-for="book in books"> |  | ||||||
|               <app-book-list-row :key="book.id" :book="book" @edit="editBook" /> |  | ||||||
|             </template> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     books: { |  | ||||||
|       type: Array, |  | ||||||
|       default: () => [] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       isScrollable: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: {}, |  | ||||||
|   methods: { |  | ||||||
|     checkIsScrolled() { |  | ||||||
|       if (!this.$refs.tableBody) return |  | ||||||
|       this.isScrollable = this.$refs.tableBody.scrollTop > 0 |  | ||||||
|     }, |  | ||||||
|     tableScrolled() { |  | ||||||
|       this.checkIsScrolled() |  | ||||||
|     }, |  | ||||||
|     editBook(book) { |  | ||||||
|       var bookIds = this.books.map((e) => e.id) |  | ||||||
|       this.$store.commit('setBookshelfBookIds', bookIds) |  | ||||||
|       this.$store.commit('showEditModal', book) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() { |  | ||||||
|     this.checkIsScrolled() |  | ||||||
|   }, |  | ||||||
|   beforeDestroy() {} |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
| .outer-container { |  | ||||||
|   position: absolute; |  | ||||||
|   top: 0; |  | ||||||
|   left: 0; |  | ||||||
|   overflow: visible; |  | ||||||
|   height: calc(100% - 50px); |  | ||||||
|   width: calc(100% - 10px); |  | ||||||
|   margin: 10px; |  | ||||||
| } |  | ||||||
| .inner-container { |  | ||||||
|   width: 100%; |  | ||||||
|   height: 100%; |  | ||||||
|   position: relative; |  | ||||||
| } |  | ||||||
| .table-header { |  | ||||||
|   float: left; |  | ||||||
|   overflow: hidden; |  | ||||||
|   width: 100%; |  | ||||||
| } |  | ||||||
| .header-shadow { |  | ||||||
|   box-shadow: 3px 8px 3px #11111155; |  | ||||||
| } |  | ||||||
| .table-body { |  | ||||||
|   float: left; |  | ||||||
|   height: 100%; |  | ||||||
|   width: inherit; |  | ||||||
|   overflow-y: scroll; |  | ||||||
|   padding-right: 0px; |  | ||||||
| } |  | ||||||
| .header-cell { |  | ||||||
|   background-color: #22222288; |  | ||||||
|   padding: 0px 4px; |  | ||||||
|   text-align: left; |  | ||||||
|   height: 40px; |  | ||||||
|   font-size: 0.9rem; |  | ||||||
|   font-weight: semi-bold; |  | ||||||
| } |  | ||||||
| .body-cell { |  | ||||||
|   text-align: left; |  | ||||||
|   font-size: 0.9rem; |  | ||||||
| } |  | ||||||
| .book-row { |  | ||||||
|   background-color: #22222288; |  | ||||||
| } |  | ||||||
| .book-row:nth-child(odd) { |  | ||||||
|   background-color: #333; |  | ||||||
| } |  | ||||||
| .book-row.selected { |  | ||||||
|   background-color: rgba(0, 255, 0, 0.05); |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
| @ -1,164 +0,0 @@ | |||||||
| <template> |  | ||||||
|   <tr class="book-row" :class="selected ? 'selected' : ''"> |  | ||||||
|     <td class="body-cell min-w-12 max-w-12"> |  | ||||||
|       <div class="flex justify-center"> |  | ||||||
|         <div class="bg-white border-2 rounded border-gray-400 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500 w-4 h-4" @click="selectBtnClick"> |  | ||||||
|           <svg v-if="selected" class="fill-current text-green-500 pointer-events-none w-2.5 h-2.5" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-6 max-w-6"> |  | ||||||
|       <covers-hover-book-cover :audiobook="book" /> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-64 max-w-64 px-2"> |  | ||||||
|       <nuxt-link :to="`/item/${book.id}`" class="hover:underline"> |  | ||||||
|         <p class="truncate"> |  | ||||||
|           {{ book.book.title }}<span v-if="book.book.subtitle">: {{ book.book.subtitle }}</span> |  | ||||||
|         </p> |  | ||||||
|       </nuxt-link> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-48 max-w-48 px-2"> |  | ||||||
|       <p class="truncate">{{ book.book.authorFL }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-48 max-w-48 px-2"> |  | ||||||
|       <p class="truncate">{{ seriesText }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-24 max-w-24 px-2"> |  | ||||||
|       <p class="truncate">{{ book.book.publishedYear }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-80 max-w-80 px-2"> |  | ||||||
|       <p class="truncate">{{ book.book.description }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-48 max-w-48 px-2"> |  | ||||||
|       <p class="truncate">{{ book.book.narrator }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-48 max-w-48 px-2"> |  | ||||||
|       <p class="truncate">{{ genresText }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-48 max-w-48 px-2"> |  | ||||||
|       <p class="truncate">{{ tagsText }}</p> |  | ||||||
|     </td> |  | ||||||
|     <td class="body-cell min-w-24 max-w-24 px-2"> |  | ||||||
|       <div class="flex"> |  | ||||||
|         <span v-if="userCanUpdate" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-xl" @click="editClick">edit</span> |  | ||||||
|         <span v-if="showPlayButton" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-2xl mx-1" @click="startStream">play_arrow</span> |  | ||||||
|         <span v-if="showReadButton" class="material-icons cursor-pointer text-white text-opacity-60 hover:text-opacity-100 text-xl" @click="openEbook">auto_stories</span> |  | ||||||
|       </div> |  | ||||||
|     </td> |  | ||||||
|   </tr> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script> |  | ||||||
| export default { |  | ||||||
|   props: { |  | ||||||
|     book: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => {} |  | ||||||
|     }, |  | ||||||
|     userAudiobook: { |  | ||||||
|       type: Object, |  | ||||||
|       default: () => {} |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       isProcessingReadUpdate: false |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   computed: { |  | ||||||
|     showExperimentalFeatures() { |  | ||||||
|       return this.$store.state.showExperimentalFeatures |  | ||||||
|     }, |  | ||||||
|     libraryItemId() { |  | ||||||
|       return this.book.id |  | ||||||
|     }, |  | ||||||
|     selected: { |  | ||||||
|       get() { |  | ||||||
|         return this.$store.getters['getIsLibraryItemSelected'](this.libraryItemId) |  | ||||||
|       }, |  | ||||||
|       set(val) { |  | ||||||
|         if (this.processingBatch) return |  | ||||||
|         this.$store.commit('setLibraryItemSelected', { libraryItemId: this.libraryItemId, selected: val }) |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     processingBatch() { |  | ||||||
|       return this.$store.state.processingBatch |  | ||||||
|     }, |  | ||||||
|     bookObj() { |  | ||||||
|       return this.book.book || {} |  | ||||||
|     }, |  | ||||||
|     series() { |  | ||||||
|       return this.bookObj.series || null |  | ||||||
|     }, |  | ||||||
|     volumeNumber() { |  | ||||||
|       return this.bookObj.volumeNumber || null |  | ||||||
|     }, |  | ||||||
|     seriesText() { |  | ||||||
|       if (!this.series) return '' |  | ||||||
|       if (!this.volumeNumber) return this.series |  | ||||||
|       return `${this.series} #${this.volumeNumber}` |  | ||||||
|     }, |  | ||||||
|     genresText() { |  | ||||||
|       if (!this.bookObj.genres) return '' |  | ||||||
|       return this.bookObj.genres.join(', ') |  | ||||||
|     }, |  | ||||||
|     tagsText() { |  | ||||||
|       return (this.book.tags || []).join(', ') |  | ||||||
|     }, |  | ||||||
|     isMissing() { |  | ||||||
|       return this.book.isMissing |  | ||||||
|     }, |  | ||||||
|     isInvalid() { |  | ||||||
|       return this.book.isInvalid |  | ||||||
|     }, |  | ||||||
|     numEbooks() { |  | ||||||
|       return this.book.numEbooks |  | ||||||
|     }, |  | ||||||
|     numTracks() { |  | ||||||
|       return this.book.numTracks |  | ||||||
|     }, |  | ||||||
|     isStreaming() { |  | ||||||
|       return this.$store.getters['getLibraryItemIdStreaming'] === this.libraryItemId |  | ||||||
|     }, |  | ||||||
|     showReadButton() { |  | ||||||
|       return this.showExperimentalFeatures && this.numEbooks |  | ||||||
|     }, |  | ||||||
|     showPlayButton() { |  | ||||||
|       return !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming |  | ||||||
|     }, |  | ||||||
|     userIsRead() { |  | ||||||
|       return this.userAudiobook ? !!this.userAudiobook.isRead : false |  | ||||||
|     }, |  | ||||||
|     userCanUpdate() { |  | ||||||
|       return this.$store.getters['user/getUserCanUpdate'] |  | ||||||
|     }, |  | ||||||
|     userCanDelete() { |  | ||||||
|       return this.$store.getters['user/getUserCanDelete'] |  | ||||||
|     }, |  | ||||||
|     userCanDownload() { |  | ||||||
|       return this.$store.getters['user/getUserCanDownload'] |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   methods: { |  | ||||||
|     selectBtnClick() { |  | ||||||
|       if (this.processingBatch) return |  | ||||||
|       this.$store.commit('toggleLibraryItemSelected', this.libraryItemId) |  | ||||||
|     }, |  | ||||||
|     openEbook() { |  | ||||||
|       this.$store.commit('showEReader', this.book) |  | ||||||
|     }, |  | ||||||
|     downloadClick() { |  | ||||||
|       this.$store.commit('showEditModalOnTab', { libraryItem: this.book, tab: 'download' }) |  | ||||||
|     }, |  | ||||||
|     startStream() { |  | ||||||
|       this.$eventBus.$emit('play-item', { |  | ||||||
|         libraryItemId: this.book.id |  | ||||||
|       }) |  | ||||||
|     }, |  | ||||||
|     editClick() { |  | ||||||
|       this.$emit('edit', this.book) |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   mounted() {} |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| @ -27,7 +27,7 @@ | |||||||
|         </div> |         </div> | ||||||
|         <div class="flex-grow hidden sm:inline-block" /> |         <div class="flex-grow hidden sm:inline-block" /> | ||||||
| 
 | 
 | ||||||
|         <ui-checkbox v-show="showSortFilters" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" /> |         <ui-checkbox v-show="showSortFilters && !isPodcast" v-model="settings.collapseSeries" label="Collapse Series" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" /> | ||||||
|         <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" /> |         <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" /> | ||||||
|         <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" /> |         <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" /> | ||||||
|         <!-- <div v-show="showSortFilters" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md"> |         <!-- <div v-show="showSortFilters" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md"> | ||||||
| @ -70,6 +70,9 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     isPodcast() { | ||||||
|  |       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||||
|  |     }, | ||||||
|     isGridMode() { |     isGridMode() { | ||||||
|       return this.viewMode === 'grid' |       return this.viewMode === 'grid' | ||||||
|     }, |     }, | ||||||
| @ -80,6 +83,7 @@ export default { | |||||||
|       return this.totalEntities |       return this.totalEntities | ||||||
|     }, |     }, | ||||||
|     entityName() { |     entityName() { | ||||||
|  |       if (this.isPodcast) return 'Podcasts' | ||||||
|       if (!this.page) return 'Books' |       if (!this.page) return 'Books' | ||||||
|       if (this.page === 'series') return 'Series' |       if (this.page === 'series') return 'Series' | ||||||
|       if (this.page === 'collections') return 'Collections' |       if (this.page === 'collections') return 'Collections' | ||||||
|  | |||||||
| @ -85,6 +85,9 @@ export default { | |||||||
|     showExperimentalFeatures() { |     showExperimentalFeatures() { | ||||||
|       return this.$store.state.showExperimentalFeatures |       return this.$store.state.showExperimentalFeatures | ||||||
|     }, |     }, | ||||||
|  |     isPodcast() { | ||||||
|  |       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||||
|  |     }, | ||||||
|     emptyMessage() { |     emptyMessage() { | ||||||
|       if (this.page === 'series') return `You have no series` |       if (this.page === 'series') return `You have no series` | ||||||
|       if (this.page === 'collections') return "You haven't made any collections yet" |       if (this.page === 'collections') return "You haven't made any collections yet" | ||||||
| @ -386,7 +389,7 @@ export default { | |||||||
|           searchParams.set('sort', this.orderBy) |           searchParams.set('sort', this.orderBy) | ||||||
|           searchParams.set('desc', this.orderDesc ? 1 : 0) |           searchParams.set('desc', this.orderDesc ? 1 : 0) | ||||||
|         } |         } | ||||||
|         if (this.collapseSeries) { |         if (this.collapseSeries && !this.isPodcast) { | ||||||
|           searchParams.set('collapseseries', 1) |           searchParams.set('collapseseries', 1) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -33,6 +33,7 @@ | |||||||
|       :bookmarks="bookmarks" |       :bookmarks="bookmarks" | ||||||
|       :sleep-timer-set="sleepTimerSet" |       :sleep-timer-set="sleepTimerSet" | ||||||
|       :sleep-timer-remaining="sleepTimerRemaining" |       :sleep-timer-remaining="sleepTimerRemaining" | ||||||
|  |       :is-podcast="isPodcast" | ||||||
|       @playPause="playPause" |       @playPause="playPause" | ||||||
|       @jumpForward="jumpForward" |       @jumpForward="jumpForward" | ||||||
|       @jumpBackward="jumpBackward" |       @jumpBackward="jumpBackward" | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ | |||||||
| 
 | 
 | ||||||
|     <div 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"> |     <div 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"> | ||||||
|       <ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> |       <ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> | ||||||
|         <template v-for="item in items"> |         <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)"> |           <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)"> | ||||||
|             <div class="flex items-center justify-between"> |             <div class="flex items-center justify-between"> | ||||||
|               <span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span> |               <span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span> | ||||||
| @ -67,7 +67,7 @@ export default { | |||||||
|     return { |     return { | ||||||
|       showMenu: false, |       showMenu: false, | ||||||
|       sublist: null, |       sublist: null, | ||||||
|       items: [ |       bookItems: [ | ||||||
|         { |         { | ||||||
|           text: 'All', |           text: 'All', | ||||||
|           value: 'all' |           value: 'all' | ||||||
| @ -112,6 +112,22 @@ export default { | |||||||
|           value: 'issues', |           value: 'issues', | ||||||
|           sublist: false |           sublist: false | ||||||
|         } |         } | ||||||
|  |       ], | ||||||
|  |       podcastItems: [ | ||||||
|  |         { | ||||||
|  |           text: 'All', | ||||||
|  |           value: 'all' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'Genre', | ||||||
|  |           value: 'genres', | ||||||
|  |           sublist: true | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'Tag', | ||||||
|  |           value: 'tags', | ||||||
|  |           sublist: true | ||||||
|  |         } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| @ -132,6 +148,13 @@ export default { | |||||||
|         this.$emit('input', val) |         this.$emit('input', val) | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     isPodcast() { | ||||||
|  |       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||||
|  |     }, | ||||||
|  |     selectItems() { | ||||||
|  |       if (this.isPodcast) return this.podcastItems | ||||||
|  |       return this.bookItems | ||||||
|  |     }, | ||||||
|     selectedItemSublist() { |     selectedItemSublist() { | ||||||
|       return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false |       return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false | ||||||
|     }, |     }, | ||||||
| @ -152,7 +175,7 @@ export default { | |||||||
|         } |         } | ||||||
|         return decoded |         return decoded | ||||||
|       } |       } | ||||||
|       var _sel = this.items.find((i) => i.value === this.selected) |       var _sel = this.selectItems.find((i) => i.value === this.selected) | ||||||
|       if (!_sel) return '' |       if (!_sel) return '' | ||||||
|       return _sel.text |       return _sel.text | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ | |||||||
|     </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-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"> |       <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> | ||||||
| @ -31,7 +31,7 @@ export default { | |||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       showMenu: false, |       showMenu: false, | ||||||
|       items: [ |       bookItems: [ | ||||||
|         { |         { | ||||||
|           text: 'Title', |           text: 'Title', | ||||||
|           value: 'media.metadata.title' |           value: 'media.metadata.title' | ||||||
| @ -48,10 +48,32 @@ export default { | |||||||
|           text: 'Added At', |           text: 'Added At', | ||||||
|           value: 'addedAt' |           value: 'addedAt' | ||||||
|         }, |         }, | ||||||
|         // { |         { | ||||||
|         //   text: 'Duration', |           text: 'Size', | ||||||
|         //   value: 'media.duration' |           value: 'size' | ||||||
|         // }, |         }, | ||||||
|  |         { | ||||||
|  |           text: 'File Birthtime', | ||||||
|  |           value: 'birthtimeMs' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'File Modified', | ||||||
|  |           value: 'mtimeMs' | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       podcastItems: [ | ||||||
|  |         { | ||||||
|  |           text: 'Title', | ||||||
|  |           value: 'media.metadata.title' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'Author', | ||||||
|  |           value: 'media.metadata.author' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'Added At', | ||||||
|  |           value: 'addedAt' | ||||||
|  |         }, | ||||||
|         { |         { | ||||||
|           text: 'Size', |           text: 'Size', | ||||||
|           value: 'size' |           value: 'size' | ||||||
| @ -84,11 +106,18 @@ export default { | |||||||
|         this.$emit('update:descending', val) |         this.$emit('update:descending', val) | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     isPodcast() { | ||||||
|  |       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||||
|  |     }, | ||||||
|  |     selectItems() { | ||||||
|  |       if (this.isPodcast) return this.podcastItems | ||||||
|  |       return this.bookItems | ||||||
|  |     }, | ||||||
|     selectedText() { |     selectedText() { | ||||||
|       var _selected = this.selected |       var _selected = this.selected | ||||||
|       if (!_selected) return '' |       if (!_selected) return '' | ||||||
|       if (this.selected.startsWith('book.')) _selected = _selected.replace('book.', 'media.metadata.') |       if (this.selected.startsWith('book.')) _selected = _selected.replace('book.', 'media.metadata.') | ||||||
|       var _sel = this.items.find((i) => i.value === _selected) |       var _sel = this.selectItems.find((i) => i.value === _selected) | ||||||
|       if (!_sel) return '' |       if (!_sel) return '' | ||||||
|       return _sel.text |       return _sel.text | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -7,13 +7,13 @@ | |||||||
|     </svg> |     </svg> | ||||||
|     <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> |     <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> | ||||||
|     <div class="flex-grow" /> |     <div class="flex-grow" /> | ||||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="success" @click.stop="scan">Scan</ui-btn> |     <ui-btn v-show="isHovering && !libraryScan" small color="success" @click.stop="scan">Scan</ui-btn> | ||||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn> |     <ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn> | ||||||
| 
 | 
 | ||||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" class="ml-2" @click.stop="matchAll">Match Books</ui-btn> |     <ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2" @click.stop="matchAll">Match Books</ui-btn> | ||||||
| 
 | 
 | ||||||
|     <span v-show="isHovering && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span> |     <span v-show="isHovering && !libraryScan && showEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span> | ||||||
|     <span v-show="!libraryScan && isHovering && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span> |     <span v-show="!libraryScan && isHovering && showEdit && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span> | ||||||
|     <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> |     <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> | ||||||
|       <svg viewBox="0 0 24 24" class="w-6 h-6"> |       <svg viewBox="0 0 24 24" class="w-6 h-6"> | ||||||
|         <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> |         <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> | ||||||
| @ -48,15 +48,6 @@ export default { | |||||||
|     }, |     }, | ||||||
|     libraryScan() { |     libraryScan() { | ||||||
|       return this.$store.getters['scanners/getLibraryScan'](this.library.id) |       return this.$store.getters['scanners/getLibraryScan'](this.library.id) | ||||||
|     }, |  | ||||||
|     canEdit() { |  | ||||||
|       return this.$store.getters['user/getIsRoot'] |  | ||||||
|     }, |  | ||||||
|     canDelete() { |  | ||||||
|       return this.$store.getters['user/getIsRoot'] |  | ||||||
|     }, |  | ||||||
|     canScan() { |  | ||||||
|       return this.$store.getters['user/getIsRoot'] |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  | |||||||
| @ -31,10 +31,10 @@ | |||||||
|     <div class="w-24 min-w-24 -right-0 absolute top-0 h-full transform transition-transform" :class="!isHovering ? 'translate-x-32' : 'translate-x-0'"> |     <div class="w-24 min-w-24 -right-0 absolute top-0 h-full transform transition-transform" :class="!isHovering ? 'translate-x-32' : 'translate-x-0'"> | ||||||
|       <div class="flex h-full items-center"> |       <div class="flex h-full items-center"> | ||||||
|         <div class="mx-1"> |         <div class="mx-1"> | ||||||
|           <ui-icon-btn icon="edit" borderless @click="clickEdit" /> |           <ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" /> | ||||||
|         </div> |         </div> | ||||||
|         <div class="mx-1"> |         <div class="mx-1"> | ||||||
|           <ui-icon-btn icon="close" borderless @click="removeClick" /> |           <ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @ -70,6 +70,12 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|  |     userCanUpdate() { | ||||||
|  |       return this.$store.getters['user/getUserCanUpdate'] | ||||||
|  |     }, | ||||||
|  |     userCanDelete() { | ||||||
|  |       return this.$store.getters['user/getUserCanDelete'] | ||||||
|  |     }, | ||||||
|     audioFile() { |     audioFile() { | ||||||
|       return this.episode.audioFile |       return this.episode.audioFile | ||||||
|     }, |     }, | ||||||
|  | |||||||
| @ -104,10 +104,6 @@ export default { | |||||||
|           console.warn('Stream Container not mounted') |           console.warn('Stream Container not mounted') | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       if (payload.user) { |  | ||||||
|         this.$store.commit('user/setUser', payload.user) |  | ||||||
|         this.$store.commit('user/setSettings', payload.user.settings) |  | ||||||
|       } |  | ||||||
|       if (payload.serverSettings) { |       if (payload.serverSettings) { | ||||||
|         this.$store.commit('setServerSettings', payload.serverSettings) |         this.$store.commit('setServerSettings', payload.serverSettings) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ export const actions = { | |||||||
|         return [] |         return [] | ||||||
|       }) |       }) | ||||||
|   }, |   }, | ||||||
|   fetch({ state, commit, rootState, rootGetters }, libraryId) { |   fetch({ state, dispatch, commit, rootState, rootGetters }, libraryId) { | ||||||
|     if (!rootState.user || !rootState.user.user) { |     if (!rootState.user || !rootState.user.user) { | ||||||
|       console.error('libraries/fetch - User not set') |       console.error('libraries/fetch - User not set') | ||||||
|       return false |       return false | ||||||
| @ -83,6 +83,9 @@ export const actions = { | |||||||
|         var library = data.library |         var library = data.library | ||||||
|         var filterData = data.filterdata |         var filterData = data.filterdata | ||||||
|         var issues = data.issues || 0 |         var issues = data.issues || 0 | ||||||
|  | 
 | ||||||
|  |         dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) | ||||||
|  | 
 | ||||||
|         commit('addUpdate', library) |         commit('addUpdate', library) | ||||||
|         commit('setLibraryIssues', issues) |         commit('setLibraryIssues', issues) | ||||||
|         commit('setLibraryFilterData', filterData) |         commit('setLibraryFilterData', filterData) | ||||||
|  | |||||||
| @ -1,10 +1,7 @@ | |||||||
| 
 |  | ||||||
| import Vue from 'vue' |  | ||||||
| 
 |  | ||||||
| export const state = () => ({ | export const state = () => ({ | ||||||
|   user: null, |   user: null, | ||||||
|   settings: { |   settings: { | ||||||
|     orderBy: 'book.title', |     orderBy: 'media.metadata.title', | ||||||
|     orderDesc: false, |     orderDesc: false, | ||||||
|     filterBy: 'all', |     filterBy: 'all', | ||||||
|     playbackRate: 1, |     playbackRate: 1, | ||||||
| @ -67,6 +64,27 @@ export const getters = { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const actions = { | export const actions = { | ||||||
|  |   // When changing libraries make sure sort and filter is still valid
 | ||||||
|  |   checkUpdateLibrarySortFilter({ state, dispatch, commit }, mediaType) { | ||||||
|  |     var settingsUpdate = {} | ||||||
|  |     if (mediaType == 'podcast') { | ||||||
|  |       if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') { | ||||||
|  |         settingsUpdate.orderBy = 'media.metadata.author' | ||||||
|  |       } | ||||||
|  |       var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues'] | ||||||
|  |       var filterByFirstPart = (state.settings.filterBy || '').split('.').shift() | ||||||
|  |       if (invalidFilters.includes(filterByFirstPart)) { | ||||||
|  |         settingsUpdate.filterBy = 'all' | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       if (state.settings.orderBy == 'media.metadata.author') { | ||||||
|  |         settingsUpdate.orderBy = 'media.metadata.authorName' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (Object.keys(settingsUpdate).length) { | ||||||
|  |       dispatch('updateUserSettings', settingsUpdate) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   updateUserSettings({ commit }, payload) { |   updateUserSettings({ commit }, payload) { | ||||||
|     var updatePayload = { |     var updatePayload = { | ||||||
|       ...payload |       ...payload | ||||||
| @ -104,6 +122,7 @@ export const actions = { | |||||||
| export const mutations = { | export const mutations = { | ||||||
|   setUser(state, user) { |   setUser(state, user) { | ||||||
|     state.user = user |     state.user = user | ||||||
|  |     state.settings = user.settings | ||||||
|     if (user) { |     if (user) { | ||||||
|       if (user.token) localStorage.setItem('token', user.token) |       if (user.token) localStorage.setItem('token', user.token) | ||||||
|     } else { |     } else { | ||||||
| @ -125,7 +144,6 @@ export const mutations = { | |||||||
|   }, |   }, | ||||||
|   setSettings(state, settings) { |   setSettings(state, settings) { | ||||||
|     if (!settings) return |     if (!settings) return | ||||||
| 
 |  | ||||||
|     var hasChanges = false |     var hasChanges = false | ||||||
|     for (const key in settings) { |     for (const key in settings) { | ||||||
|       if (state.settings[key] !== settings[key]) { |       if (state.settings[key] !== settings[key]) { | ||||||
|  | |||||||
| @ -235,6 +235,13 @@ class Server { | |||||||
|       socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) |       socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) | ||||||
|       socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket)) |       socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket)) | ||||||
| 
 | 
 | ||||||
|  |       socket.on('ping', () => { | ||||||
|  |         var client = this.clients[socket.id] || {} | ||||||
|  |         var user = client.user || {} | ||||||
|  |         Logger.debug(`[Server] Received ping from socket ${user.username || 'No User'}`) | ||||||
|  |         socket.emit('pong') | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|       socket.on('disconnect', () => { |       socket.on('disconnect', () => { | ||||||
|         Logger.removeSocketListener(socket.id) |         Logger.removeSocketListener(socket.id) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -167,7 +167,6 @@ class LibraryItemController { | |||||||
|     res.sendStatus(500) |     res.sendStatus(500) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   // POST: api/items/:id/play
 |   // POST: api/items/:id/play
 | ||||||
|   startPlaybackSession(req, res) { |   startPlaybackSession(req, res) { | ||||||
|     if (!req.libraryItem.media.numTracks) { |     if (!req.libraryItem.media.numTracks) { | ||||||
| @ -338,7 +337,6 @@ class LibraryItemController { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   middleware(req, res, next) { |   middleware(req, res, next) { | ||||||
|     var item = this.db.libraryItems.find(li => li.id === req.params.id) |     var item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||||
|     if (!item || !item.media) return res.sendStatus(404) |     if (!item || !item.media) return res.sendStatus(404) | ||||||
|  | |||||||
| @ -54,7 +54,7 @@ class Podcast { | |||||||
| 
 | 
 | ||||||
|   toJSONMinified() { |   toJSONMinified() { | ||||||
|     return { |     return { | ||||||
|       metadata: this.metadata.toJSON(), |       metadata: this.metadata.toJSONMinified(), | ||||||
|       coverPath: this.coverPath, |       coverPath: this.coverPath, | ||||||
|       tags: [...this.tags], |       tags: [...this.tags], | ||||||
|       numEpisodes: this.episodes.length, |       numEpisodes: this.episodes.length, | ||||||
|  | |||||||
| @ -53,14 +53,44 @@ class PodcastMetadata { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   toJSONMinified() { | ||||||
|  |     return { | ||||||
|  |       title: this.title, | ||||||
|  |       titleIgnorePrefix: this.titleIgnorePrefix, | ||||||
|  |       author: this.author, | ||||||
|  |       description: this.description, | ||||||
|  |       releaseDate: this.releaseDate, | ||||||
|  |       genres: [...this.genres], | ||||||
|  |       feedUrl: this.feedUrl, | ||||||
|  |       imageUrl: this.imageUrl, | ||||||
|  |       itunesPageUrl: this.itunesPageUrl, | ||||||
|  |       itunesId: this.itunesId, | ||||||
|  |       itunesArtistId: this.itunesArtistId, | ||||||
|  |       explicit: this.explicit, | ||||||
|  |       language: this.language | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   toJSONExpanded() { |   toJSONExpanded() { | ||||||
|     return this.toJSON() |     return this.toJSONMinified() | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   clone() { |   clone() { | ||||||
|     return new PodcastMetadata(this.toJSON()) |     return new PodcastMetadata(this.toJSON()) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   get titleIgnorePrefix() { | ||||||
|  |     if (!this.title) return '' | ||||||
|  |     var prefixesToIgnore = global.ServerSettings.sortingPrefixes || [] | ||||||
|  |     for (const prefix of prefixesToIgnore) { | ||||||
|  |       // e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
 | ||||||
|  |       if (this.title.toLowerCase().startsWith(`${prefix} `)) { | ||||||
|  |         return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}` | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return this.title | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   searchQuery(query) { // Returns key if match is found
 |   searchQuery(query) { // Returns key if match is found
 | ||||||
|     var keysToCheck = ['title', 'author', 'itunesId', 'itunesArtistId'] |     var keysToCheck = ['title', 'author', 'itunesId', 'itunesArtistId'] | ||||||
|     for (var key of keysToCheck) { |     for (var key of keysToCheck) { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user