mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Lazy bookshelf, api routes for categories and filter data
This commit is contained in:
		
							parent
							
								
									4587916c8e
								
							
						
					
					
						commit
						5c92aef048
					
				@ -40,7 +40,7 @@
 | 
				
			|||||||
      <div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
 | 
					      <div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
 | 
				
			||||||
        <h1 class="text-2xl px-4">{{ numAudiobooksSelected }} Selected</h1>
 | 
					        <h1 class="text-2xl px-4">{{ numAudiobooksSelected }} Selected</h1>
 | 
				
			||||||
        <ui-btn v-show="!isHome" small class="text-sm mx-2" @click="toggleSelectAll"
 | 
					        <ui-btn v-show="!isHome" small class="text-sm mx-2" @click="toggleSelectAll"
 | 
				
			||||||
          >{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ audiobooksShowing.length }})</span></ui-btn
 | 
					          >{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ totalBooks }})</span></ui-btn
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="flex-grow" />
 | 
					        <div class="flex-grow" />
 | 
				
			||||||
@ -65,7 +65,9 @@
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      processingBatchDelete: false
 | 
					      processingBatchDelete: false,
 | 
				
			||||||
 | 
					      totalBooks: 0,
 | 
				
			||||||
 | 
					      isAllSelected: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -96,9 +98,9 @@ export default {
 | 
				
			|||||||
    selectedAudiobooks() {
 | 
					    selectedAudiobooks() {
 | 
				
			||||||
      return this.$store.state.selectedAudiobooks
 | 
					      return this.$store.state.selectedAudiobooks
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isAllSelected() {
 | 
					    // isAllSelected() {
 | 
				
			||||||
      return this.audiobooksShowing.length === this.selectedAudiobooks.length
 | 
					    //   return this.audiobooksShowing.length === this.selectedAudiobooks.length
 | 
				
			||||||
    },
 | 
					    // },
 | 
				
			||||||
    userAudiobooks() {
 | 
					    userAudiobooks() {
 | 
				
			||||||
      return this.$store.state.user.user.audiobooks || {}
 | 
					      return this.$store.state.user.user.audiobooks || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -145,13 +147,17 @@ export default {
 | 
				
			|||||||
    cancelSelectionMode() {
 | 
					    cancelSelectionMode() {
 | 
				
			||||||
      if (this.processingBatchDelete) return
 | 
					      if (this.processingBatchDelete) return
 | 
				
			||||||
      this.$store.commit('setSelectedAudiobooks', [])
 | 
					      this.$store.commit('setSelectedAudiobooks', [])
 | 
				
			||||||
 | 
					      this.$eventBus.$emit('bookshelf-clear-selection')
 | 
				
			||||||
 | 
					      this.isAllSelected = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleSelectAll() {
 | 
					    toggleSelectAll() {
 | 
				
			||||||
      if (this.isAllSelected) {
 | 
					      if (this.isAllSelected) {
 | 
				
			||||||
        this.cancelSelectionMode()
 | 
					        this.cancelSelectionMode()
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        var audiobookIds = this.audiobooksShowing.map((a) => a.id)
 | 
					        this.$eventBus.$emit('bookshelf-select-all')
 | 
				
			||||||
        this.$store.commit('setSelectedAudiobooks', audiobookIds)
 | 
					        this.isAllSelected = true
 | 
				
			||||||
 | 
					        // var audiobookIds = this.audiobooksShowing.map((a) => a.id)
 | 
				
			||||||
 | 
					        // this.$store.commit('setSelectedAudiobooks', audiobookIds)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleBatchRead() {
 | 
					    toggleBatchRead() {
 | 
				
			||||||
@ -205,9 +211,17 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    batchAddToCollectionClick() {
 | 
					    batchAddToCollectionClick() {
 | 
				
			||||||
      this.$store.commit('globals/setShowBatchUserCollectionsModal', true)
 | 
					      this.$store.commit('globals/setShowBatchUserCollectionsModal', true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setBookshelfTotalBooks(totalBooks) {
 | 
				
			||||||
 | 
					      this.totalBooks = totalBooks
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {}
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.$eventBus.$on('bookshelf-total-books', this.setBookshelfTotalBooks)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    this.$eventBus.$off('bookshelf-total-books', this.setBookshelfTotalBooks)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@
 | 
				
			|||||||
      <div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
 | 
					      <div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div v-if="!audiobooks.length" class="w-full flex flex-col items-center justify-center py-12">
 | 
					    <div v-if="loaded && !shelves.length" class="w-full flex flex-col items-center justify-center py-12">
 | 
				
			||||||
      <p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p>
 | 
					      <p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p>
 | 
				
			||||||
      <div class="flex">
 | 
					      <div class="flex">
 | 
				
			||||||
        <ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn>
 | 
					        <ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn>
 | 
				
			||||||
@ -30,89 +30,36 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    search: Boolean,
 | 
				
			||||||
 | 
					    results: {
 | 
				
			||||||
 | 
					      type: Object,
 | 
				
			||||||
 | 
					      default: () => {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
 | 
					      loaded: false,
 | 
				
			||||||
      availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
 | 
					      availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
 | 
				
			||||||
      selectedSizeIndex: 3,
 | 
					      selectedSizeIndex: 3,
 | 
				
			||||||
      rowPaddingX: 40,
 | 
					 | 
				
			||||||
      keywordFilterTimeout: null,
 | 
					      keywordFilterTimeout: null,
 | 
				
			||||||
      scannerParseSubtitle: false,
 | 
					      scannerParseSubtitle: false,
 | 
				
			||||||
      wrapperClientWidth: 0,
 | 
					      wrapperClientWidth: 0,
 | 
				
			||||||
      overflowingShelvesRight: {},
 | 
					      shelves: []
 | 
				
			||||||
      overflowingShelvesLeft: {}
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    showExperimentalFeatures() {
 | 
					    showExperimentalFeatures() {
 | 
				
			||||||
      return this.$store.state.showExperimentalFeatures
 | 
					      return this.$store.state.showExperimentalFeatures
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    userAudiobooks() {
 | 
					    currentLibraryId() {
 | 
				
			||||||
      return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
 | 
					      return this.$store.state.libraries.currentLibraryId
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    audiobooks() {
 | 
					 | 
				
			||||||
      return this.$store.state.audiobooks.audiobooks
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    bookCoverWidth() {
 | 
					    bookCoverWidth() {
 | 
				
			||||||
      return this.availableSizes[this.selectedSizeIndex]
 | 
					      return this.availableSizes[this.selectedSizeIndex]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    sizeMultiplier() {
 | 
					    sizeMultiplier() {
 | 
				
			||||||
      return this.bookCoverWidth / 120
 | 
					      return this.bookCoverWidth / 120
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    paddingX() {
 | 
					 | 
				
			||||||
      return 16 * this.sizeMultiplier
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    bookWidth() {
 | 
					 | 
				
			||||||
      return this.bookCoverWidth + this.paddingX * 2
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mostRecentPlayed() {
 | 
					 | 
				
			||||||
      var audiobooks = this.audiobooks.filter((ab) => this.userAudiobooks[ab.id] && this.userAudiobooks[ab.id].lastUpdate > 0 && this.userAudiobooks[ab.id].progress > 0 && !this.userAudiobooks[ab.id].isRead).map((ab) => ({ ...ab }))
 | 
					 | 
				
			||||||
      audiobooks.sort((a, b) => {
 | 
					 | 
				
			||||||
        return this.userAudiobooks[b.id].lastUpdate - this.userAudiobooks[a.id].lastUpdate
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      return audiobooks.slice(0, 10)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mostRecentAdded() {
 | 
					 | 
				
			||||||
      var audiobooks = this.audiobooks.map((ab) => ({ ...ab })).sort((a, b) => b.addedAt - a.addedAt)
 | 
					 | 
				
			||||||
      return audiobooks.slice(0, 10)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    seriesGroups() {
 | 
					 | 
				
			||||||
      return this.$store.getters['audiobooks/getSeriesGroups']()
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    recentlyUpdatedSeries() {
 | 
					 | 
				
			||||||
      var mostRecentTime = 0
 | 
					 | 
				
			||||||
      var mostRecentSeries = null
 | 
					 | 
				
			||||||
      this.seriesGroups.forEach((series) => {
 | 
					 | 
				
			||||||
        if ((series.books.length && mostRecentSeries === null) || series.lastUpdate > mostRecentTime) {
 | 
					 | 
				
			||||||
          mostRecentTime = series.lastUpdate
 | 
					 | 
				
			||||||
          mostRecentSeries = series
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      if (!mostRecentSeries) return null
 | 
					 | 
				
			||||||
      return mostRecentSeries.books
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    booksRecentlyRead() {
 | 
					 | 
				
			||||||
      var audiobooks = this.audiobooks.filter((ab) => this.userAudiobooks[ab.id] && this.userAudiobooks[ab.id].isRead).map((ab) => ({ ...ab }))
 | 
					 | 
				
			||||||
      audiobooks.sort((a, b) => {
 | 
					 | 
				
			||||||
        return this.userAudiobooks[b.id].finishedAt - this.userAudiobooks[a.id].finishedAt
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      return audiobooks.slice(0, 10)
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    shelves() {
 | 
					 | 
				
			||||||
      var shelves = []
 | 
					 | 
				
			||||||
      if (this.mostRecentPlayed.length) {
 | 
					 | 
				
			||||||
        shelves.push({ books: this.mostRecentPlayed, label: 'Continue Reading' })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      shelves.push({ books: this.mostRecentAdded, label: 'Recently Added' })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.recentlyUpdatedSeries) {
 | 
					 | 
				
			||||||
        shelves.push({ books: this.recentlyUpdatedSeries, label: 'Newest Series' })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.booksRecentlyRead.length) {
 | 
					 | 
				
			||||||
        shelves.push({ books: this.booksRecentlyRead, label: 'Read Again' })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return shelves
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@ -136,10 +83,73 @@ export default {
 | 
				
			|||||||
      var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
 | 
					      var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
 | 
				
			||||||
      if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
 | 
					      if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.$store.dispatch('audiobooks/load')
 | 
					      // await this.$store.dispatch('audiobooks/load')
 | 
				
			||||||
 | 
					      if (this.search) {
 | 
				
			||||||
 | 
					        this.setShelvesFromSearch()
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        var categories = await this.$axios
 | 
				
			||||||
 | 
					          .$get(`/api/libraries/${this.currentLibraryId}/categories`)
 | 
				
			||||||
 | 
					          .then((data) => {
 | 
				
			||||||
 | 
					            console.log('Category data', data)
 | 
				
			||||||
 | 
					            return data
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch((error) => {
 | 
				
			||||||
 | 
					            console.error('Failed to fetch cats', error)
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        this.shelves = categories
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.loaded = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async setShelvesFromSearch() {
 | 
				
			||||||
 | 
					      var shelves = []
 | 
				
			||||||
 | 
					      if (this.results.audiobooks) {
 | 
				
			||||||
 | 
					        shelves.push({
 | 
				
			||||||
 | 
					          id: 'audiobooks',
 | 
				
			||||||
 | 
					          label: 'Books',
 | 
				
			||||||
 | 
					          type: 'books',
 | 
				
			||||||
 | 
					          entities: this.results.audiobooks.map((ab) => ab.audiobook)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.results.authors) {
 | 
				
			||||||
 | 
					        shelves.push({
 | 
				
			||||||
 | 
					          id: 'authors',
 | 
				
			||||||
 | 
					          label: 'Authors',
 | 
				
			||||||
 | 
					          type: 'authors',
 | 
				
			||||||
 | 
					          entities: this.results.authors.map((a) => a.author)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.results.series) {
 | 
				
			||||||
 | 
					        shelves.push({
 | 
				
			||||||
 | 
					          id: 'series',
 | 
				
			||||||
 | 
					          label: 'Series',
 | 
				
			||||||
 | 
					          type: 'series',
 | 
				
			||||||
 | 
					          entities: this.results.series.map((seriesObj) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              name: seriesObj.series,
 | 
				
			||||||
 | 
					              books: seriesObj.audiobooks,
 | 
				
			||||||
 | 
					              type: 'series'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.results.tags) {
 | 
				
			||||||
 | 
					        shelves.push({
 | 
				
			||||||
 | 
					          id: 'tags',
 | 
				
			||||||
 | 
					          label: 'Tags',
 | 
				
			||||||
 | 
					          type: 'tags',
 | 
				
			||||||
 | 
					          entities: this.results.tags.map((tagObj) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              name: tagObj.tag,
 | 
				
			||||||
 | 
					              books: tagObj.audiobooks,
 | 
				
			||||||
 | 
					              type: 'tags'
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.shelves = shelves
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    resize() {},
 | 
					    resize() {},
 | 
				
			||||||
    audiobooksUpdated() {},
 | 
					 | 
				
			||||||
    settingsUpdated(settings) {
 | 
					    settingsUpdated(settings) {
 | 
				
			||||||
      if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
 | 
					      if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
 | 
				
			||||||
        var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
 | 
					        var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
 | 
				
			||||||
@ -154,15 +164,11 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    window.addEventListener('resize', this.resize)
 | 
					 | 
				
			||||||
    this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
 | 
					 | 
				
			||||||
    this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
 | 
					    this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.init()
 | 
					    this.init()
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeDestroy() {
 | 
					  beforeDestroy() {
 | 
				
			||||||
    window.removeEventListener('resize', this.resize)
 | 
					 | 
				
			||||||
    this.$store.commit('audiobooks/removeListener', 'bookshelf')
 | 
					 | 
				
			||||||
    this.$store.commit('user/removeSettingsListener', 'bookshelf')
 | 
					    this.$store.commit('user/removeSettingsListener', 'bookshelf')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,23 @@
 | 
				
			|||||||
  <div class="relative">
 | 
					  <div class="relative">
 | 
				
			||||||
    <div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem' }" @scroll="scrolled">
 | 
					    <div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem' }" @scroll="scrolled">
 | 
				
			||||||
      <div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
 | 
					      <div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
 | 
				
			||||||
        <div v-if="shelf.books" class="flex items-center -mb-2">
 | 
					        <div v-if="shelf.type === 'books'" class="flex items-center -mb-2">
 | 
				
			||||||
          <template v-for="entity in shelf.books">
 | 
					          <template v-for="entity in shelf.entities">
 | 
				
			||||||
            <cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" :padding-y="24" @edit="editBook" />
 | 
					            <cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" :padding-y="24" @edit="editBook" />
 | 
				
			||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div v-if="shelf.type === 'series'" class="flex items-center -mb-2">
 | 
				
			||||||
 | 
					          <template v-for="entity in shelf.entities">
 | 
				
			||||||
 | 
					            <cards-group-card :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div v-if="shelf.type === 'tags'" class="flex items-center -mb-2">
 | 
				
			||||||
 | 
					          <template v-for="entity in shelf.entities">
 | 
				
			||||||
 | 
					            <nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
 | 
				
			||||||
 | 
					              <cards-group-card is-search :width="bookCoverWidth" :group="entity" />
 | 
				
			||||||
 | 
					            </nuxt-link>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
        <div v-else-if="shelf.series" class="flex items-center -mb-2">
 | 
					        <div v-else-if="shelf.series" class="flex items-center -mb-2">
 | 
				
			||||||
          <template v-for="entity in shelf.series">
 | 
					          <template v-for="entity in shelf.series">
 | 
				
			||||||
            <cards-group-card is-search :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
 | 
					            <cards-group-card is-search :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
 | 
				
			||||||
@ -70,7 +82,7 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    editBook(audiobook) {
 | 
					    editBook(audiobook) {
 | 
				
			||||||
      var bookIds = this.shelf.books.map((e) => e.id)
 | 
					      var bookIds = this.shelf.entities.map((e) => e.id)
 | 
				
			||||||
      this.$store.commit('setBookshelfBookIds', bookIds)
 | 
					      this.$store.commit('setBookshelfBookIds', bookIds)
 | 
				
			||||||
      this.$store.commit('showEditModal', audiobook)
 | 
					      this.$store.commit('showEditModal', audiobook)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -28,7 +28,7 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="flex-grow hidden md:inline-block" />
 | 
					        <div class="flex-grow hidden md:inline-block" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40 hidden md:block" />
 | 
					        <!-- <ui-text-input v-show="showSortFilters" v-model="keywordFilter" @input="keywordFilterInput" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40 hidden md:block" /> -->
 | 
				
			||||||
        <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
 | 
					        <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
 | 
				
			||||||
        <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
 | 
					        <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 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">
 | 
				
			||||||
@ -69,7 +69,10 @@ export default {
 | 
				
			|||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      settings: {},
 | 
					      settings: {},
 | 
				
			||||||
      hasInit: false
 | 
					      hasInit: false,
 | 
				
			||||||
 | 
					      totalEntities: 0,
 | 
				
			||||||
 | 
					      keywordFilter: null,
 | 
				
			||||||
 | 
					      keywordTimeout: null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -80,8 +83,11 @@ export default {
 | 
				
			|||||||
      return this.page === ''
 | 
					      return this.page === ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    numShowing() {
 | 
					    numShowing() {
 | 
				
			||||||
 | 
					      return this.totalEntities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.page === '') {
 | 
					      if (this.page === '') {
 | 
				
			||||||
        return this.$store.getters['audiobooks/getFiltered']().length
 | 
					        // return this.$store.getters['audiobooks/getFiltered']().length
 | 
				
			||||||
 | 
					        return this.totalEntities
 | 
				
			||||||
      } else if (this.page === 'search') {
 | 
					      } else if (this.page === 'search') {
 | 
				
			||||||
        var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
 | 
					        var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
 | 
				
			||||||
        return audiobookSearchResults.length
 | 
					        return audiobookSearchResults.length
 | 
				
			||||||
@ -103,14 +109,14 @@ export default {
 | 
				
			|||||||
      if (this.page === 'collections') return 'Collections'
 | 
					      if (this.page === 'collections') return 'Collections'
 | 
				
			||||||
      return ''
 | 
					      return ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    _keywordFilter: {
 | 
					    // _keywordFilter: {
 | 
				
			||||||
      get() {
 | 
					    //   get() {
 | 
				
			||||||
        return this.$store.state.audiobooks.keywordFilter
 | 
					    //     return this.$store.state.audiobooks.keywordFilter
 | 
				
			||||||
      },
 | 
					    //   },
 | 
				
			||||||
      set(val) {
 | 
					    //   set(val) {
 | 
				
			||||||
        this.$store.commit('audiobooks/setKeywordFilter', val)
 | 
					    //     this.$store.commit('audiobooks/setKeywordFilter', val)
 | 
				
			||||||
      }
 | 
					    //   }
 | 
				
			||||||
    },
 | 
					    // },
 | 
				
			||||||
    paramId() {
 | 
					    paramId() {
 | 
				
			||||||
      return this.$route.params ? this.$route.params.id || '' : ''
 | 
					      return this.$route.params ? this.$route.params.id || '' : ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -151,14 +157,28 @@ export default {
 | 
				
			|||||||
      for (const key in settings) {
 | 
					      for (const key in settings) {
 | 
				
			||||||
        this.settings[key] = settings[key]
 | 
					        this.settings[key] = settings[key]
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setBookshelfTotalEntities(totalEntities) {
 | 
				
			||||||
 | 
					      this.totalEntities = totalEntities
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    keywordFilterInput() {
 | 
				
			||||||
 | 
					      clearTimeout(this.keywordTimeout)
 | 
				
			||||||
 | 
					      this.keywordTimeout = setTimeout(() => {
 | 
				
			||||||
 | 
					        this.keywordUpdated(this.keywordFilter)
 | 
				
			||||||
 | 
					      }, 1000)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    keywordUpdated() {
 | 
				
			||||||
 | 
					      this.$eventBus.$emit('bookshelf-keyword-filter', this.keywordFilter)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    this.init()
 | 
					    this.init()
 | 
				
			||||||
    this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
 | 
					    this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
 | 
				
			||||||
 | 
					    this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeDestroy() {
 | 
					  beforeDestroy() {
 | 
				
			||||||
    this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
 | 
					    this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
 | 
				
			||||||
 | 
					    this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
				
			|||||||
@ -2,47 +2,77 @@
 | 
				
			|||||||
  <div id="bookshelf" class="w-full overflow-y-auto">
 | 
					  <div id="bookshelf" class="w-full overflow-y-auto">
 | 
				
			||||||
    <template v-for="shelf in totalShelves">
 | 
					    <template v-for="shelf in totalShelves">
 | 
				
			||||||
      <div :key="shelf" class="w-full px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
 | 
					      <div :key="shelf" class="w-full px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
 | 
				
			||||||
        <div class="absolute top-0 left-0 bottom-0 p-4 z-10">
 | 
					        <!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10">
 | 
				
			||||||
          <p class="text-white text-2xl">{{ shelf }}</p>
 | 
					          <p class="text-white text-2xl">{{ shelf }}</p>
 | 
				
			||||||
        </div>
 | 
					        </div> -->
 | 
				
			||||||
        <div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="`h-${shelfDividerHeightIndex}`" />
 | 
					        <div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="`h-${shelfDividerHeightIndex}`" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div v-if="!totalShelves && initialized" class="w-full py-16">
 | 
				
			||||||
 | 
					      <p class="text-xl text-center">{{ emptyMessage }}</p>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import Vue from 'vue'
 | 
					import bookshelfCardsHelpers from '@/mixins/bookshelfCardsHelpers'
 | 
				
			||||||
import LazyBookCard from '../cards/LazyBookCard'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    page: String
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mixins: [bookshelfCardsHelpers],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      initialized: false,
 | 
					      initialized: false,
 | 
				
			||||||
      bookshelfHeight: 0,
 | 
					      bookshelfHeight: 0,
 | 
				
			||||||
      bookshelfWidth: 0,
 | 
					      bookshelfWidth: 0,
 | 
				
			||||||
      shelvesPerPage: 0,
 | 
					      shelvesPerPage: 0,
 | 
				
			||||||
      booksPerShelf: 8,
 | 
					      entitiesPerShelf: 8,
 | 
				
			||||||
      currentPage: 0,
 | 
					      currentPage: 0,
 | 
				
			||||||
      totalBooks: 0,
 | 
					      totalEntities: 0,
 | 
				
			||||||
      books: [],
 | 
					      entities: [],
 | 
				
			||||||
      pagesLoaded: {},
 | 
					      pagesLoaded: {},
 | 
				
			||||||
      bookIndexesMounted: [],
 | 
					      entityIndexesMounted: [],
 | 
				
			||||||
      bookComponentRefs: {},
 | 
					      entityComponentRefs: {},
 | 
				
			||||||
      bookWidth: 120,
 | 
					      bookWidth: 120,
 | 
				
			||||||
      pageLoadQueue: [],
 | 
					      pageLoadQueue: [],
 | 
				
			||||||
      isFetchingBooks: false,
 | 
					      isFetchingEntities: false,
 | 
				
			||||||
      scrollTimeout: null,
 | 
					      scrollTimeout: null,
 | 
				
			||||||
      booksPerFetch: 100,
 | 
					      booksPerFetch: 250,
 | 
				
			||||||
      totalShelves: 0,
 | 
					      totalShelves: 0,
 | 
				
			||||||
      bookshelfMarginLeft: 0
 | 
					      bookshelfMarginLeft: 0,
 | 
				
			||||||
 | 
					      isSelectionMode: false,
 | 
				
			||||||
 | 
					      isSelectAll: false,
 | 
				
			||||||
 | 
					      currentSFQueryString: null,
 | 
				
			||||||
 | 
					      pendingReset: false,
 | 
				
			||||||
 | 
					      keywordFilter: null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    sortBy() {
 | 
					    // booksFiltered() {
 | 
				
			||||||
 | 
					    //   const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
 | 
				
			||||||
 | 
					    //   const keyworkFilter = state.keywordFilter.toLowerCase()
 | 
				
			||||||
 | 
					    //   return this.books.filter((ab) => {
 | 
				
			||||||
 | 
					    //     if (!ab.book) return false
 | 
				
			||||||
 | 
					    //     return !!keywordFilterKeys.find((key) => ab.book[key] && ab.book[key].toLowerCase().includes(keyworkFilter))
 | 
				
			||||||
 | 
					    //   })
 | 
				
			||||||
 | 
					    // },
 | 
				
			||||||
 | 
					    emptyMessage() {
 | 
				
			||||||
 | 
					      if (this.page === 'series') return `You have no series`
 | 
				
			||||||
 | 
					      if (this.page === 'collections') return "You haven't made any collections yet"
 | 
				
			||||||
 | 
					      return 'No results'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    entityName() {
 | 
				
			||||||
 | 
					      if (this.page === 'series') return 'series'
 | 
				
			||||||
 | 
					      if (this.page === 'collections') return 'collections'
 | 
				
			||||||
 | 
					      return 'books'
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    orderBy() {
 | 
				
			||||||
      return this.$store.getters['user/getUserSetting']('orderBy')
 | 
					      return this.$store.getters['user/getUserSetting']('orderBy')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    sortDesc() {
 | 
					    orderDesc() {
 | 
				
			||||||
      return this.$store.getters['user/getUserSetting']('orderDesc')
 | 
					      return this.$store.getters['user/getUserSetting']('orderDesc')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    filterBy() {
 | 
					    filterBy() {
 | 
				
			||||||
@ -51,6 +81,11 @@ export default {
 | 
				
			|||||||
    currentLibraryId() {
 | 
					    currentLibraryId() {
 | 
				
			||||||
      return this.$store.state.libraries.currentLibraryId
 | 
					      return this.$store.state.libraries.currentLibraryId
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    entityWidth() {
 | 
				
			||||||
 | 
					      if (this.entityName === 'series') return this.bookWidth * 1.6
 | 
				
			||||||
 | 
					      if (this.entityName === 'collections') return this.bookWidth * 2
 | 
				
			||||||
 | 
					      return this.bookWidth
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    bookHeight() {
 | 
					    bookHeight() {
 | 
				
			||||||
      return this.bookWidth * 1.6
 | 
					      return this.bookWidth * 1.6
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -60,119 +95,133 @@ export default {
 | 
				
			|||||||
    shelfHeight() {
 | 
					    shelfHeight() {
 | 
				
			||||||
      return this.bookHeight + 40
 | 
					      return this.bookHeight + 40
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    totalBookCardWidth() {
 | 
					    totalEntityCardWidth() {
 | 
				
			||||||
      // Includes margin
 | 
					      // Includes margin
 | 
				
			||||||
      return this.bookWidth + 24
 | 
					      return this.entityWidth + 24
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    booksPerPage() {
 | 
					    booksPerPage() {
 | 
				
			||||||
      return this.shelvesPerPage * this.booksPerShelf
 | 
					      return this.shelvesPerPage * this.entitiesPerShelf
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    selectedAudiobooks() {
 | 
				
			||||||
 | 
					      return this.$store.state.selectedAudiobooks || []
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    async fetchBooks(page = 0) {
 | 
					    editEntity(entity) {
 | 
				
			||||||
 | 
					      if (this.entityName === 'books') {
 | 
				
			||||||
 | 
					        var bookIds = this.entities.map((e) => e.id)
 | 
				
			||||||
 | 
					        this.$store.commit('setBookshelfBookIds', bookIds)
 | 
				
			||||||
 | 
					        this.$store.commit('showEditModal', entity)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clearSelectedBooks() {
 | 
				
			||||||
 | 
					      this.updateBookSelectionMode(false)
 | 
				
			||||||
 | 
					      this.isSelectionMode = false
 | 
				
			||||||
 | 
					      this.isSelectAll = false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    selectAllBooks() {
 | 
				
			||||||
 | 
					      this.isSelectAll = true
 | 
				
			||||||
 | 
					      for (const key in this.entityComponentRefs) {
 | 
				
			||||||
 | 
					        if (this.entityIndexesMounted.includes(Number(key))) {
 | 
				
			||||||
 | 
					          this.entityComponentRefs[key].selected = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    selectEntity(entity) {
 | 
				
			||||||
 | 
					      if (this.entityName === 'books') {
 | 
				
			||||||
 | 
					        this.$store.commit('toggleAudiobookSelected', entity.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var newIsSelectionMode = !!this.selectedAudiobooks.length
 | 
				
			||||||
 | 
					        if (this.isSelectionMode !== newIsSelectionMode) {
 | 
				
			||||||
 | 
					          this.isSelectionMode = newIsSelectionMode
 | 
				
			||||||
 | 
					          this.updateBookSelectionMode(newIsSelectionMode)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    updateBookSelectionMode(isSelectionMode) {
 | 
				
			||||||
 | 
					      for (const key in this.entityComponentRefs) {
 | 
				
			||||||
 | 
					        if (this.entityIndexesMounted.includes(Number(key))) {
 | 
				
			||||||
 | 
					          this.entityComponentRefs[key].setSelectionMode(isSelectionMode)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async fetchEntites(page = 0) {
 | 
				
			||||||
      var startIndex = page * this.booksPerFetch
 | 
					      var startIndex = page * this.booksPerFetch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.isFetchingBooks = true
 | 
					      this.isFetchingEntities = true
 | 
				
			||||||
      var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/books/all?sort=${this.sortBy}&desc=${this.sortDesc}&filter=${this.filterBy}&limit=${this.booksPerFetch}&page=${page}`).catch((error) => {
 | 
					
 | 
				
			||||||
 | 
					      if (!this.initialized) {
 | 
				
			||||||
 | 
					        this.currentSFQueryString = this.buildSearchParams()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
 | 
				
			||||||
 | 
					      var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
 | 
				
			||||||
 | 
					      var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}?${sfQueryString}limit=${this.booksPerFetch}&page=${page}`).catch((error) => {
 | 
				
			||||||
        console.error('failed to fetch books', error)
 | 
					        console.error('failed to fetch books', error)
 | 
				
			||||||
        return null
 | 
					        return null
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					      this.isFetchingEntities = false
 | 
				
			||||||
 | 
					      if (this.pendingReset) {
 | 
				
			||||||
 | 
					        this.pendingReset = false
 | 
				
			||||||
 | 
					        this.resetEntities()
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (payload) {
 | 
					      if (payload) {
 | 
				
			||||||
        console.log('Received payload with start index', startIndex, 'pages loaded', this.pagesLoaded)
 | 
					        console.log('Received payload', payload)
 | 
				
			||||||
        if (!this.initialized) {
 | 
					        if (!this.initialized) {
 | 
				
			||||||
          this.initialized = true
 | 
					          this.initialized = true
 | 
				
			||||||
          this.totalBooks = payload.total
 | 
					          this.totalEntities = payload.total
 | 
				
			||||||
          this.totalShelves = Math.ceil(this.totalBooks / this.booksPerShelf)
 | 
					          this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
 | 
				
			||||||
          this.books = new Array(this.totalBooks)
 | 
					          this.entities = new Array(this.totalEntities)
 | 
				
			||||||
 | 
					          this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (let i = 0; i < payload.results.length; i++) {
 | 
					        for (let i = 0; i < payload.results.length; i++) {
 | 
				
			||||||
          var bookIndex = i + startIndex
 | 
					          var index = i + startIndex
 | 
				
			||||||
          this.books[bookIndex] = payload.results[i]
 | 
					          this.entities[index] = payload.results[i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (this.bookComponentRefs[bookIndex]) {
 | 
					          if (this.entityComponentRefs[index]) {
 | 
				
			||||||
            this.bookComponentRefs[bookIndex].setBook(this.books[bookIndex])
 | 
					            this.entityComponentRefs[index].setEntity(this.entities[index])
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    loadPage(page) {
 | 
					    loadPage(page) {
 | 
				
			||||||
      this.pagesLoaded[page] = true
 | 
					      this.pagesLoaded[page] = true
 | 
				
			||||||
      this.fetchBooks(page)
 | 
					      this.fetchEntites(page)
 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async mountBookCard(index) {
 | 
					 | 
				
			||||||
      var shelf = Math.floor(index / this.booksPerShelf)
 | 
					 | 
				
			||||||
      var shelfEl = document.getElementById(`shelf-${shelf}`)
 | 
					 | 
				
			||||||
      if (!shelfEl) {
 | 
					 | 
				
			||||||
        console.error('invalid shelf', shelf)
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.bookIndexesMounted.push(index)
 | 
					 | 
				
			||||||
      if (this.bookComponentRefs[index] && !this.bookIndexesMounted.includes(index)) {
 | 
					 | 
				
			||||||
        shelfEl.appendChild(this.bookComponentRefs[index].$el)
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var shelfOffsetY = 16
 | 
					 | 
				
			||||||
      var row = index % this.booksPerShelf
 | 
					 | 
				
			||||||
      var shelfOffsetX = row * this.totalBookCardWidth + this.bookshelfMarginLeft
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var ComponentClass = Vue.extend(LazyBookCard)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      var _this = this
 | 
					 | 
				
			||||||
      var instance = new ComponentClass({
 | 
					 | 
				
			||||||
        propsData: {
 | 
					 | 
				
			||||||
          index: index,
 | 
					 | 
				
			||||||
          bookWidth: this.bookWidth
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        created() {
 | 
					 | 
				
			||||||
          // this.$on('action', (func) => {
 | 
					 | 
				
			||||||
          //   if (_this[func]) _this[func]()
 | 
					 | 
				
			||||||
          // })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      this.bookComponentRefs[index] = instance
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      instance.$mount()
 | 
					 | 
				
			||||||
      instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
 | 
					 | 
				
			||||||
      shelfEl.appendChild(instance.$el)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.books[index]) {
 | 
					 | 
				
			||||||
        instance.setBook(this.books[index])
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showHideBookPlaceholder(index, show) {
 | 
					    showHideBookPlaceholder(index, show) {
 | 
				
			||||||
      var el = document.getElementById(`book-${index}-placeholder`)
 | 
					      var el = document.getElementById(`book-${index}-placeholder`)
 | 
				
			||||||
      if (el) el.style.display = show ? 'flex' : 'none'
 | 
					      if (el) el.style.display = show ? 'flex' : 'none'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    unmountBookCard(index) {
 | 
					    mountEntites(fromIndex, toIndex) {
 | 
				
			||||||
      if (this.bookComponentRefs[index]) {
 | 
					 | 
				
			||||||
        this.bookComponentRefs[index].detach()
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mountBooks(fromIndex, toIndex) {
 | 
					 | 
				
			||||||
      for (let i = fromIndex; i < toIndex; i++) {
 | 
					      for (let i = fromIndex; i < toIndex; i++) {
 | 
				
			||||||
        this.mountBookCard(i)
 | 
					        if (!this.entityIndexesMounted.includes(i)) {
 | 
				
			||||||
 | 
					          this.cardsHelpers.mountEntityCard(i)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    handleScroll(scrollTop) {
 | 
					    handleScroll(scrollTop) {
 | 
				
			||||||
      var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
 | 
					      var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
 | 
				
			||||||
      var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
 | 
					      var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
 | 
				
			||||||
 | 
					      lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var topShelfPage = Math.floor(firstShelfIndex / this.shelvesPerPage)
 | 
					      var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
 | 
				
			||||||
      var bottomShelfPage = Math.floor(lastShelfIndex / this.shelvesPerPage)
 | 
					      var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
 | 
				
			||||||
      if (!this.pagesLoaded[topShelfPage]) {
 | 
					      lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
 | 
				
			||||||
        this.loadPage(topShelfPage)
 | 
					
 | 
				
			||||||
 | 
					      var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
 | 
				
			||||||
 | 
					      var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
 | 
				
			||||||
 | 
					      if (!this.pagesLoaded[firstBookPage]) {
 | 
				
			||||||
 | 
					        console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
 | 
				
			||||||
 | 
					        this.loadPage(firstBookPage)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (!this.pagesLoaded[bottomShelfPage]) {
 | 
					      if (!this.pagesLoaded[lastBookPage]) {
 | 
				
			||||||
        this.loadPage(bottomShelfPage)
 | 
					        console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
 | 
				
			||||||
 | 
					        this.loadPage(lastBookPage)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      console.log('Shelves in view', firstShelfIndex, 'to', lastShelfIndex)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var firstBookIndex = firstShelfIndex * this.booksPerShelf
 | 
					      this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
 | 
				
			||||||
      var lastBookIndex = lastShelfIndex * this.booksPerShelf + this.booksPerShelf
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this.bookIndexesMounted = this.bookIndexesMounted.filter((_index) => {
 | 
					 | 
				
			||||||
        if (_index < firstBookIndex || _index >= lastBookIndex) {
 | 
					        if (_index < firstBookIndex || _index >= lastBookIndex) {
 | 
				
			||||||
          var el = document.getElementById(`book-card-${_index}`)
 | 
					          var el = document.getElementById(`book-card-${_index}`)
 | 
				
			||||||
          if (el) el.remove()
 | 
					          if (el) el.remove()
 | 
				
			||||||
@ -180,7 +229,68 @@ export default {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return true
 | 
					        return true
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      this.mountBooks(firstBookIndex, lastBookIndex)
 | 
					      this.mountEntites(firstBookIndex, lastBookIndex)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async resetEntities() {
 | 
				
			||||||
 | 
					      if (this.isFetchingEntities) {
 | 
				
			||||||
 | 
					        console.warn('RESET BOOKS BUT ALREADY FETCHING, WAIT FOR FETCH')
 | 
				
			||||||
 | 
					        this.pendingReset = true
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.destroyEntityComponents()
 | 
				
			||||||
 | 
					      this.entityIndexesMounted = []
 | 
				
			||||||
 | 
					      this.entityComponentRefs = {}
 | 
				
			||||||
 | 
					      this.pagesLoaded = {}
 | 
				
			||||||
 | 
					      this.entities = []
 | 
				
			||||||
 | 
					      this.totalShelves = 0
 | 
				
			||||||
 | 
					      this.totalEntities = 0
 | 
				
			||||||
 | 
					      this.currentPage = 0
 | 
				
			||||||
 | 
					      this.isSelectionMode = false
 | 
				
			||||||
 | 
					      this.isSelectAll = false
 | 
				
			||||||
 | 
					      this.initialized = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.pagesLoaded[0] = true
 | 
				
			||||||
 | 
					      await this.fetchEntites(0)
 | 
				
			||||||
 | 
					      var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
 | 
				
			||||||
 | 
					      this.mountEntites(0, lastBookIndex)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    buildSearchParams() {
 | 
				
			||||||
 | 
					      if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
 | 
				
			||||||
 | 
					        return ''
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let searchParams = new URLSearchParams()
 | 
				
			||||||
 | 
					      if (this.filterBy && this.filterBy !== 'all') {
 | 
				
			||||||
 | 
					        searchParams.set('filter', this.filterBy)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.orderBy) {
 | 
				
			||||||
 | 
					        searchParams.set('sort', this.orderBy)
 | 
				
			||||||
 | 
					        searchParams.set('desc', this.orderDesc ? 1 : 0)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return searchParams.toString()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    checkUpdateSearchParams() {
 | 
				
			||||||
 | 
					      var newSearchParams = this.buildSearchParams()
 | 
				
			||||||
 | 
					      var currentQueryString = window.location.search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (newSearchParams === '') {
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
 | 
				
			||||||
 | 
					        let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
 | 
				
			||||||
 | 
					        window.history.replaceState({ path: newurl }, '', newurl)
 | 
				
			||||||
 | 
					        return true
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    settingsUpdated(settings) {
 | 
				
			||||||
 | 
					      var wasUpdated = this.checkUpdateSearchParams()
 | 
				
			||||||
 | 
					      if (wasUpdated) {
 | 
				
			||||||
 | 
					        this.resetEntities()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    scroll(e) {
 | 
					    scroll(e) {
 | 
				
			||||||
      if (!e || !e.target) return
 | 
					      if (!e || !e.target) return
 | 
				
			||||||
@ -191,32 +301,57 @@ export default {
 | 
				
			|||||||
      // }, 250)
 | 
					      // }, 250)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async init(bookshelf) {
 | 
					    async init(bookshelf) {
 | 
				
			||||||
 | 
					      this.checkUpdateSearchParams()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      var { clientHeight, clientWidth } = bookshelf
 | 
					      var { clientHeight, clientWidth } = bookshelf
 | 
				
			||||||
      this.bookshelfHeight = clientHeight
 | 
					      this.bookshelfHeight = clientHeight
 | 
				
			||||||
      this.bookshelfWidth = clientWidth
 | 
					      this.bookshelfWidth = clientWidth
 | 
				
			||||||
      this.booksPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalBookCardWidth)
 | 
					      this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalEntityCardWidth)
 | 
				
			||||||
      this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
 | 
					      this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
 | 
				
			||||||
      this.bookshelfMarginLeft = (this.bookshelfWidth - this.booksPerShelf * this.totalBookCardWidth) / 2
 | 
					      this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
 | 
				
			||||||
      console.log('Shelves per page', this.shelvesPerPage, 'books per page =', this.booksPerShelf * this.shelvesPerPage)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.pagesLoaded[0] = true
 | 
					      this.pagesLoaded[0] = true
 | 
				
			||||||
      await this.fetchBooks(0)
 | 
					      await this.fetchEntites(0)
 | 
				
			||||||
      var lastBookIndex = this.shelvesPerPage * this.booksPerShelf
 | 
					      var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
 | 
				
			||||||
      this.mountBooks(0, lastBookIndex)
 | 
					      this.mountEntites(0, lastBookIndex)
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  mounted() {
 | 
					    initListeners() {
 | 
				
			||||||
      var bookshelf = document.getElementById('bookshelf')
 | 
					      var bookshelf = document.getElementById('bookshelf')
 | 
				
			||||||
      if (bookshelf) {
 | 
					      if (bookshelf) {
 | 
				
			||||||
        this.init(bookshelf)
 | 
					        this.init(bookshelf)
 | 
				
			||||||
        bookshelf.addEventListener('scroll', this.scroll)
 | 
					        bookshelf.addEventListener('scroll', this.scroll)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedBooks)
 | 
				
			||||||
 | 
					      this.$eventBus.$on('bookshelf-select-all', this.selectAllBooks)
 | 
				
			||||||
 | 
					      this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  beforeDestroy() {
 | 
					    removeListeners() {
 | 
				
			||||||
      var bookshelf = document.getElementById('bookshelf')
 | 
					      var bookshelf = document.getElementById('bookshelf')
 | 
				
			||||||
      if (bookshelf) {
 | 
					      if (bookshelf) {
 | 
				
			||||||
        bookshelf.removeEventListener('scroll', this.scroll)
 | 
					        bookshelf.removeEventListener('scroll', this.scroll)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedBooks)
 | 
				
			||||||
 | 
					      this.$eventBus.$off('bookshelf-select-all', this.selectAllBooks)
 | 
				
			||||||
 | 
					      this.$eventBus.$off('bookshelf-keyword-filter', this.updateKeywordFilter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    destroyEntityComponents() {
 | 
				
			||||||
 | 
					      for (const key in this.entityComponentRefs) {
 | 
				
			||||||
 | 
					        if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
 | 
				
			||||||
 | 
					          this.entityComponentRefs[key].destroy()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.initListeners()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  beforeDestroy() {
 | 
				
			||||||
 | 
					    this.destroyEntityComponents()
 | 
				
			||||||
 | 
					    this.removeListeners()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@
 | 
				
			|||||||
            <covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
 | 
					            <covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <!-- Hidden SM and DOWN -->
 | 
					            <!-- Hidden SM and DOWN -->
 | 
				
			||||||
            <div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
 | 
					            <div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block z-20" :class="overlayWrapperClasslist">
 | 
				
			||||||
              <div v-show="showPlayButton" class="h-full flex items-center justify-center">
 | 
					              <div v-show="showPlayButton" class="h-full flex items-center justify-center">
 | 
				
			||||||
                <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
 | 
					                <div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
 | 
				
			||||||
                  <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
 | 
					                  <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
 | 
				
			||||||
@ -63,7 +63,7 @@
 | 
				
			|||||||
              <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
 | 
					              <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
					            <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
 | 
					            <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
 | 
				
			||||||
              <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
 | 
					              <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,43 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div ref="card" :id="`book-card-${index}`" :style="{ width: bookWidth + 'px', height: bookHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-20">
 | 
					  <div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', height: bookHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
 | 
				
			||||||
    <div class="w-full h-full bg-primary relative rounded-sm">
 | 
					    <div class="w-full h-full bg-primary relative rounded overflow-hidden">
 | 
				
			||||||
      <div class="absolute top-0 left-0 w-full flex items-center justify-center">
 | 
					      <div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
 | 
				
			||||||
        <p>{{ title }}/{{ index }}</p>
 | 
					        <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <img v-show="audiobook" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
 | 
				
			||||||
 | 
					      <!-- <covers-book-cover v-show="audiobook" :audiobook="audiobook" :width="width" /> -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div v-show="audiobook && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded" :class="overlayWrapperClasslist">
 | 
				
			||||||
 | 
					      <div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
 | 
				
			||||||
 | 
					        <div class="hover:text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
 | 
				
			||||||
 | 
					          <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
 | 
				
			||||||
 | 
					        <div class="hover:text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
 | 
				
			||||||
 | 
					          <span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
 | 
				
			||||||
 | 
					        <span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
 | 
				
			||||||
 | 
					        <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
 | 
				
			||||||
 | 
					        <span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <img v-show="audiobook" :src="bookCoverSrc" class="w-full h-full object-contain" />
 | 
					 | 
				
			||||||
      <!-- <covers-book-cover v-show="audiobook" :audiobook="audiobook" :width="bookWidth" /> -->
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- <div ref="overlay-wrapper" class="w-full h-full relative box-shadow-book cursor-pointer" @click="clickCard" @mouseover="mouseover" @mouseleave="isHovering = false">
 | 
					    <!-- <div ref="overlay-wrapper" class="w-full h-full relative box-shadow-book cursor-pointer" @click="clickCard" @mouseover="mouseover" @mouseleave="isHovering = false">
 | 
				
			||||||
      <covers-book-cover :audiobook="audiobook" :width="bookWidth" />
 | 
					      <covers-book-cover :audiobook="audiobook" :width="width" />
 | 
				
			||||||
      <div v-if="false" ref="overlay">
 | 
					      <div v-if="false" ref="overlay">
 | 
				
			||||||
        <div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block z-20" :class="overlayWrapperClasslist">
 | 
					        <div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block z-20" :class="overlayWrapperClasslist">
 | 
				
			||||||
          <div v-show="showPlayButton" class="h-full flex items-center justify-center">
 | 
					          <div v-show="showPlayButton" class="h-full flex items-center justify-center">
 | 
				
			||||||
@ -47,7 +75,7 @@
 | 
				
			|||||||
          <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
 | 
					          <span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: bookWidth * userProgressPercent + 'px' }"></div>
 | 
					        <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
 | 
					        <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
 | 
				
			||||||
          <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
 | 
					          <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
 | 
				
			||||||
@ -60,10 +88,13 @@
 | 
				
			|||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import Vue from 'vue'
 | 
				
			||||||
 | 
					import MoreMenu from '@/components/widgets/MoreMenu'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    index: Number,
 | 
					    index: Number,
 | 
				
			||||||
    bookWidth: {
 | 
					    width: {
 | 
				
			||||||
      type: Number,
 | 
					      type: Number,
 | 
				
			||||||
      default: 120
 | 
					      default: 120
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -75,7 +106,11 @@ export default {
 | 
				
			|||||||
      isMoreMenuOpen: false,
 | 
					      isMoreMenuOpen: false,
 | 
				
			||||||
      isProcessingReadUpdate: false,
 | 
					      isProcessingReadUpdate: false,
 | 
				
			||||||
      overlayEl: null,
 | 
					      overlayEl: null,
 | 
				
			||||||
      audiobook: null
 | 
					      audiobook: null,
 | 
				
			||||||
 | 
					      imageReady: false,
 | 
				
			||||||
 | 
					      rescanning: false,
 | 
				
			||||||
 | 
					      selected: false,
 | 
				
			||||||
 | 
					      isSelectionMode: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -94,15 +129,15 @@ export default {
 | 
				
			|||||||
    hasTracks() {
 | 
					    hasTracks() {
 | 
				
			||||||
      return this._audiobook.numTracks
 | 
					      return this._audiobook.numTracks
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isSelectionMode() {
 | 
					    // isSelectionMode() {
 | 
				
			||||||
      return !!this.selectedAudiobooks.length
 | 
					    //   return !!this.selectedAudiobooks.length
 | 
				
			||||||
    },
 | 
					    // },
 | 
				
			||||||
    selectedAudiobooks() {
 | 
					    // selectedAudiobooks() {
 | 
				
			||||||
      return this.store.state.selectedAudiobooks
 | 
					    //   return this.store.state.selectedAudiobooks
 | 
				
			||||||
    },
 | 
					    // },
 | 
				
			||||||
    selected() {
 | 
					    // selected() {
 | 
				
			||||||
      return this.store.getters['getIsAudiobookSelected'](this.audiobookId)
 | 
					    //   return this.store.getters['getIsAudiobookSelected'](this.audiobookId)
 | 
				
			||||||
    },
 | 
					    // },
 | 
				
			||||||
    processingBatch() {
 | 
					    processingBatch() {
 | 
				
			||||||
      return this.store.state.processingBatch
 | 
					      return this.store.state.processingBatch
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -110,13 +145,13 @@ export default {
 | 
				
			|||||||
      return this._audiobook.book || {}
 | 
					      return this._audiobook.book || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    bookHeight() {
 | 
					    bookHeight() {
 | 
				
			||||||
      return this.bookWidth * 1.6
 | 
					      return this.width * 1.6
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    sizeMultiplier() {
 | 
					    sizeMultiplier() {
 | 
				
			||||||
      return this.bookWidth / 120
 | 
					      return this.width / 120
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    title() {
 | 
					    title() {
 | 
				
			||||||
      return this.book.title
 | 
					      return this.book.title || ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    playIconFontSize() {
 | 
					    playIconFontSize() {
 | 
				
			||||||
      return Math.max(2, 3 * this.sizeMultiplier)
 | 
					      return Math.max(2, 3 * this.sizeMultiplier)
 | 
				
			||||||
@ -160,16 +195,16 @@ export default {
 | 
				
			|||||||
      return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
 | 
					      return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isMissing() {
 | 
					    isMissing() {
 | 
				
			||||||
      return this.audiobook.isMissing
 | 
					      return this._audiobook.isMissing
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isIncomplete() {
 | 
					    isIncomplete() {
 | 
				
			||||||
      return this.audiobook.isIncomplete
 | 
					      return this._audiobook.isIncomplete
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    hasMissingParts() {
 | 
					    hasMissingParts() {
 | 
				
			||||||
      return this.audiobook.hasMissingParts
 | 
					      return this._audiobook.hasMissingParts
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    hasInvalidParts() {
 | 
					    hasInvalidParts() {
 | 
				
			||||||
      return this.audiobook.hasInvalidParts
 | 
					      return this._audiobook.hasInvalidParts
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    errorText() {
 | 
					    errorText() {
 | 
				
			||||||
      if (this.isMissing) return 'Audiobook directory is missing!'
 | 
					      if (this.isMissing) return 'Audiobook directory is missing!'
 | 
				
			||||||
@ -247,7 +282,11 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    setBook(audiobook) {
 | 
					    setSelectionMode(val) {
 | 
				
			||||||
 | 
					      this.isSelectionMode = val
 | 
				
			||||||
 | 
					      if (!val) this.selected = false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setEntity(audiobook) {
 | 
				
			||||||
      this.audiobook = audiobook
 | 
					      this.audiobook = audiobook
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    clickCard(e) {
 | 
					    clickCard(e) {
 | 
				
			||||||
@ -255,14 +294,125 @@ export default {
 | 
				
			|||||||
        e.stopPropagation()
 | 
					        e.stopPropagation()
 | 
				
			||||||
        e.preventDefault()
 | 
					        e.preventDefault()
 | 
				
			||||||
        this.selectBtnClick()
 | 
					        this.selectBtnClick()
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        var router = this.$router || this.$nuxt.$router
 | 
				
			||||||
 | 
					        if (router) router.push(`/audiobook/${this.audiobookId}`)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    clickShowMore() {},
 | 
					    editClick() {
 | 
				
			||||||
 | 
					      this.$emit('edit', this.audiobook)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    toggleRead() {
 | 
				
			||||||
 | 
					      // More menu func
 | 
				
			||||||
 | 
					      var updatePayload = {
 | 
				
			||||||
 | 
					        isRead: !this.userIsRead
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.isProcessingReadUpdate = true
 | 
				
			||||||
 | 
					      var toast = this.$toast || this.$nuxt.$toast
 | 
				
			||||||
 | 
					      var axios = this.$axios || this.$nuxt.$axios
 | 
				
			||||||
 | 
					      axios
 | 
				
			||||||
 | 
					        .$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          this.isProcessingReadUpdate = false
 | 
				
			||||||
 | 
					          toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed', error)
 | 
				
			||||||
 | 
					          this.isProcessingReadUpdate = false
 | 
				
			||||||
 | 
					          toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    audiobookScanComplete(result) {
 | 
				
			||||||
 | 
					      this.rescanning = false
 | 
				
			||||||
 | 
					      var toast = this.$toast || this.$nuxt.$toast
 | 
				
			||||||
 | 
					      if (!result) {
 | 
				
			||||||
 | 
					        toast.error(`Re-Scan Failed for "${this.title}"`)
 | 
				
			||||||
 | 
					      } else if (result === 'UPDATED') {
 | 
				
			||||||
 | 
					        toast.success(`Re-Scan complete audiobook was updated`)
 | 
				
			||||||
 | 
					      } else if (result === 'UPTODATE') {
 | 
				
			||||||
 | 
					        toast.success(`Re-Scan complete audiobook was up to date`)
 | 
				
			||||||
 | 
					      } else if (result === 'REMOVED') {
 | 
				
			||||||
 | 
					        toast.error(`Re-Scan complete audiobook was removed`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    rescan() {
 | 
				
			||||||
 | 
					      var socket = this.$root.socket || this.$nuxt.$root.socket
 | 
				
			||||||
 | 
					      this.rescanning = true
 | 
				
			||||||
 | 
					      socket.once('audiobook_scan_complete', this.audiobookScanComplete)
 | 
				
			||||||
 | 
					      socket.emit('scan_audiobook', this.audiobookId)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showEditModalTracks() {
 | 
				
			||||||
 | 
					      // More menu func
 | 
				
			||||||
 | 
					      this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'tracks' })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showEditModalMatch() {
 | 
				
			||||||
 | 
					      // More menu func
 | 
				
			||||||
 | 
					      this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'match' })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showEditModalDownload() {
 | 
				
			||||||
 | 
					      // More menu func
 | 
				
			||||||
 | 
					      this.store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    openCollections() {
 | 
				
			||||||
 | 
					      this.store.commit('setSelectedAudiobook', this.audiobook)
 | 
				
			||||||
 | 
					      this.store.commit('globals/setShowUserCollectionsModal', true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createMoreMenu() {
 | 
				
			||||||
 | 
					      if (!this.$refs.moreIcon) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var ComponentClass = Vue.extend(MoreMenu)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var _this = this
 | 
				
			||||||
 | 
					      var instance = new ComponentClass({
 | 
				
			||||||
 | 
					        propsData: {
 | 
				
			||||||
 | 
					          items: this.moreMenuItems
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        created() {
 | 
				
			||||||
 | 
					          this.$on('action', (func) => {
 | 
				
			||||||
 | 
					            if (_this[func]) _this[func]()
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          this.$on('close', () => {
 | 
				
			||||||
 | 
					            _this.isMoreMenuOpen = false
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      instance.$mount()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
 | 
				
			||||||
 | 
					      var el = instance.$el
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var elHeight = this.moreMenuItems.length * 28 + 2
 | 
				
			||||||
 | 
					      var elWidth = 130
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var bottomOfIcon = wrapperBox.top + wrapperBox.height
 | 
				
			||||||
 | 
					      var rightOfIcon = wrapperBox.left + wrapperBox.width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var elTop = bottomOfIcon
 | 
				
			||||||
 | 
					      var elLeft = rightOfIcon
 | 
				
			||||||
 | 
					      if (bottomOfIcon + elHeight > window.innerHeight - 100) {
 | 
				
			||||||
 | 
					        elTop = wrapperBox.top - elHeight
 | 
				
			||||||
 | 
					        elLeft = wrapperBox.left
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (rightOfIcon + elWidth > window.innerWidth - 100) {
 | 
				
			||||||
 | 
					        elLeft = rightOfIcon - elWidth
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      el.style.top = elTop + 'px'
 | 
				
			||||||
 | 
					      el.style.left = elLeft + 'px'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.isMoreMenuOpen = true
 | 
				
			||||||
 | 
					      document.body.appendChild(el)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clickShowMore() {
 | 
				
			||||||
 | 
					      this.createMoreMenu()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    clickReadEBook() {},
 | 
					    clickReadEBook() {},
 | 
				
			||||||
    editBtnClick() {},
 | 
					    editBtnClick() {},
 | 
				
			||||||
    selectBtnClick() {
 | 
					    selectBtnClick() {
 | 
				
			||||||
      if (this.processingBatch) return
 | 
					      if (this.processingBatch) return
 | 
				
			||||||
      this.store.commit('toggleAudiobookSelected', this.audiobookId)
 | 
					      this.selected = !this.selected
 | 
				
			||||||
 | 
					      this.$emit('select', this.audiobook)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    play() {},
 | 
					    play() {},
 | 
				
			||||||
    detach() {
 | 
					    detach() {
 | 
				
			||||||
@ -286,15 +436,22 @@ export default {
 | 
				
			|||||||
    mouseover() {
 | 
					    mouseover() {
 | 
				
			||||||
      this.isHovering = true
 | 
					      this.isHovering = true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    // mouseleave() {
 | 
					    mouseleave() {
 | 
				
			||||||
    //   this.isHovering = false
 | 
					      this.isHovering = false
 | 
				
			||||||
    // },
 | 
					    },
 | 
				
			||||||
    destroy() {
 | 
					    destroy() {
 | 
				
			||||||
      // destroy the vue listeners, etc
 | 
					      // destroy the vue listeners, etc
 | 
				
			||||||
      this.$destroy()
 | 
					      this.$destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // remove the element from the DOM
 | 
					      // remove the element from the DOM
 | 
				
			||||||
 | 
					      if (this.$el && this.$el.parentNode) {
 | 
				
			||||||
        this.$el.parentNode.removeChild(this.$el)
 | 
					        this.$el.parentNode.removeChild(this.$el)
 | 
				
			||||||
 | 
					      } else if (this.$el && this.$el.remove) {
 | 
				
			||||||
 | 
					        this.$el.remove()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    imageLoaded() {
 | 
				
			||||||
 | 
					      this.imageReady = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {}
 | 
					  mounted() {}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										87
									
								
								client/components/cards/LazyCollectionCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								client/components/cards/LazyCollectionCard.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div ref="card" :id="`collection-card-${index}`" :style="{ width: width + 'px', height: cardHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
 | 
				
			||||||
 | 
					    <div class="w-full h-full bg-primary relative rounded overflow-hidden">
 | 
				
			||||||
 | 
					      <covers-collection-cover ref="cover" :book-items="books" :width="width" :height="width" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
 | 
				
			||||||
 | 
					    </div> -->
 | 
				
			||||||
 | 
					    <div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
 | 
				
			||||||
 | 
					      <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
 | 
				
			||||||
 | 
					        <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    index: Number,
 | 
				
			||||||
 | 
					    width: Number
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      collection: null,
 | 
				
			||||||
 | 
					      isSelectionMode: false,
 | 
				
			||||||
 | 
					      selected: false,
 | 
				
			||||||
 | 
					      isHovering: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    labelFontSize() {
 | 
				
			||||||
 | 
					      if (this.width < 160) return 0.75
 | 
				
			||||||
 | 
					      return 0.875
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cardHeight() {
 | 
				
			||||||
 | 
					      return (this.width / 2) * 1.6
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    sizeMultiplier() {
 | 
				
			||||||
 | 
					      return this.width / 120
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    title() {
 | 
				
			||||||
 | 
					      return this.collection ? this.collection.name : ''
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    books() {
 | 
				
			||||||
 | 
					      return this.collection ? this.collection.books || [] : []
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    store() {
 | 
				
			||||||
 | 
					      return this.$store || this.$nuxt.$store
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    currentLibraryId() {
 | 
				
			||||||
 | 
					      return this.store.state.libraries.currentLibraryId
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    setEntity(_collection) {
 | 
				
			||||||
 | 
					      this.collection = _collection
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setSelectionMode(val) {
 | 
				
			||||||
 | 
					      this.isSelectionMode = val
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mouseover() {
 | 
				
			||||||
 | 
					      this.isHovering = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mouseleave() {
 | 
				
			||||||
 | 
					      this.isHovering = false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clickCard() {
 | 
				
			||||||
 | 
					      if (!this.collection) return
 | 
				
			||||||
 | 
					      var router = this.$router || this.$nuxt.$router
 | 
				
			||||||
 | 
					      router.push(`/collection/${this.collection.id}`)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    destroy() {
 | 
				
			||||||
 | 
					      // destroy the vue listeners, etc
 | 
				
			||||||
 | 
					      this.$destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // remove the element from the DOM
 | 
				
			||||||
 | 
					      if (this.$el && this.$el.parentNode) {
 | 
				
			||||||
 | 
					        this.$el.parentNode.removeChild(this.$el)
 | 
				
			||||||
 | 
					      } else if (this.$el && this.$el.remove) {
 | 
				
			||||||
 | 
					        this.$el.remove()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										102
									
								
								client/components/cards/LazySeriesCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								client/components/cards/LazySeriesCard.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: cardHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
 | 
				
			||||||
 | 
					    <div class="w-full h-full bg-primary relative rounded overflow-hidden">
 | 
				
			||||||
 | 
					      <covers-group-cover ref="cover" :name="title" :book-items="books" :width="width" :height="width" />
 | 
				
			||||||
 | 
					      <!-- <div v-show="series && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
 | 
				
			||||||
 | 
					        <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <img v-show="series" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> -->
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
 | 
				
			||||||
 | 
					    </div> -->
 | 
				
			||||||
 | 
					    <div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
 | 
				
			||||||
 | 
					      <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
 | 
				
			||||||
 | 
					        <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    index: Number,
 | 
				
			||||||
 | 
					    width: Number
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      series: null,
 | 
				
			||||||
 | 
					      isSelectionMode: false,
 | 
				
			||||||
 | 
					      selected: false,
 | 
				
			||||||
 | 
					      isHovering: false,
 | 
				
			||||||
 | 
					      imageReady: false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    labelFontSize() {
 | 
				
			||||||
 | 
					      if (this.width < 160) return 0.75
 | 
				
			||||||
 | 
					      return 0.875
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    cardHeight() {
 | 
				
			||||||
 | 
					      return this.width
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    sizeMultiplier() {
 | 
				
			||||||
 | 
					      return this.width / 120
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    title() {
 | 
				
			||||||
 | 
					      return this.series ? this.series.name : ''
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    books() {
 | 
				
			||||||
 | 
					      return this.series ? this.series.books || [] : []
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    store() {
 | 
				
			||||||
 | 
					      return this.$store || this.$nuxt.$store
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    firstBookInSeries() {
 | 
				
			||||||
 | 
					      if (!this.series || !this.series.books.length) return null
 | 
				
			||||||
 | 
					      return this.series.books[0]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    bookCoverSrc() {
 | 
				
			||||||
 | 
					      return this.store.getters['audiobooks/getBookCoverSrc'](this.firstBookInSeries)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    currentLibraryId() {
 | 
				
			||||||
 | 
					      return this.store.state.libraries.currentLibraryId
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    setEntity(_series) {
 | 
				
			||||||
 | 
					      this.series = _series
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setSelectionMode(val) {
 | 
				
			||||||
 | 
					      this.isSelectionMode = val
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mouseover() {
 | 
				
			||||||
 | 
					      this.isHovering = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    mouseleave() {
 | 
				
			||||||
 | 
					      this.isHovering = false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clickCard() {
 | 
				
			||||||
 | 
					      if (!this.series) return
 | 
				
			||||||
 | 
					      var router = this.$router || this.$nuxt.$router
 | 
				
			||||||
 | 
					      router.push(`/library/${this.currentLibraryId}/bookshelf/series?series=${this.$encode(this.series.id)}`)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    imageLoaded() {
 | 
				
			||||||
 | 
					      this.imageReady = true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    destroy() {
 | 
				
			||||||
 | 
					      // destroy the vue listeners, etc
 | 
				
			||||||
 | 
					      this.$destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // remove the element from the DOM
 | 
				
			||||||
 | 
					      if (this.$el && this.$el.parentNode) {
 | 
				
			||||||
 | 
					        this.$el.parentNode.removeChild(this.$el)
 | 
				
			||||||
 | 
					      } else if (this.$el && this.$el.remove) {
 | 
				
			||||||
 | 
					        this.$el.remove()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -141,19 +141,24 @@ export default {
 | 
				
			|||||||
      return _sel.text
 | 
					      return _sel.text
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    genres() {
 | 
					    genres() {
 | 
				
			||||||
      return this.$store.getters['audiobooks/getGenresUsed']
 | 
					      // return this.$store.getters['audiobooks/getGenresUsed']
 | 
				
			||||||
 | 
					      return this.filterData.genres || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    tags() {
 | 
					    tags() {
 | 
				
			||||||
      return this.$store.state.audiobooks.tags
 | 
					      // return this.$store.state.audiobooks.tags
 | 
				
			||||||
 | 
					      return this.filterData.tags || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    series() {
 | 
					    series() {
 | 
				
			||||||
      return this.$store.state.audiobooks.series
 | 
					      // return this.$store.state.audiobooks.series
 | 
				
			||||||
 | 
					      return this.filterData.series || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    authors() {
 | 
					    authors() {
 | 
				
			||||||
      return this.$store.getters['audiobooks/getUniqueAuthors']
 | 
					      // return this.$store.getters['audiobooks/getUniqueAuthors']
 | 
				
			||||||
 | 
					      return this.filterData.authors || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    narrators() {
 | 
					    narrators() {
 | 
				
			||||||
      return this.$store.getters['audiobooks/getUniqueNarrators']
 | 
					      // return this.$store.getters['audiobooks/getUniqueNarrators']
 | 
				
			||||||
 | 
					      return this.filterData.narrators || []
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    progress() {
 | 
					    progress() {
 | 
				
			||||||
      return ['Read', 'Unread', 'In Progress']
 | 
					      return ['Read', 'Unread', 'In Progress']
 | 
				
			||||||
@ -165,6 +170,9 @@ export default {
 | 
				
			|||||||
          value: this.$encode(item)
 | 
					          value: this.$encode(item)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    filterData() {
 | 
				
			||||||
 | 
					      return this.$store.state.libraries.filterData || {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
				
			|||||||
@ -94,7 +94,7 @@ export default {
 | 
				
			|||||||
      if (!this.search) return
 | 
					      if (!this.search) return
 | 
				
			||||||
      var search = this.search
 | 
					      var search = this.search
 | 
				
			||||||
      this.clearResults()
 | 
					      this.clearResults()
 | 
				
			||||||
      this.$router.push(`/library/${this.currentLibraryId}/bookshelf/search?query=${search}`)
 | 
					      this.$router.push(`/library/${this.currentLibraryId}/search?q=${search}`)
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    clearResults() {
 | 
					    clearResults() {
 | 
				
			||||||
      this.search = null
 | 
					      this.search = null
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@
 | 
				
			|||||||
      <div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
 | 
					      <div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
 | 
				
			||||||
        <div class="w-full h-full z-0" ref="coverBg" />
 | 
					        <div class="w-full h-full z-0" ref="coverBg" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
 | 
					      <img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
 | 
				
			||||||
      <div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
 | 
					      <div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
 | 
				
			||||||
        <p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
 | 
					        <p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
 | 
				
			||||||
        <div class="absolute top-2 right-2">
 | 
					        <div class="absolute top-2 right-2">
 | 
				
			||||||
@ -57,7 +57,8 @@ export default {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      loading: true,
 | 
					      loading: true,
 | 
				
			||||||
      imageFailed: false,
 | 
					      imageFailed: false,
 | 
				
			||||||
      showCoverBg: false
 | 
					      showCoverBg: false,
 | 
				
			||||||
 | 
					      imageReady: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
@ -135,6 +136,9 @@ export default {
 | 
				
			|||||||
    hideCoverBg() {},
 | 
					    hideCoverBg() {},
 | 
				
			||||||
    imageLoaded() {
 | 
					    imageLoaded() {
 | 
				
			||||||
      this.loading = false
 | 
					      this.loading = false
 | 
				
			||||||
 | 
					      this.$nextTick(() => {
 | 
				
			||||||
 | 
					        this.imageReady = true
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
      if (this.$refs.cover && this.cover !== this.placeholderUrl) {
 | 
					      if (this.$refs.cover && this.cover !== this.placeholderUrl) {
 | 
				
			||||||
        var { naturalWidth, naturalHeight } = this.$refs.cover
 | 
					        var { naturalWidth, naturalHeight } = this.$refs.cover
 | 
				
			||||||
        var aspectRatio = naturalHeight / naturalWidth
 | 
					        var aspectRatio = naturalHeight / naturalWidth
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,6 @@ export default {
 | 
				
			|||||||
    width: Number,
 | 
					    width: Number,
 | 
				
			||||||
    height: Number,
 | 
					    height: Number,
 | 
				
			||||||
    groupTo: String,
 | 
					    groupTo: String,
 | 
				
			||||||
    type: String,
 | 
					 | 
				
			||||||
    isSearch: Boolean
 | 
					    isSearch: Boolean
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
@ -51,10 +50,16 @@ export default {
 | 
				
			|||||||
      return this.width / 192
 | 
					      return this.width / 192
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showExperimentalFeatures() {
 | 
					    showExperimentalFeatures() {
 | 
				
			||||||
      return this.$store.state.showExperimentalFeatures
 | 
					      return this.store.state.showExperimentalFeatures
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showCoverFan() {
 | 
					    showCoverFan() {
 | 
				
			||||||
      return this.showExperimentalFeatures && this.windowWidth > 1024 && !this.isSearch
 | 
					      return this.showExperimentalFeatures && this.windowWidth > 1024 && !this.isSearch
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    store() {
 | 
				
			||||||
 | 
					      return this.$store || this.$nuxt.$store
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    router() {
 | 
				
			||||||
 | 
					      return this.$router || this.$nuxt.$router
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
@ -167,14 +172,14 @@ export default {
 | 
				
			|||||||
        if (coverEl.dataset.audiobookId) {
 | 
					        if (coverEl.dataset.audiobookId) {
 | 
				
			||||||
          let audiobookId = coverEl.dataset.audiobookId
 | 
					          let audiobookId = coverEl.dataset.audiobookId
 | 
				
			||||||
          coverOverlay.addEventListener('click', (e) => {
 | 
					          coverOverlay.addEventListener('click', (e) => {
 | 
				
			||||||
            this.$router.push(`/audiobook/${audiobookId}`)
 | 
					            this.router.push(`/audiobook/${audiobookId}`)
 | 
				
			||||||
            e.stopPropagation()
 | 
					            e.stopPropagation()
 | 
				
			||||||
            e.preventDefault()
 | 
					            e.preventDefault()
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // Is Series
 | 
					          // Is Series
 | 
				
			||||||
          coverOverlay.addEventListener('click', (e) => {
 | 
					          coverOverlay.addEventListener('click', (e) => {
 | 
				
			||||||
            this.$router.push(this.groupTo)
 | 
					            this.router.push(this.groupTo)
 | 
				
			||||||
            e.stopPropagation()
 | 
					            e.stopPropagation()
 | 
				
			||||||
            e.preventDefault()
 | 
					            e.preventDefault()
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
@ -193,7 +198,7 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    getCoverUrl(book) {
 | 
					    getCoverUrl(book) {
 | 
				
			||||||
      return this.$store.getters['audiobooks/getBookCoverSrc'](book, '')
 | 
					      return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
 | 
					    async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
 | 
				
			||||||
      var src = coverData.coverUrl
 | 
					      var src = coverData.coverUrl
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,8 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="75">
 | 
					  <modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="75">
 | 
				
			||||||
    <template #outer>
 | 
					    <template #outer>
 | 
				
			||||||
      <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
 | 
					      <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
 | 
				
			||||||
        <p class="font-book text-3xl text-white truncate">{{ title }}</p>
 | 
					        <p class="font-book text-3xl text-white truncate pointer-events-none">{{ title }}</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </template>
 | 
					    </template>
 | 
				
			||||||
    <div class="absolute -top-10 left-0 w-full flex">
 | 
					    <div class="absolute -top-10 left-0 w-full flex">
 | 
				
			||||||
 | 
				
			|||||||
@ -74,9 +74,18 @@ export default {
 | 
				
			|||||||
      this.showMenu = false
 | 
					      this.showMenu = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async updateLibrary(library) {
 | 
					    async updateLibrary(library) {
 | 
				
			||||||
 | 
					      var currLibraryId = this.currentLibraryId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.disabled = true
 | 
					      this.disabled = true
 | 
				
			||||||
      await this.$store.dispatch('libraries/fetch', library.id)
 | 
					      await this.$store.dispatch('libraries/fetch', library.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.$route.name.startsWith('library')) {
 | 
				
			||||||
 | 
					        var newRoute = this.$route.path.replace(currLibraryId, library.id)
 | 
				
			||||||
 | 
					        this.$router.push(newRoute)
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        this.$router.push(`/library/${library.id}`)
 | 
					        this.$router.push(`/library/${library.id}`)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.disabled = false
 | 
					      this.disabled = false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								client/mixins/bookshelfCardsHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								client/mixins/bookshelfCardsHelpers.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import Vue from 'vue'
 | 
				
			||||||
 | 
					import LazyBookCard from '@/components/cards/LazyBookCard'
 | 
				
			||||||
 | 
					import LazySeriesCard from '@/components/cards/LazySeriesCard'
 | 
				
			||||||
 | 
					import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      cardsHelpers: {
 | 
				
			||||||
 | 
					        mountEntityCard: this.mountEntityCard
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    getComponentClass() {
 | 
				
			||||||
 | 
					      if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
 | 
				
			||||||
 | 
					      if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
 | 
				
			||||||
 | 
					      return Vue.extend(LazyBookCard)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async mountEntityCard(index) {
 | 
				
			||||||
 | 
					      var shelf = Math.floor(index / this.entitiesPerShelf)
 | 
				
			||||||
 | 
					      var shelfEl = document.getElementById(`shelf-${shelf}`)
 | 
				
			||||||
 | 
					      if (!shelfEl) {
 | 
				
			||||||
 | 
					        console.error('invalid shelf', shelf, 'book index', index)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.entityIndexesMounted.push(index)
 | 
				
			||||||
 | 
					      if (this.entityComponentRefs[index]) {
 | 
				
			||||||
 | 
					        var bookComponent = this.entityComponentRefs[index]
 | 
				
			||||||
 | 
					        shelfEl.appendChild(bookComponent.$el)
 | 
				
			||||||
 | 
					        if (this.isSelectionMode) {
 | 
				
			||||||
 | 
					          bookComponent.setSelectionMode(true)
 | 
				
			||||||
 | 
					          if (this.selectedAudiobooks.includes(bookComponent.audiobookId) || this.isSelectAll) {
 | 
				
			||||||
 | 
					            bookComponent.selected = true
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            bookComponent.selected = false
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          bookComponent.setSelectionMode(false)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        bookComponent.isHovering = false
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      var shelfOffsetY = 16
 | 
				
			||||||
 | 
					      var row = index % this.entitiesPerShelf
 | 
				
			||||||
 | 
					      var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var ComponentClass = this.getComponentClass()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var _this = this
 | 
				
			||||||
 | 
					      var instance = new ComponentClass({
 | 
				
			||||||
 | 
					        propsData: {
 | 
				
			||||||
 | 
					          index: index,
 | 
				
			||||||
 | 
					          width: this.entityWidth
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        created() {
 | 
				
			||||||
 | 
					          this.$on('edit', (entity) => {
 | 
				
			||||||
 | 
					            if (_this.editEntity) _this.editEntity(entity)
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          this.$on('select', (entity) => {
 | 
				
			||||||
 | 
					            if (_this.selectEntity) _this.selectEntity(entity)
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      this.entityComponentRefs[index] = instance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      instance.$mount()
 | 
				
			||||||
 | 
					      instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
 | 
				
			||||||
 | 
					      shelfEl.appendChild(instance.$el)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this.entities[index]) {
 | 
				
			||||||
 | 
					        instance.setEntity(this.entities[index])
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this.isSelectionMode) {
 | 
				
			||||||
 | 
					        instance.setSelectionMode(true)
 | 
				
			||||||
 | 
					        if (this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
 | 
				
			||||||
 | 
					          instance.selected = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,7 +4,7 @@
 | 
				
			|||||||
      <app-side-rail class="hidden md:block" />
 | 
					      <app-side-rail class="hidden md:block" />
 | 
				
			||||||
      <div class="flex-grow">
 | 
					      <div class="flex-grow">
 | 
				
			||||||
        <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode.sync="viewMode" />
 | 
					        <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode.sync="viewMode" />
 | 
				
			||||||
        <app-lazy-bookshelf />
 | 
					        <app-lazy-bookshelf :page="id || ''" />
 | 
				
			||||||
        <!-- <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode="viewMode" /> -->
 | 
					        <!-- <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode="viewMode" /> -->
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										62
									
								
								client/pages/library/_library/search.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								client/pages/library/_library/search.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="page" :class="streamAudiobook ? 'streaming' : ''">
 | 
				
			||||||
 | 
					    <div class="flex h-full">
 | 
				
			||||||
 | 
					      <app-side-rail class="hidden md:block" />
 | 
				
			||||||
 | 
					      <div class="flex-grow">
 | 
				
			||||||
 | 
					        <app-book-shelf-toolbar is-home />
 | 
				
			||||||
 | 
					        <app-book-shelf-categorized v-if="hasResults" search :results="results" />
 | 
				
			||||||
 | 
					        <div v-else class="w-full py-16">
 | 
				
			||||||
 | 
					          <p class="text-xl text-center">No Search results for "{{ query }}"</p>
 | 
				
			||||||
 | 
					          <div class="flex justify-center">
 | 
				
			||||||
 | 
					            <ui-btn class="w-52 my-4" @click="back">Back</ui-btn>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  async asyncData({ store, params, redirect, query, app }) {
 | 
				
			||||||
 | 
					    var libraryId = params.library
 | 
				
			||||||
 | 
					    var query = query.q
 | 
				
			||||||
 | 
					    var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
 | 
				
			||||||
 | 
					      console.error('Failed to search library', error)
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    results = {
 | 
				
			||||||
 | 
					      audiobooks: results && results.audiobooks.length ? results.audiobooks : null,
 | 
				
			||||||
 | 
					      authors: results && results.authors.length ? results.authors : null,
 | 
				
			||||||
 | 
					      series: results && results.series.length ? results.series : null,
 | 
				
			||||||
 | 
					      tags: results && results.tags.length ? results.tags : null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    console.log('SEARCH RESULTS', results)
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      results,
 | 
				
			||||||
 | 
					      query
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {}
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    streamAudiobook() {
 | 
				
			||||||
 | 
					      return this.$store.state.streamAudiobook
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    hasResults() {
 | 
				
			||||||
 | 
					      return Object.values(this.results).find((r) => !!r)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    async back() {
 | 
				
			||||||
 | 
					      var popped = await this.$store.dispatch('popRoute')
 | 
				
			||||||
 | 
					      if (popped) this.$store.commit('setIsRoutingBack', true)
 | 
				
			||||||
 | 
					      var backTo = popped || '/'
 | 
				
			||||||
 | 
					      this.$router.push(backTo)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {},
 | 
				
			||||||
 | 
					  beforeDestroy() {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -155,6 +155,7 @@ export const getters = {
 | 
				
			|||||||
    return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
 | 
					    return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
 | 
					  getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
 | 
				
			||||||
 | 
					    if (!bookItem) return placeholder
 | 
				
			||||||
    var book = bookItem.book
 | 
					    var book = bookItem.book
 | 
				
			||||||
    if (!book || !book.cover || book.cover === placeholder) return placeholder
 | 
					    if (!book || !book.cover || book.cover === placeholder) return placeholder
 | 
				
			||||||
    var cover = book.cover
 | 
					    var cover = book.cover
 | 
				
			||||||
 | 
				
			|||||||
@ -33,6 +33,5 @@ export const mutations = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  setShowBookshelfTextureModal(state, val) {
 | 
					  setShowBookshelfTextureModal(state, val) {
 | 
				
			||||||
    state.showBookshelfTextureModal = val
 | 
					    state.showBookshelfTextureModal = val
 | 
				
			||||||
    console.log('shopw', val)
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -4,7 +4,8 @@ export const state = () => ({
 | 
				
			|||||||
  listeners: [],
 | 
					  listeners: [],
 | 
				
			||||||
  currentLibraryId: 'main',
 | 
					  currentLibraryId: 'main',
 | 
				
			||||||
  folders: [],
 | 
					  folders: [],
 | 
				
			||||||
  folderLastUpdate: 0
 | 
					  folderLastUpdate: 0,
 | 
				
			||||||
 | 
					  filterData: null
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getters = {
 | 
					export const getters = {
 | 
				
			||||||
@ -53,16 +54,19 @@ export const actions = {
 | 
				
			|||||||
      return false
 | 
					      return false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var library = state.libraries.find(lib => lib.id === libraryId)
 | 
					    // var library = state.libraries.find(lib => lib.id === libraryId)
 | 
				
			||||||
    if (library) {
 | 
					    // if (library) {
 | 
				
			||||||
      commit('setCurrentLibrary', libraryId)
 | 
					    //   commit('setCurrentLibrary', libraryId)
 | 
				
			||||||
      return library
 | 
					    //   return library
 | 
				
			||||||
    }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.$axios
 | 
					    return this.$axios
 | 
				
			||||||
      .$get(`/api/libraries/${libraryId}`)
 | 
					      .$get(`/api/libraries/${libraryId}?include=filterdata`)
 | 
				
			||||||
      .then((data) => {
 | 
					      .then((data) => {
 | 
				
			||||||
        commit('addUpdate', data)
 | 
					        var library = data.library
 | 
				
			||||||
 | 
					        var filterData = data.filterdata
 | 
				
			||||||
 | 
					        commit('addUpdate', library)
 | 
				
			||||||
 | 
					        commit('setLibraryFilterData', filterData)
 | 
				
			||||||
        commit('setCurrentLibrary', libraryId)
 | 
					        commit('setCurrentLibrary', libraryId)
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@ -97,7 +101,22 @@ export const actions = {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
    return true
 | 
					    return true
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  loadLibraryFilterData({ state, commit, rootState }) {
 | 
				
			||||||
 | 
					    if (!rootState.user || !rootState.user.user) {
 | 
				
			||||||
 | 
					      console.error('libraries/loadLibraryFilterData - User not set')
 | 
				
			||||||
 | 
					      return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.$axios
 | 
				
			||||||
 | 
					      .$get(`/api/libraries/${state.currentLibraryId}/filters`)
 | 
				
			||||||
 | 
					      .then((data) => {
 | 
				
			||||||
 | 
					        commit('setLibraryFilterData', data)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((error) => {
 | 
				
			||||||
 | 
					        console.error('Failed', error)
 | 
				
			||||||
 | 
					        commit('setLibraryFilterData', null)
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const mutations = {
 | 
					export const mutations = {
 | 
				
			||||||
@ -145,5 +164,8 @@ export const mutations = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  removeListener(state, listenerId) {
 | 
					  removeListener(state, listenerId) {
 | 
				
			||||||
    state.listeners = state.listeners.filter(l => l.id !== listenerId)
 | 
					    state.listeners = state.listeners.filter(l => l.id !== listenerId)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  setLibraryFilterData(state, filterData) {
 | 
				
			||||||
 | 
					    state.filterData = filterData
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -25,7 +25,7 @@ export const getters = {
 | 
				
			|||||||
    return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
 | 
					    return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getUserSetting: (state) => (key) => {
 | 
					  getUserSetting: (state) => (key) => {
 | 
				
			||||||
    return state.settings ? state.settings[key] || null : null
 | 
					    return state.settings ? state.settings[key] : null
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getUserCanUpdate: (state) => {
 | 
					  getUserCanUpdate: (state) => {
 | 
				
			||||||
    return state.user && state.user.permissions ? !!state.user.permissions.update : false
 | 
					    return state.user && state.user.permissions ? !!state.user.permissions.update : false
 | 
				
			||||||
 | 
				
			|||||||
@ -49,13 +49,17 @@ class ApiController {
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    this.router.post('/libraries', LibraryController.create.bind(this))
 | 
					    this.router.post('/libraries', LibraryController.create.bind(this))
 | 
				
			||||||
    this.router.get('/libraries', LibraryController.findAll.bind(this))
 | 
					    this.router.get('/libraries', LibraryController.findAll.bind(this))
 | 
				
			||||||
    this.router.get('/libraries/:id', LibraryController.findOne.bind(this))
 | 
					    this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this))
 | 
				
			||||||
    this.router.patch('/libraries/:id', LibraryController.update.bind(this))
 | 
					    this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
 | 
				
			||||||
    this.router.delete('/libraries/:id', LibraryController.delete.bind(this))
 | 
					    this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.router.get('/libraries/:id/books/all', LibraryController.getBooksForLibrary2.bind(this))
 | 
					    this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
 | 
				
			||||||
    this.router.get('/libraries/:id/books', LibraryController.getBooksForLibrary.bind(this))
 | 
					    this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
 | 
				
			||||||
    this.router.get('/libraries/:id/search', LibraryController.search.bind(this))
 | 
					    this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
 | 
				
			||||||
    this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
 | 
					    this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TEMP: Support old syntax for mobile app
 | 
					    // TEMP: Support old syntax for mobile app
 | 
				
			||||||
@ -491,43 +495,103 @@ class ApiController {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  decode(text) {
 | 
					  // decode(text) {
 | 
				
			||||||
    return Buffer.from(decodeURIComponent(text), 'base64').toString()
 | 
					  //   return Buffer.from(decodeURIComponent(text), 'base64').toString()
 | 
				
			||||||
  }
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getFiltered(audiobooks, filterBy, user) {
 | 
					  // getFiltered(audiobooks, filterBy, user) {
 | 
				
			||||||
    var filtered = audiobooks
 | 
					  //   var filtered = audiobooks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
 | 
					  //   var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
 | 
				
			||||||
    var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
 | 
					  //   var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
 | 
				
			||||||
    if (group) {
 | 
					  //   if (group) {
 | 
				
			||||||
      var filterVal = filterBy.replace(`${group}.`, '')
 | 
					  //     var filterVal = filterBy.replace(`${group}.`, '')
 | 
				
			||||||
      var filter = this.decode(filterVal)
 | 
					  //     var filter = this.decode(filterVal)
 | 
				
			||||||
      if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
 | 
					  //     if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
 | 
				
			||||||
      else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
 | 
					  //     else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
 | 
				
			||||||
      else if (group === 'series') {
 | 
					  //     else if (group === 'series') {
 | 
				
			||||||
        if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
 | 
					  //       if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
 | 
				
			||||||
        else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
 | 
					  //       else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
 | 
				
			||||||
      }
 | 
					  //     }
 | 
				
			||||||
      else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
 | 
					  //     else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
 | 
				
			||||||
      else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
 | 
					  //     else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
 | 
				
			||||||
      else if (group === 'progress') {
 | 
					  //     else if (group === 'progress') {
 | 
				
			||||||
        filtered = filtered.filter(ab => {
 | 
					  //       filtered = filtered.filter(ab => {
 | 
				
			||||||
          var userAudiobook = user.getAudiobookJSON(ab.id)
 | 
					  //         var userAudiobook = user.getAudiobookJSON(ab.id)
 | 
				
			||||||
          var isRead = userAudiobook && userAudiobook.isRead
 | 
					  //         var isRead = userAudiobook && userAudiobook.isRead
 | 
				
			||||||
          if (filter === 'Read' && isRead) return true
 | 
					  //         if (filter === 'Read' && isRead) return true
 | 
				
			||||||
          if (filter === 'Unread' && !isRead) return true
 | 
					  //         if (filter === 'Unread' && !isRead) return true
 | 
				
			||||||
          if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
 | 
					  //         if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
 | 
				
			||||||
          return false
 | 
					  //         return false
 | 
				
			||||||
        })
 | 
					  //       })
 | 
				
			||||||
      }
 | 
					  //     }
 | 
				
			||||||
    } else if (filterBy === 'issues') {
 | 
					  //   } else if (filterBy === 'issues') {
 | 
				
			||||||
      filtered = filtered.filter(ab => {
 | 
					  //     filtered = filtered.filter(ab => {
 | 
				
			||||||
        return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
 | 
					  //       return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
 | 
				
			||||||
      })
 | 
					  //     })
 | 
				
			||||||
    }
 | 
					  //   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return filtered
 | 
					  //   return filtered
 | 
				
			||||||
  }
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // getDistinctFilterData(audiobooks) {
 | 
				
			||||||
 | 
					  //   var data = {
 | 
				
			||||||
 | 
					  //     authors: [],
 | 
				
			||||||
 | 
					  //     genres: [],
 | 
				
			||||||
 | 
					  //     tags: [],
 | 
				
			||||||
 | 
					  //     series: [],
 | 
				
			||||||
 | 
					  //     narrators: []
 | 
				
			||||||
 | 
					  //   }
 | 
				
			||||||
 | 
					  //   audiobooks.forEach((ab) => {
 | 
				
			||||||
 | 
					  //     if (ab.book._authorsList.length) {
 | 
				
			||||||
 | 
					  //       ab.book._authorsList.forEach((author) => {
 | 
				
			||||||
 | 
					  //         if (author && !data.authors.includes(author)) data.authors.push(author)
 | 
				
			||||||
 | 
					  //       })
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //     if (ab.book._genres.length) {
 | 
				
			||||||
 | 
					  //       ab.book._genres.forEach((genre) => {
 | 
				
			||||||
 | 
					  //         if (genre && !data.genres.includes(genre)) data.genres.push(genre)
 | 
				
			||||||
 | 
					  //       })
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //     if (ab.tags.length) {
 | 
				
			||||||
 | 
					  //       ab.tags.forEach((tag) => {
 | 
				
			||||||
 | 
					  //         if (tag && !data.tags.includes(tag)) data.tags.push(tag)
 | 
				
			||||||
 | 
					  //       })
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //     if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
 | 
				
			||||||
 | 
					  //     if (ab.book._narratorsList.length) {
 | 
				
			||||||
 | 
					  //       ab.book._narratorsList.forEach((narrator) => {
 | 
				
			||||||
 | 
					  //         if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
 | 
				
			||||||
 | 
					  //       })
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //   })
 | 
				
			||||||
 | 
					  //   return data
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // getBooksMostRecentlyRead(user, books, limit) {
 | 
				
			||||||
 | 
					  //   var booksWithProgress = books.map(book => {
 | 
				
			||||||
 | 
					  //     return {
 | 
				
			||||||
 | 
					  //       userAudiobook: user.getAudiobookJSON(book.id),
 | 
				
			||||||
 | 
					  //       book
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //   }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
 | 
				
			||||||
 | 
					  //   booksWithProgress.sort((a, b) => {
 | 
				
			||||||
 | 
					  //     return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
 | 
				
			||||||
 | 
					  //   })
 | 
				
			||||||
 | 
					  //   return booksWithProgress.map(b => b.book).slice(0, limit)
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // getBooksMostRecentlyAdded(user, books, limit) {
 | 
				
			||||||
 | 
					  //   var booksWithProgress = books.map(book => {
 | 
				
			||||||
 | 
					  //     return {
 | 
				
			||||||
 | 
					  //       userAudiobook: user.getAudiobookJSON(book.id),
 | 
				
			||||||
 | 
					  //       book
 | 
				
			||||||
 | 
					  //     }
 | 
				
			||||||
 | 
					  //   }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
 | 
				
			||||||
 | 
					  //   booksWithProgress.sort((a, b) => {
 | 
				
			||||||
 | 
					  //     return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
 | 
				
			||||||
 | 
					  //   })
 | 
				
			||||||
 | 
					  //   return booksWithProgress.map(b => b.book).slice(0, limit)
 | 
				
			||||||
 | 
					  // }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = ApiController
 | 
					module.exports = ApiController
 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
const Library = require('../objects/Library')
 | 
					const Library = require('../objects/Library')
 | 
				
			||||||
const { sort } = require('fast-sort')
 | 
					const { sort } = require('fast-sort')
 | 
				
			||||||
 | 
					const libraryHelpers = require('../utils/libraryHelpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LibraryController {
 | 
					class LibraryController {
 | 
				
			||||||
  constructor() { }
 | 
					  constructor() { }
 | 
				
			||||||
@ -29,21 +30,19 @@ class LibraryController {
 | 
				
			|||||||
    res.json(this.db.libraries.map(lib => lib.toJSON()))
 | 
					    res.json(this.db.libraries.map(lib => lib.toJSON()))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  findOne(req, res) {
 | 
					  async findOne(req, res) {
 | 
				
			||||||
    if (!req.params.id) return res.status(500).send('Invalid id parameter')
 | 
					    if (req.query.include && req.query.include === 'filterdata') {
 | 
				
			||||||
 | 
					      var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
 | 
					      return res.json({
 | 
				
			||||||
    if (!library) {
 | 
					        filterdata: libraryHelpers.getDistinctFilterData(books),
 | 
				
			||||||
      return res.status(404).send('Library not found')
 | 
					        library: req.library
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return res.json(library.toJSON())
 | 
					    return res.json(req.library)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async update(req, res) {
 | 
					  async update(req, res) {
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
 | 
					    var library = req.library
 | 
				
			||||||
    if (!library) {
 | 
					 | 
				
			||||||
      return res.status(404).send('Library not found')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var hasUpdates = library.update(req.body)
 | 
					    var hasUpdates = library.update(req.body)
 | 
				
			||||||
    if (hasUpdates) {
 | 
					    if (hasUpdates) {
 | 
				
			||||||
      // Update watcher
 | 
					      // Update watcher
 | 
				
			||||||
@ -64,10 +63,7 @@ class LibraryController {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async delete(req, res) {
 | 
					  async delete(req, res) {
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
 | 
					    var library = req.library
 | 
				
			||||||
    if (!library) {
 | 
					 | 
				
			||||||
      return res.status(404).send('Library not found')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Remove library watcher
 | 
					    // Remove library watcher
 | 
				
			||||||
    this.watcher.removeLibrary(library)
 | 
					    this.watcher.removeLibrary(library)
 | 
				
			||||||
@ -87,11 +83,7 @@ class LibraryController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // api/libraries/:id/books
 | 
					  // api/libraries/:id/books
 | 
				
			||||||
  getBooksForLibrary(req, res) {
 | 
					  getBooksForLibrary(req, res) {
 | 
				
			||||||
    var libraryId = req.params.id
 | 
					    var libraryId = req.library.id
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === libraryId)
 | 
					 | 
				
			||||||
    if (!library) {
 | 
					 | 
				
			||||||
      return res.status(400).send('Library does not exist')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
 | 
					    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
 | 
				
			||||||
    // if (req.query.q) {
 | 
					    // if (req.query.q) {
 | 
				
			||||||
    //   audiobooks = this.db.audiobooks.filter(ab => {
 | 
					    //   audiobooks = this.db.audiobooks.filter(ab => {
 | 
				
			||||||
@ -102,7 +94,7 @@ class LibraryController {
 | 
				
			|||||||
    // }
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (req.query.filter) {
 | 
					    if (req.query.filter) {
 | 
				
			||||||
      audiobooks = this.getFiltered(this.db.audiobooks, req.query.filter, req.user)
 | 
					      audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -126,13 +118,9 @@ class LibraryController {
 | 
				
			|||||||
    res.json(audiobooks)
 | 
					    res.json(audiobooks)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // api/libraries/:id/books/fs
 | 
					  // api/libraries/:id/books/all
 | 
				
			||||||
  getBooksForLibrary2(req, res) {
 | 
					  getBooksForLibrary2(req, res) {
 | 
				
			||||||
    var libraryId = req.params.id
 | 
					    var libraryId = req.library.id
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === libraryId)
 | 
					 | 
				
			||||||
    if (!library) {
 | 
					 | 
				
			||||||
      return res.status(400).send('Library does not exist')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
 | 
					    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
 | 
				
			||||||
    var payload = {
 | 
					    var payload = {
 | 
				
			||||||
@ -146,7 +134,8 @@ class LibraryController {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (payload.filterBy) {
 | 
					    if (payload.filterBy) {
 | 
				
			||||||
      audiobooks = this.getFiltered(this.db.audiobooks, payload.filterBy, req.user)
 | 
					      audiobooks = libraryHelpers.getFiltered(audiobooks, payload.filterBy, req.user)
 | 
				
			||||||
 | 
					      payload.total = audiobooks.length
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (payload.sortBy) {
 | 
					    if (payload.sortBy) {
 | 
				
			||||||
@ -170,6 +159,110 @@ class LibraryController {
 | 
				
			|||||||
    res.json(payload)
 | 
					    res.json(payload)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // api/libraries/:id/series
 | 
				
			||||||
 | 
					  async getSeriesForLibrary(req, res) {
 | 
				
			||||||
 | 
					    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var payload = {
 | 
				
			||||||
 | 
					      results: [],
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
				
			||||||
 | 
					      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
 | 
				
			||||||
 | 
					      sortBy: req.query.sort,
 | 
				
			||||||
 | 
					      sortDesc: req.query.desc === '1',
 | 
				
			||||||
 | 
					      filterBy: req.query.filter
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var series = libraryHelpers.getSeriesFromBooks(audiobooks)
 | 
				
			||||||
 | 
					    payload.total = series.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (payload.limit) {
 | 
				
			||||||
 | 
					      var startIndex = payload.page * payload.limit
 | 
				
			||||||
 | 
					      series = series.slice(startIndex, startIndex + payload.limit)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    payload.results = series
 | 
				
			||||||
 | 
					    console.log('returning series', series.length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(payload)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // api/libraries/:id/series
 | 
				
			||||||
 | 
					  async getCollectionsForLibrary(req, res) {
 | 
				
			||||||
 | 
					    var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var payload = {
 | 
				
			||||||
 | 
					      results: [],
 | 
				
			||||||
 | 
					      total: 0,
 | 
				
			||||||
 | 
					      limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
 | 
				
			||||||
 | 
					      page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0,
 | 
				
			||||||
 | 
					      sortBy: req.query.sort,
 | 
				
			||||||
 | 
					      sortDesc: req.query.desc === '1',
 | 
				
			||||||
 | 
					      filterBy: req.query.filter
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => c.toJSONExpanded(audiobooks))
 | 
				
			||||||
 | 
					    payload.total = collections.length
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (payload.limit) {
 | 
				
			||||||
 | 
					      var startIndex = payload.page * payload.limit
 | 
				
			||||||
 | 
					      collections = collections.slice(startIndex, startIndex + payload.limit)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    payload.results = collections
 | 
				
			||||||
 | 
					    console.log('returning collections', collections.length)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(payload)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // api/libraries/:id/books/filters
 | 
				
			||||||
 | 
					  async getLibraryFilters(req, res) {
 | 
				
			||||||
 | 
					    var library = req.library
 | 
				
			||||||
 | 
					    var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
 | 
				
			||||||
 | 
					    res.json(libraryHelpers.getDistinctFilterData(books))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // api/libraries/:id/books/categories
 | 
				
			||||||
 | 
					  async getLibraryCategories(req, res) {
 | 
				
			||||||
 | 
					    var library = req.library
 | 
				
			||||||
 | 
					    var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
 | 
				
			||||||
 | 
					    var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var booksWithUserAb = libraryHelpers.getBooksWithUserAudiobook(req.user, books)
 | 
				
			||||||
 | 
					    var series = libraryHelpers.getSeriesFromBooks(books)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var categories = [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'continue-reading',
 | 
				
			||||||
 | 
					        label: 'Continue Reading',
 | 
				
			||||||
 | 
					        type: 'books',
 | 
				
			||||||
 | 
					        entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'recently-added',
 | 
				
			||||||
 | 
					        label: 'Recently Added',
 | 
				
			||||||
 | 
					        type: 'books',
 | 
				
			||||||
 | 
					        entities: libraryHelpers.getBooksMostRecentlyAdded(books, limitPerShelf)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'read-again',
 | 
				
			||||||
 | 
					        label: 'Read Again',
 | 
				
			||||||
 | 
					        type: 'books',
 | 
				
			||||||
 | 
					        entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        id: 'recent-series',
 | 
				
			||||||
 | 
					        label: 'Recent Series',
 | 
				
			||||||
 | 
					        type: 'series',
 | 
				
			||||||
 | 
					        entities: libraryHelpers.getSeriesMostRecentlyAdded(series, limitPerShelf)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ].filter(cats => { // Remove categories with no items
 | 
				
			||||||
 | 
					      return cats.entities.length
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(categories)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // PATCH: Change the order of libraries
 | 
					  // PATCH: Change the order of libraries
 | 
				
			||||||
  async reorder(req, res) {
 | 
					  async reorder(req, res) {
 | 
				
			||||||
    if (!req.user.isRoot) {
 | 
					    if (!req.user.isRoot) {
 | 
				
			||||||
@ -203,10 +296,7 @@ class LibraryController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // GET: Global library search
 | 
					  // GET: Global library search
 | 
				
			||||||
  search(req, res) {
 | 
					  search(req, res) {
 | 
				
			||||||
    var library = this.db.libraries.find(lib => lib.id === req.params.id)
 | 
					    var library = req.library
 | 
				
			||||||
    if (!library) {
 | 
					 | 
				
			||||||
      return res.status(404).send('Library not found')
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!req.query.q) {
 | 
					    if (!req.query.q) {
 | 
				
			||||||
      return res.status(400).send('No query string')
 | 
					      return res.status(400).send('No query string')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -268,5 +358,14 @@ class LibraryController {
 | 
				
			|||||||
      series: Object.values(seriesMatches).slice(0, maxResults)
 | 
					      series: Object.values(seriesMatches).slice(0, maxResults)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  middleware(req, res, next) {
 | 
				
			||||||
 | 
					    var library = this.db.libraries.find(lib => lib.id === req.params.id)
 | 
				
			||||||
 | 
					    if (!library) {
 | 
				
			||||||
 | 
					      return res.status(404).send('Library not found')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    req.library = library
 | 
				
			||||||
 | 
					    next()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
module.exports = new LibraryController()
 | 
					module.exports = new LibraryController()
 | 
				
			||||||
@ -11,6 +11,7 @@ class Book {
 | 
				
			|||||||
    this.authorLF = null
 | 
					    this.authorLF = null
 | 
				
			||||||
    this.authors = []
 | 
					    this.authors = []
 | 
				
			||||||
    this.narrator = null
 | 
					    this.narrator = null
 | 
				
			||||||
 | 
					    this.narratorFL = null
 | 
				
			||||||
    this.series = null
 | 
					    this.series = null
 | 
				
			||||||
    this.volumeNumber = null
 | 
					    this.volumeNumber = null
 | 
				
			||||||
    this.publishYear = null
 | 
					    this.publishYear = null
 | 
				
			||||||
@ -40,6 +41,7 @@ class Book {
 | 
				
			|||||||
  get _author() { return this.authorFL || '' }
 | 
					  get _author() { return this.authorFL || '' }
 | 
				
			||||||
  get _series() { return this.series || '' }
 | 
					  get _series() { return this.series || '' }
 | 
				
			||||||
  get _authorsList() { return this._author.split(', ') }
 | 
					  get _authorsList() { return this._author.split(', ') }
 | 
				
			||||||
 | 
					  get _narratorsList() { return this._narrator.split(', ') }
 | 
				
			||||||
  get _genres() { return this.genres || [] }
 | 
					  get _genres() { return this.genres || [] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get shouldSearchForCover() {
 | 
					  get shouldSearchForCover() {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										132
									
								
								server/utils/libraryHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								server/utils/libraryHelpers.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,132 @@
 | 
				
			|||||||
 | 
					const { sort } = require('fast-sort')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  decode(text) {
 | 
				
			||||||
 | 
					    return Buffer.from(decodeURIComponent(text), 'base64').toString()
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getFiltered(audiobooks, filterBy, user) {
 | 
				
			||||||
 | 
					    var filtered = audiobooks
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
 | 
				
			||||||
 | 
					    var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
 | 
				
			||||||
 | 
					    if (group) {
 | 
				
			||||||
 | 
					      var filterVal = filterBy.replace(`${group}.`, '')
 | 
				
			||||||
 | 
					      var filter = this.decode(filterVal)
 | 
				
			||||||
 | 
					      if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
 | 
				
			||||||
 | 
					      else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
 | 
				
			||||||
 | 
					      else if (group === 'series') {
 | 
				
			||||||
 | 
					        if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
 | 
				
			||||||
 | 
					        else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
 | 
				
			||||||
 | 
					      else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
 | 
				
			||||||
 | 
					      else if (group === 'progress') {
 | 
				
			||||||
 | 
					        filtered = filtered.filter(ab => {
 | 
				
			||||||
 | 
					          var userAudiobook = user.getAudiobookJSON(ab.id)
 | 
				
			||||||
 | 
					          var isRead = userAudiobook && userAudiobook.isRead
 | 
				
			||||||
 | 
					          if (filter === 'Read' && isRead) return true
 | 
				
			||||||
 | 
					          if (filter === 'Unread' && !isRead) return true
 | 
				
			||||||
 | 
					          if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
 | 
				
			||||||
 | 
					          return false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (filterBy === 'issues') {
 | 
				
			||||||
 | 
					      filtered = filtered.filter(ab => {
 | 
				
			||||||
 | 
					        return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return filtered
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getDistinctFilterData(audiobooks) {
 | 
				
			||||||
 | 
					    var data = {
 | 
				
			||||||
 | 
					      authors: [],
 | 
				
			||||||
 | 
					      genres: [],
 | 
				
			||||||
 | 
					      tags: [],
 | 
				
			||||||
 | 
					      series: [],
 | 
				
			||||||
 | 
					      narrators: []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    audiobooks.forEach((ab) => {
 | 
				
			||||||
 | 
					      if (ab.book._authorsList.length) {
 | 
				
			||||||
 | 
					        ab.book._authorsList.forEach((author) => {
 | 
				
			||||||
 | 
					          if (author && !data.authors.includes(author)) data.authors.push(author)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (ab.book._genres.length) {
 | 
				
			||||||
 | 
					        ab.book._genres.forEach((genre) => {
 | 
				
			||||||
 | 
					          if (genre && !data.genres.includes(genre)) data.genres.push(genre)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (ab.tags.length) {
 | 
				
			||||||
 | 
					        ab.tags.forEach((tag) => {
 | 
				
			||||||
 | 
					          if (tag && !data.tags.includes(tag)) data.tags.push(tag)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
 | 
				
			||||||
 | 
					      if (ab.book._narratorsList.length) {
 | 
				
			||||||
 | 
					        ab.book._narratorsList.forEach((narrator) => {
 | 
				
			||||||
 | 
					          if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return data
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getSeriesFromBooks(books) {
 | 
				
			||||||
 | 
					    var _series = {}
 | 
				
			||||||
 | 
					    books.forEach((audiobook) => {
 | 
				
			||||||
 | 
					      if (audiobook.book.series) {
 | 
				
			||||||
 | 
					        if (!_series[audiobook.book.series]) {
 | 
				
			||||||
 | 
					          _series[audiobook.book.series] = {
 | 
				
			||||||
 | 
					            id: audiobook.book.series,
 | 
				
			||||||
 | 
					            name: audiobook.book.series,
 | 
				
			||||||
 | 
					            books: [audiobook.toJSONExpanded()]
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          _series[audiobook.book.series].books.push(audiobook.toJSONExpanded())
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return Object.values(_series)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBooksWithUserAudiobook(user, books) {
 | 
				
			||||||
 | 
					    return books.map(book => {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        userAudiobook: user.getAudiobookJSON(book.id),
 | 
				
			||||||
 | 
					        book
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBooksMostRecentlyRead(booksWithUserAb, limit) {
 | 
				
			||||||
 | 
					    var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
 | 
				
			||||||
 | 
					    booksWithProgress.sort((a, b) => {
 | 
				
			||||||
 | 
					      return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return booksWithProgress.map(b => b.book).slice(0, limit)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBooksMostRecentlyAdded(books, limit) {
 | 
				
			||||||
 | 
					    var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
 | 
				
			||||||
 | 
					    return booksSortedByAddedAt.slice(0, limit)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getBooksMostRecentlyFinished(booksWithUserAb, limit) {
 | 
				
			||||||
 | 
					    var booksRead = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.isRead)
 | 
				
			||||||
 | 
					    booksRead.sort((a, b) => {
 | 
				
			||||||
 | 
					      return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return booksRead.map(b => b.book).slice(0, limit)
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getSeriesMostRecentlyAdded(series, limit) {
 | 
				
			||||||
 | 
					    var seriesSortedByAddedAt = sort(series).desc(_series => {
 | 
				
			||||||
 | 
					      var booksSortedByMostRecent = sort(_series.books).desc(b => b.addedAt)
 | 
				
			||||||
 | 
					      return booksSortedByMostRecent[0].addedAt
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    return seriesSortedByAddedAt.slice(0, limit)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user