mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update sorting and filtering for podcasts, add title ignore prefix to podcast metadata, check user permissions for podcast episode row UI
This commit is contained in:
		
							parent
							
								
									23cc6bb210
								
							
						
					
					
						commit
						ac097862fc
					
				@ -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