Compare commits

..

18 Commits

Author SHA1 Message Date
advplyr 095f49824e Version bump 2.0.12 2022-05-09 18:45:33 -05:00
advplyr b330030f50 Fix:Ignore file metadata updates to metadata.abs files 2022-05-09 18:40:54 -05:00
advplyr a7d422e23f Add:Alternate view for home page, series and collections without wood texture #424 2022-05-09 18:23:23 -05:00
advplyr f51a31c8ca Update:Remove back arrow in appbar 2022-05-09 15:12:55 -05:00
advplyr 290340a385 Fix:Rescan filter out items not updated #577 2022-05-09 07:23:29 -05:00
advplyr 0137f6dfeb Update:Show publishedYear below book cards instead of author name 2022-05-08 18:54:41 -05:00
advplyr 7f27eabf3e Update:Authors page check user can access library items and can edit 2022-05-08 18:48:57 -05:00
advplyr 4f7588c87d Update:Author names to link to authors page 2022-05-08 18:43:24 -05:00
advplyr a19b6370c4 Fix:getBookCoverAspectRatio 2022-05-08 18:40:43 -05:00
advplyr fbd7ae10d1 Add:Authors landing page #187 2022-05-08 18:21:46 -05:00
advplyr f94c706fc8 Update:Item edit modal add Save and Save & Close buttons 2022-05-08 15:25:33 -05:00
advplyr 9de4b1069a Fix:Item edit modal scrollable and overflowing #574 2022-05-08 14:52:58 -05:00
advplyr 8fbe3c3884 Remove unnecessary background-image #569 2022-05-07 20:21:08 -05:00
advplyr abf9120363 Fix:Hide book library settings for podcast libraries #573 2022-05-07 20:10:23 -05:00
advplyr 69f250cba5 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-05-07 20:01:33 -05:00
advplyr 2103edfcdc Update:Max levenshtein distance for matching author names to 3 #572 2022-05-07 20:01:29 -05:00
advplyr 02ba147bd4 Merge pull request #571 from jflattery/main
fix repo
2022-05-07 11:50:27 -05:00
jflattery 230b548921 fix repo 2022-05-07 16:09:59 +00:00
38 changed files with 991 additions and 258 deletions
+42 -24
View File
@@ -12,18 +12,30 @@
height: calc(100% - 64px);
max-height: calc(100% - 64px);
}
.page.streaming {
height: calc(100% - 64px - 165px);
max-height: calc(100% - 64px - 165px);
}
#bookshelf {
height: calc(100% - 40px);
background-image: linear-gradient(to right bottom, #2e2e2e, #303030, #313131, #333333, #353535, #343434, #323232, #313131, #2c2c2c, #282828, #232323, #1f1f1f);
}
.bookshelf-row {
/* Sidebar width + scrollbar width */
width: calc(100vw - 88px);
}
@media (max-width: 768px) {
#bookshelf {
height: calc(100% - 80px);
}
.bookshelf-row {
width: 100vw;
}
}
#page-wrapper {
@@ -34,36 +46,25 @@
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar:horizontal {
height: 8px;
}
/* ::-webkit-scrollbar:horizontal { */
/* height: 16px; */
/* height: 24px;
} */
/* Track */
::-webkit-scrollbar-track {
background-color: rgba(0,0,0,0);
background-color: rgba(0, 0, 0, 0);
}
/* ::-webkit-scrollbar-track:horizontal { */
/* background: rgb(149, 119, 90); */
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
/* background: linear-gradient(180deg, rgb(117, 88, 60) 0%, rgb(65, 41, 17) 17%, rgb(71, 43, 15) 88%, rgb(3, 2, 1) 100%);
box-shadow: 2px 14px 8px #111111aa;
} */
/* Handle */
::-webkit-scrollbar-thumb {
background: #855620;
background: #855620;
border-radius: 4px;
}
/* ::-webkit-scrollbar-thumb:horizontal { */
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
/* box-shadow: 2px 14px 8px #111111aa;
border-radius: 4px;
} */
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #704922;
background: #704922;
}
.no-scroll::-webkit-scrollbar {
@@ -71,6 +72,13 @@
opacity: 0;
}
.no-scroll {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
/* Chrome, Safari, Edge, Opera */
.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button {
@@ -89,18 +97,23 @@ input[type=number] {
width: 100%;
border: 1px solid #474747;
}
.tracksTable tr:nth-child(even) {
background-color: #2e2e2e;
}
.tracksTable tr {
background-color: #373838;
}
.tracksTable tr:hover {
background-color: #474747;
}
.tracksTable td {
padding: 4px 8px;
}
.tracksTable th {
padding: 4px 8px;
font-size: 0.75rem;
@@ -113,13 +126,14 @@ input[type=number] {
border-right: 6px solid transparent;
border-top: 6px solid white;
}
.triangle-right {
width: 0;
height: 0;
border-left: 8px solid transparent;
border-bottom: 8px solid transparent;
border-top: 8px solid rgb(34,127,35);
border-right: 8px solid rgb(34,127,35);
border-top: 8px solid rgb(34, 127, 35);
border-right: 8px solid rgb(34, 127, 35);
}
.icon-text {
@@ -149,6 +163,7 @@ input[type=number] {
.box-shadow-book {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
.shadow-height {
height: calc(100% - 4px);
}
@@ -165,9 +180,9 @@ input[type=number] {
Bookshelf Label
*/
.categoryPlacard {
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
letter-spacing: 1px;
}
.shinyBlack {
background-color: #2d3436;
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
@@ -194,8 +209,11 @@ Bookshelf Label
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px; /* fallback */
max-height: 32px; /* fallback */
-webkit-line-clamp: 2; /* number of lines to show */
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
+7 -8
View File
@@ -2,11 +2,13 @@
<div class="w-full h-16 bg-primary relative">
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
<div class="flex h-full items-center">
<img v-if="!showBack" src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" />
<a v-if="showBack" @click="back" class="rounded-full h-12 w-12 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-4 cursor-pointer">
<span class="material-icons text-4xl text-white">arrow_back</span>
</a>
<h1 class="text-2xl font-book mr-6 hidden lg:block">audiobookshelf</h1>
<nuxt-link to="/">
<img src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" />
</nuxt-link>
<nuxt-link to="/">
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
</nuxt-link>
<ui-libraries-dropdown />
@@ -94,9 +96,6 @@ export default {
isHome() {
return this.$route.name === 'library-library'
},
showBack() {
return this.$route.name !== 'library-library-bookshelf-id' && !this.isHome
},
user() {
return this.$store.state.user.user
},
+27 -3
View File
@@ -1,9 +1,9 @@
<template>
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
<!-- Cover size widget -->
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
<!-- Experimental Bookshelf Texture -->
<div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40">
<div v-show="showExperimentalFeatures && !isAlternativeBookshelfView" class="fixed bottom-4 right-28 z-40">
<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>
@@ -17,7 +17,25 @@
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
<p class="text-center text-xl font-book py-4">No results for query</p>
</div>
<div v-else class="w-full flex flex-col items-center">
<!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
<template v-for="(shelf, index) in shelves">
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
</widgets-item-slider>
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
</widgets-episode-slider>
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
</widgets-series-slider>
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ shelf.label }}</p>
</widgets-authors-slider>
</template>
</div>
<!-- Regular bookshelf view -->
<div v-else class="w-full">
<template v-for="(shelf, index) in shelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</template>
@@ -56,6 +74,12 @@ export default {
libraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
bookshelfView() {
return this.$store.getters['getServerSetting']('bookshelfView')
},
isAlternativeBookshelfView() {
return this.bookshelfView === this.$constants.BookshelfView.TITLES
},
bookCoverWidth() {
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
+4 -29
View File
@@ -1,6 +1,6 @@
<template>
<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', height: shelfHeight + 'px' }" @scroll="scrolled">
<div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled">
<div class="w-full h-full pt-6">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
@@ -17,18 +17,9 @@
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
</template>
</div>
<div v-if="shelf.type === 'tags'" class="flex items-center">
<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-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" />
</nuxt-link>
</template>
</div>
<div v-if="shelf.type === 'authors'" class="flex items-center">
<template v-for="entity in shelf.entities">
<nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.id)}`">
<cards-author-card :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
</nuxt-link>
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
</template>
</div>
</div>
@@ -48,7 +39,6 @@
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
<span class="material-icons text-6xl text-white">chevron_right</span>
</div>
<modals-authors-edit-modal v-model="showAuthorModal" :author="selectedAuthor" />
</div>
</template>
@@ -70,9 +60,7 @@ export default {
canScrollLeft: false,
isScrolling: false,
scrollTimer: null,
updateTimer: null,
showAuthorModal: false,
selectedAuthor: null
updateTimer: null
}
},
computed: {
@@ -98,8 +86,7 @@ export default {
this.updateSelectionMode(false)
},
editAuthor(author) {
this.selectedAuthor = author
this.showAuthorModal = true
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
var itemIds = this.shelf.entities.map((e) => e.id)
@@ -197,25 +184,13 @@ export default {
<style>
.categorizedBookshelfRow {
scroll-behavior: smooth;
width: calc(100vw - 80px);
/* background-color: rgb(214, 116, 36); */
background-image: var(--bookshelf-texture-img);
/* background-position: center; */
/* background-size: contain; */
background-repeat: repeat-x;
}
@media (max-width: 768px) {
.categorizedBookshelfRow {
width: 100vw;
}
}
.bookshelfDividerCategorized {
background: rgb(149, 119, 90);
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
background: linear-gradient(180deg, rgb(122, 94, 68) 0%, rgb(92, 62, 31) 17%, rgb(82, 54, 26) 88%, rgba(71, 48, 25, 1) 100%);
/* background: linear-gradient(180deg, rgb(114, 85, 59) 0%, rgb(73, 48, 22) 17%, rgb(71, 43, 15) 88%, rgb(61, 41, 20) 100%); */
box-shadow: 2px 14px 8px #111111aa;
}
+7 -3
View File
@@ -22,8 +22,9 @@
</div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
<!-- Experimental Bookshelf Texture -->
<div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40">
<div v-show="showExperimentalFeatures && !isAlternativeBookshelfView" class="fixed bottom-4 right-28 z-40">
<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>
@@ -126,7 +127,7 @@ export default {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
},
isAlternativeBookshelfView() {
if (!this.isEntityBook) return false // Only used for bookshelf showing books
// if (!this.isEntityBook) return false // Only used for bookshelf showing books
return this.bookshelfView === this.$constants.BookshelfView.TITLES
},
bookCoverAspectRatio() {
@@ -185,7 +186,10 @@ export default {
return 6
},
shelfHeight() {
if (this.isAlternativeBookshelfView) return this.entityHeight + 80 * this.sizeMultiplier
if (this.isAlternativeBookshelfView) {
var extraTitleSpace = this.isEntityBook ? 80 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40
},
totalEntityCardWidth() {
+1 -1
View File
@@ -12,7 +12,7 @@
<span class="material-icons text-sm">person</span>
<p v-if="podcastAuthor">{{ podcastAuthor }}</p>
<p v-else-if="authors.length" class="pl-1.5 text-sm sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p>
<p v-else class="text-sm sm:text-base cursor-pointer pl-2">Unknown</p>
</div>
+26 -24
View File
@@ -1,32 +1,34 @@
<template>
<div @mouseover="mouseover" @mouseout="mouseout">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder -->
<covers-author-image :author="author" />
<nuxt-link :to="`/author/${author.id}`">
<div @mouseover="mouseover" @mouseleave="mouseleave">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder -->
<covers-author-image :author="author" />
<!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
<!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
<!-- Search icon btn -->
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="searchAuthor">
<span class="material-icons text-lg">search</span>
</div>
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="$emit('edit', author)">
<span class="material-icons text-lg">edit</span>
</div>
<!-- Search icon btn -->
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
<span class="material-icons text-lg">search</span>
</div>
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
<span class="material-icons text-lg">edit</span>
</div>
<!-- Loading spinner -->
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" />
<!-- Loading spinner -->
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" />
</div>
</div>
<div v-show="nameBelow" class="w-full py-1 px-2">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
</div>
</div>
<div v-show="nameBelow" class="w-full py-1 px-2">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
</div>
</div>
</nuxt-link>
</template>
<script>
@@ -74,7 +76,7 @@ export default {
mouseover() {
this.isHovering = true
},
mouseout() {
mouseleave() {
this.isHovering = false
},
async searchAuthor() {
+13 -6
View File
@@ -6,11 +6,11 @@
</div>
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || '&nbsp;' }}</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
@@ -52,7 +52,7 @@
</div>
</div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50 top-0 right-0" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div>
@@ -61,7 +61,7 @@
</div>
<!-- More Menu Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-100" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :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>
@@ -247,8 +247,11 @@ export default {
}
return this.title
},
displayAuthor() {
displayLineTwo() {
if (this.isPodcast) return this.author
if (this.isAuthorBookshelfView) {
return this.mediaMetadata.publishedYear || ''
}
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author
},
@@ -424,8 +427,12 @@ export default {
var constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView === constants.BookshelfView.TITLES
},
isAuthorBookshelfView() {
var constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView === constants.BookshelfView.AUTHOR
},
titleDisplayBottomOffset() {
if (!this.isAlternativeBookshelfView) return 0
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
}
+13 -7
View File
@@ -5,20 +5,18 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<!-- <div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="toggleSelected">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">radio_button_unchecked</span>
</div> -->
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
</div>
</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 v-if="!isAlternativeBookshelfView" 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 ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
</template>
@@ -28,7 +26,11 @@ export default {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number
bookCoverAspectRatio: Number,
bookshelfView: {
type: Number,
default: 0
}
},
data() {
return {
@@ -58,6 +60,10 @@ export default {
},
currentLibraryId() {
return this.store.state.libraries.currentLibraryId
},
isAlternativeBookshelfView() {
const constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView == constants.BookshelfView.TITLES
}
},
methods: {
+12 -1
View File
@@ -13,11 +13,14 @@
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ title }}</p>
</div>
<div v-if="!isCategorized" 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 v-if="!isAlternativeBookshelfView" 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 ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
</template>
@@ -28,6 +31,10 @@ export default {
width: Number,
height: Number,
bookCoverAspectRatio: Number,
bookshelfView: {
type: Number,
default: 0
},
isCategorized: Boolean,
seriesMount: {
type: Object,
@@ -89,6 +96,10 @@ export default {
hasValidCovers() {
var validCovers = this.books.map((bookItem) => bookItem.media.coverPath)
return !!validCovers.length
},
isAlternativeBookshelfView() {
const constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView == constants.BookshelfView.TITLES
}
},
methods: {
+12 -9
View File
@@ -43,13 +43,13 @@
<script>
export default {
props: {
value: Boolean,
author: {
type: Object,
default: () => {}
}
},
// props: {
// value: Boolean,
// author: {
// type: Object,
// default: () => {}
// }
// },
data() {
return {
authorCopy: {
@@ -73,12 +73,15 @@ export default {
computed: {
show: {
get() {
return this.value
return this.$store.state.globals.showEditAuthorModal
},
set(val) {
this.$emit('input', val)
this.$store.commit('globals/setShowEditAuthorModal', val)
}
},
author() {
return this.$store.state.globals.selectedAuthor
},
authorId() {
if (!this.author) return ''
return this.author.id
+1 -1
View File
@@ -18,7 +18,7 @@
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
</div>
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</div>
</modals-modal>
+22 -13
View File
@@ -1,9 +1,11 @@
<template>
<div class="w-full h-full relative">
<widgets-book-details-edit v-if="mediaType == 'book'" ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="submitForm" />
<div id="formWrapper" class="w-full overflow-y-auto">
<widgets-book-details-edit v-if="mediaType == 'book'" ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
</div>
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'box-shadow-sm-up border-t border-primary border-opacity-50'">
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
<div class="flex items-center px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
@@ -17,7 +19,9 @@
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
</ui-tooltip>
<ui-btn @click="submitForm">Submit</ui-btn>
<ui-btn @click="save" class="mx-2">Save</ui-btn>
<ui-btn @click="saveAndClose">Save & Close</ui-btn>
</div>
</div>
</div>
@@ -142,19 +146,23 @@ export default {
this.rescanning = false
})
},
submitForm() {
async saveAndClose() {
const wasUpdated = await this.save()
if (wasUpdated !== null) this.$emit('close')
},
async save() {
if (this.isProcessing) {
return
return null
}
if (!this.$refs.itemDetailsEdit) {
return
return null
}
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
if (!updatedDetails.hasChanges) {
this.$toast.info('No changes were made')
return
return false
}
this.updateDetails(updatedDetails)
return this.updateDetails(updatedDetails)
},
async updateDetails(updatedDetails) {
this.isProcessing = true
@@ -166,11 +174,12 @@ export default {
if (updateResult) {
if (updateResult.updated) {
this.$toast.success('Item details updated')
this.$emit('close')
return true
} else {
this.$toast.info('No updates were necessary')
}
}
return false
},
removeItem() {
if (confirm(`Are you sure you want to remove this item?\n\n*Does not delete your files, only removes the item from audiobookshelf`)) {
@@ -224,8 +233,8 @@ export default {
</script>
<style scoped>
.details-form-wrapper {
height: calc(100% - 70px);
max-height: calc(100% - 70px);
#formWrapper {
height: calc(100% - 80px);
max-height: calc(100% - 80px);
}
</style>
@@ -8,13 +8,13 @@
</div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*Watcher is disabled globally in server settings</p>
</div>
<div class="py-3">
<div v-if="mediaType == 'book'" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
<p class="pl-4 text-lg">Skip matching books that already have an ASIN</p>
</div>
</div>
<div class="py-3">
<div v-if="mediaType == 'book'" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
<p class="pl-4 text-lg">Skip matching books that already have an ISBN</p>
@@ -37,7 +37,7 @@ export default {
provider: null,
disableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
skipMatchingMediaWithIsbn: false
}
},
computed: {
+112
View File
@@ -0,0 +1,112 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @edit="editAuthor" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio / 1.25
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['getNumLibraryItemsSelected'] > 0
}
},
methods: {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
+51 -53
View File
@@ -1,66 +1,64 @@
<template>
<div class="w-full h-full relative">
<form class="w-full h-full" @submit.prevent="submitForm">
<div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
</div>
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
</div>
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="flex-grow px-1">
<ui-multi-select-query-input ref="seriesSelect" v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
</div>
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
</div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
</div>
<div class="flex mt-2 -mx-1">
<div class="flex-grow px-1">
<ui-multi-select-query-input ref="seriesSelect" v-model="seriesItems" text-key="displayName" label="Series" readonly show-edit @edit="editSeriesItem" @add="addNewSeries" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
</div>
<div class="flex-grow px-1 pt-6">
<div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
</div>
<div class="flex-grow px-1 pt-6">
<div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
</div>
</div>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card :key="item.id" :ref="`slider-episode-${item.recentEpisode.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" class="relative mx-2" @edit="editEpisode" @editPodcast="editPodcast" @select="selectItem" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['getNumLibraryItemsSelected'] > 0
}
},
methods: {
clearSelectedEntities() {
this.updateSelectionMode(false)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
editPodcast(libraryItem) {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
},
selectItem(libraryItem) {
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
this.$nextTick(() => {
this.$eventBus.$emit('item-selected', libraryItem)
})
},
itemSelectedEvt() {
this.updateSelectionMode(this.isSelectionMode)
},
updateSelectionMode(val) {
var selectedLibraryItems = this.$store.state.selectedLibraryItems
this.items.forEach((ent) => {
var component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
component.selected = selectedLibraryItems.includes(ent.id)
})
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
},
beforeDestroy() {
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
</script>
+143
View File
@@ -0,0 +1,143 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['getNumLibraryItemsSelected'] > 0
}
},
methods: {
clearSelectedEntities() {
this.updateSelectionMode(false)
},
editItem(libraryItem) {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
},
selectItem(libraryItem) {
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
this.$nextTick(() => {
this.$eventBus.$emit('item-selected', libraryItem)
})
},
itemSelectedEvt() {
this.updateSelectionMode(this.isSelectionMode)
},
updateSelectionMode(val) {
var selectedLibraryItems = this.$store.state.selectedLibraryItems
this.items.forEach((item) => {
var component = this.$refs[`slider-item-${item.id}`]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
component.selected = selectedLibraryItems.includes(item.id)
})
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
},
beforeDestroy() {
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
</script>
@@ -1,50 +1,48 @@
<template>
<div class="w-full h-full relative">
<form class="w-full h-full" @submit.prevent="submitForm">
<div id="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" label="Author" />
</div>
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
</div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" label="RSS Feed URL" class="mt-2" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" label="Author" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" label="Release Date" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
</div>
<div class="flex-grow px-1 pt-6">
<div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
</div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" label="RSS Feed URL" class="mt-2" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" label="Release Date" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
</div>
<div class="flex-grow px-1 pt-6">
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
</div>
</div>
<div class="flex-grow px-1 pt-6">
<ui-checkbox v-model="autoDownloadEpisodes" label="Auto Download New Episodes" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div>
</form>
</div>
</template>
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.TITLES" class="relative mx-2" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return 2 * (this.cardHeight / this.bookCoverAspectRatio)
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['getNumLibraryItemsSelected'] > 0
}
},
methods: {
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
+1
View File
@@ -11,6 +11,7 @@
<modals-edit-collection-modal />
<modals-bookshelf-texture-modal />
<modals-podcast-edit-episode />
<modals-authors-edit-modal />
<readers-reader />
</div>
</template>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.0.11",
"version": "2.0.12",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
+108
View File
@@ -0,0 +1,108 @@
<template>
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-8" :class="streamLibraryItem ? 'streaming' : ''">
<div class="max-w-6xl mx-auto">
<div class="flex mb-6">
<div class="w-48 min-w-48">
<div class="w-full h-52">
<covers-author-image :author="author" rounded="0" />
</div>
</div>
<div class="flex-grow px-8">
<div class="flex items-center mb-8">
<h1 class="text-2xl">{{ author.name }}</h1>
<button v-if="userCanUpdate" class="w-8 h-8 rounded-full flex items-center justify-center mx-4 cursor-pointer text-gray-300 hover:text-warning transform hover:scale-125 duration-100" @click="editAuthor">
<span class="material-icons text-base">edit</span>
</button>
</div>
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">Description</p>
<p class="text-white max-w-3xl text-sm leading-5">{{ author.description }}</p>
</div>
</div>
<div class="py-4">
<widgets-item-slider :items="libraryItems" :bookshelf-view="$constants.BookshelfView.AUTHOR">
<h2 class="text-lg">{{ libraryItems.length }} Books</h2>
</widgets-item-slider>
</div>
<div v-for="series in authorSeries" :key="series.id" class="py-4">
<widgets-item-slider :items="series.items" :bookshelf-view="$constants.BookshelfView.AUTHOR">
<h2 class="text-lg">{{ series.name }}</h2>
<p class="text-white text-opacity-40 text-base px-2">Series</p>
</widgets-item-slider>
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ store, app, params, redirect }) {
const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => {
console.error('Failed to get author', error)
return null
})
if (!author) {
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
}
return {
author
}
},
data() {
return {}
},
computed: {
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
libraryItems() {
return this.author.libraryItems || []
},
authorSeries() {
return this.author.series || []
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
}
},
methods: {
editAuthor() {
this.$store.commit('globals/showEditAuthorModal', this.author)
},
authorUpdated(author) {
if (author.id === this.author.id) {
console.log('Author was updated', author)
this.author = {
...author,
series: this.authorSeries,
libraryItems: this.libraryItems
}
}
},
authorRemoved(author) {
if (author.id === this.author.id) {
console.warn('Author was removed')
this.$router.replace(`/library/${this.currentLibraryId}/authors`)
}
}
},
mounted() {
if (!this.author) this.$router.replace('/')
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
},
beforeDestroy() {
this.$root.socket.off('author_updated', this.authorUpdated)
this.$root.socket.off('author_removed', this.authorRemoved)
}
}
</script>
+2 -2
View File
@@ -41,7 +41,7 @@
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
<ui-tooltip :text="tooltips.bookshelfView">
<p class="pl-4 text-lg">
Use alternative library bookshelf view
Use alternative bookshelf view
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
@@ -213,7 +213,7 @@ export default {
scannerParseSubtitle: 'Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by " - "<br>i.e. "Book Title - A Subtitle Here" has the subtitle "A Subtitle Here"',
sortingIgnorePrefix: 'i.e. for prefix "the" book title "The Book Title" would sort as "Book Title, The"',
scannerFindCovers: 'If your audiobook does not have an embedded cover or a cover image inside the folder, the scanner will attempt to find a cover.<br>Note: This will extend scan time',
bookshelfView: 'Alternative bookshelf view that shows title & author under book covers',
bookshelfView: 'Alternative view without wooden bookshelf',
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers'
+1 -1
View File
@@ -33,7 +33,7 @@
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/library/${libraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p>
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
@@ -7,15 +7,12 @@
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
<div class="flex flex-wrap justify-center">
<template v-for="author in authors">
<nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`">
<cards-author-card :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
</nuxt-link>
<cards-author-card :key="author.id" :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
</template>
</div>
</div>
</div>
</div>
<modals-authors-edit-modal v-model="showAuthorModal" :author="selectedAuthor" />
</div>
</template>
@@ -40,9 +37,7 @@ export default {
data() {
return {
loading: true,
authors: [],
showAuthorModal: false,
selectedAuthor: null
authors: []
}
},
computed: {
@@ -51,6 +46,9 @@ export default {
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
}
},
methods: {
@@ -68,7 +66,7 @@ export default {
},
authorUpdated(author) {
if (this.selectedAuthor && this.selectedAuthor.id === author.id) {
this.selectedAuthor = author
this.$store.commit('globals/setSelectedAuthor', author)
}
this.authors = this.authors.map((au) => {
if (au.id === author.id) {
@@ -81,8 +79,7 @@ export default {
this.authors = this.authors.filter((au) => au.id !== author.id)
},
editAuthor(author) {
this.selectedAuthor = author
this.showAuthorModal = true
this.$store.commit('globals/showEditAuthorModal', author)
}
},
mounted() {
+2 -1
View File
@@ -21,7 +21,8 @@ const BookCoverAspectRatio = {
const BookshelfView = {
STANDARD: 0,
TITLES: 1
TITLES: 1,
AUTHOR: 2 // Books shown on author page
}
const PlayMethod = {
+12
View File
@@ -6,8 +6,10 @@ export const state = () => ({
showUserCollectionsModal: false,
showEditCollectionModal: false,
showEditPodcastEpisode: false,
showEditAuthorModal: false,
selectedEpisode: null,
selectedCollection: null,
selectedAuthor: null,
showBookshelfTextureModal: false,
isCasting: false, // Actively casting
isChromecastInitialized: false // Script loaded
@@ -61,6 +63,16 @@ export const mutations = {
setShowBookshelfTextureModal(state, val) {
state.showBookshelfTextureModal = val
},
showEditAuthorModal(state, author) {
state.selectedAuthor = author
state.showEditAuthorModal = true
},
setShowEditAuthorModal(state, val) {
state.showEditAuthorModal = val
},
setSelectedAuthor(state, author) {
state.selectedAuthor = author
},
setChromecastInitialized(state, val) {
state.isChromecastInitialized = val
},
+1 -1
View File
@@ -33,7 +33,7 @@ export const getters = {
return state.serverSettings[key]
},
getBookCoverAspectRatio: state => {
if (!state.serverSettings || !state.serverSettings.coverAspectRatio) return 1
if (!state.serverSettings || isNaN(state.serverSettings.coverAspectRatio)) return 1
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
},
getNumLibraryItemsSelected: state => state.selectedLibraryItems.length,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.0.11",
"version": "2.0.12",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
+1 -1
View File
@@ -64,7 +64,7 @@ Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join
Available in Unraid Community Apps
```bash
docker pull advplyr/audiobookshelf
docker pull ghcr.io/advplyr/audiobookshelf
docker run -d \
-e AUDIOBOOKSHELF_UID=99 \
+51 -1
View File
@@ -1,11 +1,60 @@
const Logger = require('../Logger')
const { reqSupportsWebp } = require('../utils/index')
const { createNewSortInstance } = require('fast-sort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
class AuthorController {
constructor() { }
async findOne(req, res) {
return res.json(req.author)
const include = (req.query.include || '').split(',')
const authorJson = req.author.toJSON()
// Used on author landing page to include library items and items grouped in series
if (include.includes('items')) {
authorJson.libraryItems = this.db.libraryItems.filter(li => {
if (!req.user.checkCanAccessLibraryItem(li)) return false // filter out library items user cannot access
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
})
if (include.includes('series')) {
const seriesMap = {}
// Group items into series
authorJson.libraryItems.forEach((li) => {
if (li.media.metadata.series) {
li.media.metadata.series.forEach((series) => {
const itemWithSeries = li.toJSONMinified()
itemWithSeries.media.metadata.series = series
if (seriesMap[series.id]) {
seriesMap[series.id].items.push(itemWithSeries)
} else {
seriesMap[series.id] = {
id: series.id,
name: series.name,
items: [itemWithSeries]
}
}
})
}
})
// Sort series items
for (const key in seriesMap) {
seriesMap[key].items = naturalSort(seriesMap[key].items).asc(li => li.media.metadata.series.sequence)
}
authorJson.series = Object.values(seriesMap)
}
// Minify library items
authorJson.libraryItems = authorJson.libraryItems.map(li => li.toJSONMinified())
}
return res.json(authorJson)
}
async update(req, res) {
@@ -41,6 +90,7 @@ class AuthorController {
}).length
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
res.json({
author: req.author.toJSON(),
updated: hasUpdated
+1 -6
View File
@@ -379,13 +379,8 @@ class LibraryItemController {
var item = this.db.libraryItems.find(li => li.id === req.params.id)
if (!item || !item.media) return res.sendStatus(404)
// Check user can access this library
if (!req.user.checkCanAccessLibrary(item.libraryId)) {
return res.sendStatus(403)
}
// Check user can access this library item
if (!req.user.checkCanAccessLibraryItemWithTags(item.media.tags)) {
if (!req.user.checkCanAccessLibraryItem(item)) {
return res.sendStatus(403)
}
+1 -1
View File
@@ -21,7 +21,7 @@ class AuthorFinder {
async findAuthorByName(name, options = {}) {
if (!name) return null
const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 2
const maxLevenshtein = !isNaN(options.maxLevenshtein) ? Number(options.maxLevenshtein) : 3
var author = await this.audnexus.findAuthorByName(name, maxLevenshtein)
if (!author || !author.name) {
+1 -2
View File
@@ -288,7 +288,6 @@ class LibraryItem {
// FileMetadata keys
['filename', 'ext', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'size'].forEach((key) => {
if (existingFile.metadata[key] !== fileFound.metadata[key]) {
// Add modified flag on file data object if exists and was changed
if (key === 'mtimeMs' && existingFile.metadata[key]) {
fileFound.metadata.wasModified = true
@@ -348,7 +347,7 @@ class LibraryItem {
var fileFoundCheck = this.checkFileFound(lf, true)
if (fileFoundCheck === null) {
newLibraryFiles.push(lf)
} else if (fileFoundCheck) {
} else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
hasUpdated = true
existingLibraryFiles.push(lf)
} else {
+5
View File
@@ -341,6 +341,11 @@ class User {
return this.itemTagsAccessible.some(tag => tags.includes(tag))
}
checkCanAccessLibraryItem(libraryItem) {
if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false
return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags)
}
findBookmark(libraryItemId, time) {
return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time)
}
+1 -1
View File
@@ -27,7 +27,7 @@ class Audnexus {
})
}
async findAuthorByName(name, maxLevenshtein = 2) {
async findAuthorByName(name, maxLevenshtein = 3) {
Logger.debug(`[Audnexus] Looking up author by name ${name}`)
var asins = await this.authorASINsRequest(name)
var matchingAsin = asins.find(obj => levenshteinDistance(obj.name, name) <= maxLevenshtein)
+6 -7
View File
@@ -291,12 +291,13 @@ class Scanner {
return this.rescanLibraryItem(lid, libraryScan)
}))
itemsUpdated = itemsUpdated.filter(li => li) // Filter out nulls
for (const libraryItem of itemsUpdated) {
// Temp authors & series are inserted - create them if found
await this.createNewAuthorsAndSeries(libraryItem)
}
itemsUpdated = itemsUpdated.filter(li => li) // Filter out nulls
if (itemsUpdated.length) {
libraryScan.resultsUpdated += itemsUpdated.length
await this.db.updateLibraryItems(itemsUpdated)
@@ -728,16 +729,14 @@ class Scanner {
var libraryItem = itemsInLibrary[i]
if (libraryItem.media.metadata.asin && library.settings.skipMatchingMediaWithAsin) {
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${
libraryItem.media.metadata.title
}" because it already has an ASIN (${i + 1} of ${itemsInLibrary.length})`)
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title
}" because it already has an ASIN (${i + 1} of ${itemsInLibrary.length})`)
continue;
}
if (libraryItem.media.metadata.isbn && library.settings.skipMatchingMediaWithIsbn) {
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${
libraryItem.media.metadata.title
}" because it already has an ISBN (${i + 1} of ${itemsInLibrary.length})`)
Logger.debug(`[Scanner] matchLibraryItems: Skipping "${libraryItem.media.metadata.title
}" because it already has an ISBN (${i + 1} of ${itemsInLibrary.length})`)
continue;
}