mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add global search, add reset all audiobooks
This commit is contained in:
		
							parent
							
								
									fb0a6f4ec2
								
							
						
					
					
						commit
						f70e1beca1
					
				@ -8,9 +8,9 @@
 | 
			
		||||
        </a>
 | 
			
		||||
        <h1 class="text-2xl font-book">AudioBookshelf</h1>
 | 
			
		||||
 | 
			
		||||
        <controls-global-search />
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
 | 
			
		||||
        <!-- <button class="px-4 py-2 bg-blue-500 rounded-xs" @click="scan">Scan</button> -->
 | 
			
		||||
        <nuxt-link to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center">
 | 
			
		||||
          <span class="material-icons">settings</span>
 | 
			
		||||
        </nuxt-link>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										46
									
								
								client/components/cards/AudiobookSearchCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								client/components/cards/AudiobookSearchCard.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex h-full px-1 overflow-hidden">
 | 
			
		||||
    <cards-book-cover :audiobook="audiobook" :width="40" />
 | 
			
		||||
    <div class="flex-grow px-2 searchCardContent h-full">
 | 
			
		||||
      <p class="truncate text-sm">{{ title }}</p>
 | 
			
		||||
      <p class="text-xs text-gray-200 truncate">by {{ author }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    audiobook: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
      default: () => {}
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    book() {
 | 
			
		||||
      return this.audiobook ? this.audiobook.book || {} : {}
 | 
			
		||||
    },
 | 
			
		||||
    title() {
 | 
			
		||||
      return this.book ? this.book.title : 'No Title'
 | 
			
		||||
    },
 | 
			
		||||
    author() {
 | 
			
		||||
      return this.book ? this.book.author : 'Unknown'
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {},
 | 
			
		||||
  mounted() {}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.searchCardContent {
 | 
			
		||||
  width: calc(100% - 80px);
 | 
			
		||||
  height: calc(40px * 1.5);
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@ -1,14 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div ref="wrapper" class="relative" v-click-outside="clickOutside">
 | 
			
		||||
    <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
 | 
			
		||||
    <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
 | 
			
		||||
      <span class="flex items-center justify-between">
 | 
			
		||||
        <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
 | 
			
		||||
      </span>
 | 
			
		||||
      <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
 | 
			
		||||
      <span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
 | 
			
		||||
        <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
 | 
			
		||||
          <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
 | 
			
		||||
        </svg>
 | 
			
		||||
      </span>
 | 
			
		||||
      <div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
 | 
			
		||||
        <span class="material-icons" style="font-size: 1.1rem">close</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
 | 
			
		||||
@ -118,6 +121,11 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    clearSelected() {
 | 
			
		||||
      this.selected = 'all'
 | 
			
		||||
      this.showMenu = false
 | 
			
		||||
      this.$nextTick(() => this.$emit('change', 'all'))
 | 
			
		||||
    },
 | 
			
		||||
    snakeToNormal(kebab) {
 | 
			
		||||
      if (!kebab) {
 | 
			
		||||
        return 'err'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										112
									
								
								client/components/controls/GlobalSearch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								client/components/controls/GlobalSearch.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-64 ml-8 relative">
 | 
			
		||||
    <ui-text-input v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
 | 
			
		||||
    <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
 | 
			
		||||
      <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
 | 
			
		||||
      <span v-else class="material-icons" style="font-size: 1.2rem">close</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-10 -mt-px w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
 | 
			
		||||
      <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
 | 
			
		||||
        <li v-if="isTyping" class="py-2 px-2">
 | 
			
		||||
          <p>Typing...</p>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-else-if="isFetching" class="py-2 px-2">
 | 
			
		||||
          <p>Fetching...</p>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li v-else-if="!items.length" class="py-2 px-2">
 | 
			
		||||
          <p>No Results</p>
 | 
			
		||||
        </li>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
          <template v-for="item in items">
 | 
			
		||||
            <li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item)">
 | 
			
		||||
              <template v-if="item.type === 'audiobook'">
 | 
			
		||||
                <cards-audiobook-search-card :audiobook="item.data" />
 | 
			
		||||
              </template>
 | 
			
		||||
            </li>
 | 
			
		||||
          </template>
 | 
			
		||||
        </template>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      showMenu: false,
 | 
			
		||||
      isFocused: false,
 | 
			
		||||
      focusTimeout: null,
 | 
			
		||||
      isTyping: false,
 | 
			
		||||
      isFetching: false,
 | 
			
		||||
      search: null,
 | 
			
		||||
      items: [],
 | 
			
		||||
      searchTimeout: null,
 | 
			
		||||
      lastSearch: null
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    audiobooks() {
 | 
			
		||||
      return this.$store.state.audiobooks.audiobooks
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    focussed() {
 | 
			
		||||
      this.isFocused = true
 | 
			
		||||
      this.showMenu = true
 | 
			
		||||
    },
 | 
			
		||||
    blurred() {
 | 
			
		||||
      this.isFocused = false
 | 
			
		||||
      clearTimeout(this.focusTimeout)
 | 
			
		||||
      this.focusTimeout = setTimeout(() => {
 | 
			
		||||
        this.showMenu = false
 | 
			
		||||
      }, 200)
 | 
			
		||||
    },
 | 
			
		||||
    async runSearch(value) {
 | 
			
		||||
      this.lastSearch = value
 | 
			
		||||
      if (!this.lastSearch) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.isFetching = true
 | 
			
		||||
      var results = await this.$axios.$get(`/api/audiobooks?q=${value}`).catch((error) => {
 | 
			
		||||
        console.error('Search error', error)
 | 
			
		||||
        return []
 | 
			
		||||
      })
 | 
			
		||||
      this.isFetching = false
 | 
			
		||||
      this.items = results.map((res) => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: res.id,
 | 
			
		||||
          data: res,
 | 
			
		||||
          type: 'audiobook'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    inputUpdate(val) {
 | 
			
		||||
      clearTimeout(this.searchTimeout)
 | 
			
		||||
      if (!val) {
 | 
			
		||||
        this.lastSearch = ''
 | 
			
		||||
        this.isTyping = false
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.isTyping = true
 | 
			
		||||
      this.searchTimeout = setTimeout(() => {
 | 
			
		||||
        this.isTyping = false
 | 
			
		||||
        this.runSearch(val)
 | 
			
		||||
      }, 1000)
 | 
			
		||||
    },
 | 
			
		||||
    clickedOption(option) {
 | 
			
		||||
      if (option.type === 'audiobook') {
 | 
			
		||||
        this.$router.push(`/audiobook/${option.data.id}`)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    clickClear() {
 | 
			
		||||
      if (this.search) {
 | 
			
		||||
        this.search = null
 | 
			
		||||
        this.items = []
 | 
			
		||||
        this.showMenu = false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -1,15 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div ref="wrapper" class="relative" v-click-outside="clickOutside">
 | 
			
		||||
    <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-300 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
 | 
			
		||||
    <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
 | 
			
		||||
      <span class="flex items-center justify-between">
 | 
			
		||||
        <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
 | 
			
		||||
        <span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
 | 
			
		||||
      </span>
 | 
			
		||||
      <!-- <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
 | 
			
		||||
        <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
 | 
			
		||||
          <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
 | 
			
		||||
        </svg>
 | 
			
		||||
      </span> -->
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full h-full">
 | 
			
		||||
  <div class="w-full h-full overflow-hidden overflow-y-auto px-1">
 | 
			
		||||
    <div class="flex">
 | 
			
		||||
      <cards-book-cover :audiobook="audiobook" />
 | 
			
		||||
      <div class="flex-grow pl-6 pr-2">
 | 
			
		||||
@ -10,6 +10,23 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
 | 
			
		||||
        <div v-if="localCovers.length" class="mb-4 mt-6 border-t border-b border-primary">
 | 
			
		||||
          <div class="flex items-center justify-center py-2">
 | 
			
		||||
            <p>{{ localCovers.length }} local image(s)</p>
 | 
			
		||||
            <div class="flex-grow" />
 | 
			
		||||
            <ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? 'Hide' : 'Show' }}</ui-btn>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div v-if="showLocalCovers" class="flex items-center justify-center">
 | 
			
		||||
            <template v-for="cover in localCovers">
 | 
			
		||||
              <!-- <img :src="`/local/${cover.path}`" :key="cover.path" class="w-20 h-32 object-cover m-0.5" /> -->
 | 
			
		||||
              <div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover.localPath)">
 | 
			
		||||
                <img :src="cover.localPath" class="h-24 object-cover" style="width: 60px" />
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <form @submit.prevent="submitSearchForm">
 | 
			
		||||
          <div class="flex items-center justify-start -mx-1 py-2 mt-2">
 | 
			
		||||
            <div class="flex-grow px-1">
 | 
			
		||||
@ -23,7 +40,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
        <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-72 overflow-y-scroll mt-2 max-w-full">
 | 
			
		||||
        <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-60 overflow-y-scroll mt-2 max-w-full">
 | 
			
		||||
          <p v-if="!coversFound.length">No Covers Found</p>
 | 
			
		||||
          <template v-for="cover in coversFound">
 | 
			
		||||
            <div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
 | 
			
		||||
@ -37,6 +54,8 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import Path from 'path'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  props: {
 | 
			
		||||
    processing: Boolean,
 | 
			
		||||
@ -51,7 +70,8 @@ export default {
 | 
			
		||||
      searchAuthor: null,
 | 
			
		||||
      imageUrl: null,
 | 
			
		||||
      coversFound: [],
 | 
			
		||||
      hasSearched: false
 | 
			
		||||
      hasSearched: false,
 | 
			
		||||
      showLocalCovers: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
@ -73,10 +93,23 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    book() {
 | 
			
		||||
      return this.audiobook ? this.audiobook.book || {} : {}
 | 
			
		||||
    },
 | 
			
		||||
    otherFiles() {
 | 
			
		||||
      return this.audiobook ? this.audiobook.otherFiles || [] : []
 | 
			
		||||
    },
 | 
			
		||||
    localCovers() {
 | 
			
		||||
      return this.otherFiles
 | 
			
		||||
        .filter((f) => f.filetype === 'image')
 | 
			
		||||
        .map((file) => {
 | 
			
		||||
          var _file = { ...file }
 | 
			
		||||
          _file.localPath = Path.join('local', _file.path)
 | 
			
		||||
          return _file
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    init() {
 | 
			
		||||
      this.showLocalCovers = false
 | 
			
		||||
      if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.author)) {
 | 
			
		||||
        this.coversFound = []
 | 
			
		||||
        this.hasSearched = false
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
    <div v-show="processing" class="flex h-full items-center justify-center">
 | 
			
		||||
      <p>Loading...</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-show="!processing && !searchResults.length" class="flex h-full items-center justify-center">
 | 
			
		||||
    <div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
 | 
			
		||||
      <p>No Results</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper">
 | 
			
		||||
@ -37,11 +37,13 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      audiobookId: null,
 | 
			
		||||
      searchTitle: null,
 | 
			
		||||
      searchAuthor: null,
 | 
			
		||||
      lastSearch: null,
 | 
			
		||||
      provider: 'best',
 | 
			
		||||
      searchResults: []
 | 
			
		||||
      searchResults: [],
 | 
			
		||||
      hasSearched: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
@ -90,15 +92,22 @@ export default {
 | 
			
		||||
      })
 | 
			
		||||
      this.searchResults = results
 | 
			
		||||
      this.isProcessing = false
 | 
			
		||||
      this.hasSearched = true
 | 
			
		||||
    },
 | 
			
		||||
    init() {
 | 
			
		||||
      if (this.audiobook.id !== this.audiobookId) {
 | 
			
		||||
        this.searchResults = []
 | 
			
		||||
        this.hasSearched = false
 | 
			
		||||
        this.audiobookId = this.audiobook.id
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.audiobook.book || !this.audiobook.book.title) {
 | 
			
		||||
        this.searchTitle = null
 | 
			
		||||
        this.searchAuthor = null
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      this.searchTitle = this.audiobook.book.title
 | 
			
		||||
      this.searchAuthor = this.audiobook.book.author || ''
 | 
			
		||||
      this.runSearch()
 | 
			
		||||
    },
 | 
			
		||||
    async selectMatch(match) {
 | 
			
		||||
      this.isProcessing = true
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :type="type" :class="classList" @click="click">
 | 
			
		||||
  <button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="loading" :type="type" :class="classList" @click="click">
 | 
			
		||||
    <slot />
 | 
			
		||||
    <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
 | 
			
		||||
      <!-- <span class="material-icons animate-spin">refresh</span> -->
 | 
			
		||||
      <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
 | 
			
		||||
        <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
  </button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@ -16,7 +22,8 @@ export default {
 | 
			
		||||
      default: ''
 | 
			
		||||
    },
 | 
			
		||||
    paddingX: Number,
 | 
			
		||||
    small: Boolean
 | 
			
		||||
    small: Boolean,
 | 
			
		||||
    loading: Boolean
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
@ -24,6 +31,7 @@ export default {
 | 
			
		||||
  computed: {
 | 
			
		||||
    classList() {
 | 
			
		||||
      var list = []
 | 
			
		||||
      if (this.loading) list.push('text-opacity-0')
 | 
			
		||||
      list.push('text-white')
 | 
			
		||||
      list.push(`bg-${this.color}`)
 | 
			
		||||
      if (this.small) {
 | 
			
		||||
@ -61,7 +69,10 @@ button.btn::before {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0);
 | 
			
		||||
  transition: all 0.1s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
button.btn:hover::before {
 | 
			
		||||
button.btn:hover:not(:disabled)::before {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
}
 | 
			
		||||
button:disabled::before {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
 | 
			
		||||
  <input v-model="inputValue" :type="type" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
@ -29,8 +29,17 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    focused() {
 | 
			
		||||
      this.$emit('focus')
 | 
			
		||||
    },
 | 
			
		||||
    blurred() {
 | 
			
		||||
      this.$emit('blur')
 | 
			
		||||
    },
 | 
			
		||||
    change(e) {
 | 
			
		||||
      this.$emit('change', e.target.value)
 | 
			
		||||
    },
 | 
			
		||||
    keyup(e) {
 | 
			
		||||
      this.$emit('keyup', e)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {}
 | 
			
		||||
 | 
			
		||||
@ -136,11 +136,6 @@ export default {
 | 
			
		||||
      this.socket.on('scan_start', this.scanStart)
 | 
			
		||||
      this.socket.on('scan_complete', this.scanComplete)
 | 
			
		||||
      this.socket.on('scan_progress', this.scanProgress)
 | 
			
		||||
    },
 | 
			
		||||
    checkVersion() {
 | 
			
		||||
      this.$axios.$get('http://github.com/advplyr/audiobookshelf/raw/master/package.json').then((data) => {
 | 
			
		||||
        console.log('GOT DATA', data)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  beforeMount() {
 | 
			
		||||
@ -150,7 +145,6 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.initializeSocket()
 | 
			
		||||
    this.checkVersion()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf-client",
 | 
			
		||||
  "version": "0.9.61-beta",
 | 
			
		||||
  "version": "0.9.62-beta",
 | 
			
		||||
  "description": "Audiobook manager and player",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,15 @@
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
        <ui-btn color="success" @click="scan">Scan</ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="h-0.5 bg-primary bg-opacity-50 w-full" />
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center py-4">
 | 
			
		||||
        <ui-btn color="error" small :padding-x="4" :loading="isResettingAudiobooks" @click="resetAudiobooks">Reset All Audiobooks</ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="h-0.5 bg-primary bg-opacity-50 w-full" />
 | 
			
		||||
 | 
			
		||||
      <div class="flex items-center py-4">
 | 
			
		||||
        <p class="font-mono">v{{ $config.version }}</p>
 | 
			
		||||
        <div class="flex-grow" />
 | 
			
		||||
@ -32,7 +40,9 @@
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {}
 | 
			
		||||
    return {
 | 
			
		||||
      isResettingAudiobooks: false
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    streamAudiobook() {
 | 
			
		||||
@ -42,6 +52,22 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    scan() {
 | 
			
		||||
      this.$root.socket.emit('scan')
 | 
			
		||||
    },
 | 
			
		||||
    resetAudiobooks() {
 | 
			
		||||
      if (confirm('WARNING! This action will remove all audiobooks from the database including any updates or matches you have made. This does not do anything to your actual files. Shall we continue?')) {
 | 
			
		||||
        this.isResettingAudiobooks = true
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .$delete('/api/audiobooks')
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            this.isResettingAudiobooks = false
 | 
			
		||||
            this.$toast.success('Successfully reset audiobooks')
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('failed to reset audiobooks', error)
 | 
			
		||||
            this.isResettingAudiobooks = false
 | 
			
		||||
            this.$toast.error('Failed to reset audiobooks - stop docker and manually remove appdata')
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "audiobookshelf",
 | 
			
		||||
  "version": "0.9.61-beta",
 | 
			
		||||
  "version": "0.9.62-beta",
 | 
			
		||||
  "description": "Self-hosted audiobook server for managing and playing audiobooks.",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 | 
			
		||||
@ -17,8 +17,9 @@ class ApiController {
 | 
			
		||||
    this.router.get('/find/covers', this.findCovers.bind(this))
 | 
			
		||||
    this.router.get('/find/:method', this.find.bind(this))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    this.router.get('/audiobooks', this.getAudiobooks.bind(this))
 | 
			
		||||
    this.router.delete('/audiobooks', this.deleteAllAudiobooks.bind(this))
 | 
			
		||||
 | 
			
		||||
    this.router.get('/audiobook/:id', this.getAudiobook.bind(this))
 | 
			
		||||
    this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
 | 
			
		||||
    this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
 | 
			
		||||
@ -57,9 +58,22 @@ class ApiController {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAudiobooks(req, res) {
 | 
			
		||||
    Logger.info('Get Audiobooks')
 | 
			
		||||
    var audiobooksMinified = this.db.audiobooks.map(ab => ab.toJSONMinified())
 | 
			
		||||
    res.json(audiobooksMinified)
 | 
			
		||||
    var audiobooks = []
 | 
			
		||||
    if (req.query.q) {
 | 
			
		||||
      audiobooks = this.db.audiobooks.filter(ab => {
 | 
			
		||||
        return ab.isSearchMatch(req.query.q)
 | 
			
		||||
      }).map(ab => ab.toJSONMinified())
 | 
			
		||||
    } else {
 | 
			
		||||
      audiobooks = this.db.audiobooks.map(ab => ab.toJSONMinified())
 | 
			
		||||
    }
 | 
			
		||||
    res.json(audiobooks)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async deleteAllAudiobooks(req, res) {
 | 
			
		||||
    Logger.info('Removing all Audiobooks')
 | 
			
		||||
    var success = await this.db.recreateAudiobookDb()
 | 
			
		||||
    if (success) res.sendStatus(200)
 | 
			
		||||
    else res.sendStatus(500)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAudiobook(req, res) {
 | 
			
		||||
 | 
			
		||||
@ -207,5 +207,9 @@ class Audiobook {
 | 
			
		||||
      this.addTrack(file)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isSearchMatch(search) {
 | 
			
		||||
    return this.book.isSearchMatch(search.toLowerCase().trim())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = Audiobook
 | 
			
		||||
@ -16,6 +16,10 @@ class Book {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get _title() { return this.title || '' }
 | 
			
		||||
  get _author() { return this.author || '' }
 | 
			
		||||
  get _series() { return this.series || '' }
 | 
			
		||||
 | 
			
		||||
  construct(book) {
 | 
			
		||||
    this.olid = book.olid
 | 
			
		||||
    this.title = book.title
 | 
			
		||||
@ -81,5 +85,9 @@ class Book {
 | 
			
		||||
    }
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isSearchMatch(search) {
 | 
			
		||||
    return this._title.toLowerCase().includes(search) || this._author.toLowerCase().includes(search) || this._series.toLowerCase().includes(search)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = Book
 | 
			
		||||
							
								
								
									
										18
									
								
								server/Db.js
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								server/Db.js
									
									
									
									
									
								
							@ -28,6 +28,12 @@ class Db {
 | 
			
		||||
    return this.settingsDb
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getEntityDbKey(entityName) {
 | 
			
		||||
    if (entityName === 'user') return 'usersDb'
 | 
			
		||||
    else if (entityName === 'audiobook') return 'audiobooksDb'
 | 
			
		||||
    return 'settingsDb'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getEntityArrayKey(entityName) {
 | 
			
		||||
    if (entityName === 'user') return 'users'
 | 
			
		||||
    else if (entityName === 'audiobook') return 'audiobooks'
 | 
			
		||||
@ -155,6 +161,18 @@ class Db {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  recreateAudiobookDb() {
 | 
			
		||||
    return this.audiobooksDb.drop().then((results) => {
 | 
			
		||||
      Logger.info(`[DB] Dropped audiobook db`, results)
 | 
			
		||||
      this.audiobooksDb = new njodb.Database(this.AudiobooksPath)
 | 
			
		||||
      this.audiobooks = []
 | 
			
		||||
      return true
 | 
			
		||||
    }).catch((error) => {
 | 
			
		||||
      Logger.error(`[DB] Failed to drop audiobook db`, error)
 | 
			
		||||
      return false
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getGenres() {
 | 
			
		||||
    var allGenres = []
 | 
			
		||||
    this.db.audiobooks.forEach((audiobook) => {
 | 
			
		||||
 | 
			
		||||
@ -108,8 +108,11 @@ class Server {
 | 
			
		||||
    if (process.env.NODE_ENV === 'production') {
 | 
			
		||||
      const distPath = Path.join(global.appRoot, '/client/dist')
 | 
			
		||||
      app.use(express.static(distPath))
 | 
			
		||||
    }
 | 
			
		||||
      app.use('/local', express.static(this.AudiobookPath))
 | 
			
		||||
    } else {
 | 
			
		||||
      app.use(express.static(this.AudiobookPath))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    app.use(express.static(this.MetadataPath))
 | 
			
		||||
    app.use(express.urlencoded({ extended: true }));
 | 
			
		||||
    app.use(express.json())
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user