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 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> | ||||
|         </div> | ||||
| 
 | ||||
| @ -99,7 +99,8 @@ export default { | ||||
|       default: () => [] | ||||
|     }, | ||||
|     sleepTimerSet: Boolean, | ||||
|     sleepTimerRemaining: Number | ||||
|     sleepTimerRemaining: Number, | ||||
|     isPodcast: Boolean | ||||
|   }, | ||||
|   data() { | ||||
|     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 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-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"> | ||||
| @ -70,6 +70,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     isPodcast() { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||
|     }, | ||||
|     isGridMode() { | ||||
|       return this.viewMode === 'grid' | ||||
|     }, | ||||
| @ -80,6 +83,7 @@ export default { | ||||
|       return this.totalEntities | ||||
|     }, | ||||
|     entityName() { | ||||
|       if (this.isPodcast) return 'Podcasts' | ||||
|       if (!this.page) return 'Books' | ||||
|       if (this.page === 'series') return 'Series' | ||||
|       if (this.page === 'collections') return 'Collections' | ||||
|  | ||||
| @ -85,6 +85,9 @@ export default { | ||||
|     showExperimentalFeatures() { | ||||
|       return this.$store.state.showExperimentalFeatures | ||||
|     }, | ||||
|     isPodcast() { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||
|     }, | ||||
|     emptyMessage() { | ||||
|       if (this.page === 'series') return `You have no series` | ||||
|       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('desc', this.orderDesc ? 1 : 0) | ||||
|         } | ||||
|         if (this.collapseSeries) { | ||||
|         if (this.collapseSeries && !this.isPodcast) { | ||||
|           searchParams.set('collapseseries', 1) | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @ -33,6 +33,7 @@ | ||||
|       :bookmarks="bookmarks" | ||||
|       :sleep-timer-set="sleepTimerSet" | ||||
|       :sleep-timer-remaining="sleepTimerRemaining" | ||||
|       :is-podcast="isPodcast" | ||||
|       @playPause="playPause" | ||||
|       @jumpForward="jumpForward" | ||||
|       @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"> | ||||
|       <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)"> | ||||
|             <div class="flex items-center justify-between"> | ||||
|               <span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span> | ||||
| @ -67,7 +67,7 @@ export default { | ||||
|     return { | ||||
|       showMenu: false, | ||||
|       sublist: null, | ||||
|       items: [ | ||||
|       bookItems: [ | ||||
|         { | ||||
|           text: 'All', | ||||
|           value: 'all' | ||||
| @ -112,6 +112,22 @@ export default { | ||||
|           value: 'issues', | ||||
|           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) | ||||
|       } | ||||
|     }, | ||||
|     isPodcast() { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||
|     }, | ||||
|     selectItems() { | ||||
|       if (this.isPodcast) return this.podcastItems | ||||
|       return this.bookItems | ||||
|     }, | ||||
|     selectedItemSublist() { | ||||
|       return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false | ||||
|     }, | ||||
| @ -152,7 +175,7 @@ export default { | ||||
|         } | ||||
|         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 '' | ||||
|       return _sel.text | ||||
|     }, | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|     </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"> | ||||
|       <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)"> | ||||
|           <div class="flex items-center"> | ||||
|             <span class="font-normal ml-3 block truncate text-xs">{{ item.text }}</span> | ||||
| @ -31,7 +31,7 @@ export default { | ||||
|   data() { | ||||
|     return { | ||||
|       showMenu: false, | ||||
|       items: [ | ||||
|       bookItems: [ | ||||
|         { | ||||
|           text: 'Title', | ||||
|           value: 'media.metadata.title' | ||||
| @ -48,10 +48,32 @@ export default { | ||||
|           text: 'Added At', | ||||
|           value: 'addedAt' | ||||
|         }, | ||||
|         // { | ||||
|         //   text: 'Duration', | ||||
|         //   value: 'media.duration' | ||||
|         // }, | ||||
|         { | ||||
|           text: 'Size', | ||||
|           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', | ||||
|           value: 'size' | ||||
| @ -84,11 +106,18 @@ export default { | ||||
|         this.$emit('update:descending', val) | ||||
|       } | ||||
|     }, | ||||
|     isPodcast() { | ||||
|       return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast' | ||||
|     }, | ||||
|     selectItems() { | ||||
|       if (this.isPodcast) return this.podcastItems | ||||
|       return this.bookItems | ||||
|     }, | ||||
|     selectedText() { | ||||
|       var _selected = this.selected | ||||
|       if (!_selected) return '' | ||||
|       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 '' | ||||
|       return _sel.text | ||||
|     } | ||||
|  | ||||
| @ -7,13 +7,13 @@ | ||||
|     </svg> | ||||
|     <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> | ||||
|     <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 && canScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn> | ||||
|     <ui-btn v-show="isHovering && !libraryScan" small color="success" @click.stop="scan">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="!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="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 && !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"> | ||||
|       <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" /> | ||||
| @ -48,15 +48,6 @@ export default { | ||||
|     }, | ||||
|     libraryScan() { | ||||
|       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: { | ||||
|  | ||||
| @ -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="flex h-full items-center"> | ||||
|         <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 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> | ||||
| @ -70,6 +70,12 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     userCanUpdate() { | ||||
|       return this.$store.getters['user/getUserCanUpdate'] | ||||
|     }, | ||||
|     userCanDelete() { | ||||
|       return this.$store.getters['user/getUserCanDelete'] | ||||
|     }, | ||||
|     audioFile() { | ||||
|       return this.episode.audioFile | ||||
|     }, | ||||
|  | ||||
| @ -104,10 +104,6 @@ export default { | ||||
|           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) { | ||||
|         this.$store.commit('setServerSettings', payload.serverSettings) | ||||
| 
 | ||||
|  | ||||
| @ -65,7 +65,7 @@ export const actions = { | ||||
|         return [] | ||||
|       }) | ||||
|   }, | ||||
|   fetch({ state, commit, rootState, rootGetters }, libraryId) { | ||||
|   fetch({ state, dispatch, commit, rootState, rootGetters }, libraryId) { | ||||
|     if (!rootState.user || !rootState.user.user) { | ||||
|       console.error('libraries/fetch - User not set') | ||||
|       return false | ||||
| @ -83,6 +83,9 @@ export const actions = { | ||||
|         var library = data.library | ||||
|         var filterData = data.filterdata | ||||
|         var issues = data.issues || 0 | ||||
| 
 | ||||
|         dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true }) | ||||
| 
 | ||||
|         commit('addUpdate', library) | ||||
|         commit('setLibraryIssues', issues) | ||||
|         commit('setLibraryFilterData', filterData) | ||||
|  | ||||
| @ -1,10 +1,7 @@ | ||||
| 
 | ||||
| import Vue from 'vue' | ||||
| 
 | ||||
| export const state = () => ({ | ||||
|   user: null, | ||||
|   settings: { | ||||
|     orderBy: 'book.title', | ||||
|     orderBy: 'media.metadata.title', | ||||
|     orderDesc: false, | ||||
|     filterBy: 'all', | ||||
|     playbackRate: 1, | ||||
| @ -67,6 +64,27 @@ export const getters = { | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
|     var updatePayload = { | ||||
|       ...payload | ||||
| @ -104,6 +122,7 @@ export const actions = { | ||||
| export const mutations = { | ||||
|   setUser(state, user) { | ||||
|     state.user = user | ||||
|     state.settings = user.settings | ||||
|     if (user) { | ||||
|       if (user.token) localStorage.setItem('token', user.token) | ||||
|     } else { | ||||
| @ -125,7 +144,6 @@ export const mutations = { | ||||
|   }, | ||||
|   setSettings(state, settings) { | ||||
|     if (!settings) return | ||||
| 
 | ||||
|     var hasChanges = false | ||||
|     for (const key in settings) { | ||||
|       if (state.settings[key] !== settings[key]) { | ||||
|  | ||||
| @ -235,6 +235,13 @@ class Server { | ||||
|       socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) | ||||
|       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', () => { | ||||
|         Logger.removeSocketListener(socket.id) | ||||
| 
 | ||||
|  | ||||
| @ -167,7 +167,6 @@ class LibraryItemController { | ||||
|     res.sendStatus(500) | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   // POST: api/items/:id/play
 | ||||
|   startPlaybackSession(req, res) { | ||||
|     if (!req.libraryItem.media.numTracks) { | ||||
| @ -338,7 +337,6 @@ class LibraryItemController { | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
|   middleware(req, res, next) { | ||||
|     var item = this.db.libraryItems.find(li => li.id === req.params.id) | ||||
|     if (!item || !item.media) return res.sendStatus(404) | ||||
|  | ||||
| @ -54,7 +54,7 @@ class Podcast { | ||||
| 
 | ||||
|   toJSONMinified() { | ||||
|     return { | ||||
|       metadata: this.metadata.toJSON(), | ||||
|       metadata: this.metadata.toJSONMinified(), | ||||
|       coverPath: this.coverPath, | ||||
|       tags: [...this.tags], | ||||
|       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() { | ||||
|     return this.toJSON() | ||||
|     return this.toJSONMinified() | ||||
|   } | ||||
| 
 | ||||
|   clone() { | ||||
|     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
 | ||||
|     var keysToCheck = ['title', 'author', 'itunesId', 'itunesArtistId'] | ||||
|     for (var key of keysToCheck) { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user