mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-06 18:52:43 +02:00
Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c58a6b9047 | |||
| b787fb18f3 | |||
| 17cce9c914 | |||
| 90299e348c | |||
| fe25a1bc54 | |||
| edbe1851b5 | |||
| ad6c5a4f00 | |||
| 4971787482 | |||
| 56d2ec9c22 | |||
| 106ddc9541 | |||
| 4d93e39fa9 | |||
| 54b41b15c2 | |||
| 54ca42a903 | |||
| d7cc8a052a | |||
| 5165f11460 | |||
| b47ce4fb24 | |||
| 9b1f7f566f | |||
| 10295b000a | |||
| c06d734d5e | |||
| 49a69193d8 | |||
| 7852804a9c | |||
| 415dda37a4 | |||
| 179d339afd | |||
| 858c1a7353 | |||
| 0b42b81558 | |||
| f9678dec2f | |||
| 82642b295c | |||
| ba3d84a924 | |||
| 96e2f934a3 | |||
| a68ade2b3d | |||
| 4fcdeda447 | |||
| dc03835742 | |||
| 50430e6b27 | |||
| d130dd6d5e | |||
| 793cc989de | |||
| 27d8c4d67c | |||
| 48f493a9f5 | |||
| 04992ee3fb | |||
| 4d8e2a1279 | |||
| 2af7b6b6f1 | |||
| e59351566d | |||
| 05d10b73c3 | |||
| 41e192c6a5 | |||
| ea42ab7624 | |||
| 2d9035d90b | |||
| 0ae853c119 | |||
| 3c0fdff7b4 | |||
| eede2bbd46 | |||
| 5c31687a0f | |||
| 6b654d3c2d | |||
| 91cbe45839 | |||
| 7883d4a97f | |||
| 9f4547cff8 | |||
| a98106593d | |||
| c625b3f08c | |||
| 9e7f09c21b | |||
| 616caecdf1 | |||
| cee19c5128 | |||
| 67db41a525 | |||
| 3ea3e55d17 | |||
| 4959a28485 | |||
| 6d2482a98e | |||
| 4b23b842bb | |||
| 07bebc8808 | |||
| 027d7f7a5b | |||
| 6baa0fa047 | |||
| 8425fac543 | |||
| 7b2ac7b9e9 | |||
| bf071be247 | |||
| 6c05a0af8a | |||
| 0e292c64c4 | |||
| 725f8eecdb | |||
| 521a673094 | |||
| d917f0e37d | |||
| 7ed5b1744f | |||
| c9ab2a242d | |||
| 13532cba14 | |||
| 3fb2bd3362 | |||
| e80c3a1c5a | |||
| e04d26307e | |||
| b8f74e1c98 | |||
| 0851050392 | |||
| b84882d9d1 | |||
| cd37a7618e | |||
| 64a7cfac3b | |||
| 1ee7ba54f8 | |||
| 6bb18f8800 | |||
| b26b854963 | |||
| 7d58361ced | |||
| a3723f3d06 | |||
| 78d1cd0cfb | |||
| d41366a417 | |||
| a2347150a2 | |||
| d33f23dede | |||
| cfca2be1b2 | |||
| 73f07c1392 | |||
| 4541e9ddc3 | |||
| 972271a1a9 | |||
| e97d92a8ac | |||
| 9a73e352d1 | |||
| 08f09f81fa | |||
| c72609013c | |||
| 29a6434fdc | |||
| eb2ea9950a | |||
| e307ded192 | |||
| 2d6c997b38 | |||
| 232a80a848 | |||
| 083f8faa46 | |||
| 0fcf978ffe | |||
| c1360267c6 | |||
| 084bea6b15 | |||
| 2032dd88ba | |||
| b11b1be432 | |||
| b743b34fab | |||
| 950d10091d | |||
| af0e02b9a2 | |||
| 1332147c4a | |||
| f07cb1e7a3 | |||
| 53dbdd115f | |||
| a217ed5574 | |||
| 531f947754 | |||
| c957e9483e | |||
| 623a706555 | |||
| 7e171576e0 | |||
| 0979b3e03d | |||
| 1131bfa751 | |||
| f9b87b94bf | |||
| 59ed2ec87f | |||
| 7b0b79e3a1 | |||
| 53f73e1201 | |||
| c62a1dfff0 | |||
| 61f8055493 | |||
| 000d7fd249 | |||
| 087de03a1f | |||
| a3ca6159fb | |||
| 5de6ee136a | |||
| d5a19f2b42 | |||
| e3ec5dd506 | |||
| 762748225d | |||
| 4db34e0c56 | |||
| fb078d05bc | |||
| f59edffa43 | |||
| 7aa0ddb71f | |||
| a0a6256c7a | |||
| df7e331605 | |||
| 8c23704e17 | |||
| 12abb1731c | |||
| 180293ebc1 | |||
| e2af33e136 | |||
| 42e68edc65 | |||
| 47e732c213 | |||
| 77a86d92f4 | |||
| 64a8a046c1 | |||
| 1f02cbddd3 | |||
| 5e7bca02b3 | |||
| 097f9549b1 | |||
| 45434b16e0 | |||
| 6af5ac2be1 | |||
| 34ff7efa27 | |||
| 8f4391003f | |||
| ecefb30f3d | |||
| a8162b57ba | |||
| b0edac4234 | |||
| 98c4045a71 | |||
| 24e90e2ead | |||
| 145e0217b6 | |||
| e5925fb1b6 | |||
| 9e416d02bd | |||
| 82b7068130 | |||
| 579ee36857 | |||
| 4f2d7a519d | |||
| a3642d92c5 | |||
| 224f36164f | |||
| 638c220ae8 | |||
| 51070b3e7b | |||
| 0aa2723063 | |||
| 1af66c8e8b | |||
| 7df8795d52 | |||
| a0e9ae7092 | |||
| 0f0d8e317a | |||
| 3d5ca7d5c4 | |||
| e33104fa2b | |||
| a2f1723642 | |||
| 93357cf280 | |||
| 767427c787 | |||
| 9377631896 | |||
| d08af094b8 | |||
| c307b1e6fb | |||
| d387d5b758 | |||
| c285dd666d | |||
| b37b382ea7 | |||
| a2cd755ffa | |||
| 340aedfe13 | |||
| 6fafa7a75e | |||
| 03df5aaf42 | |||
| 6d84db08a8 | |||
| 1a5e0d2a5e | |||
| 70d887bada | |||
| ee0ac00f80 | |||
| fdfb07ff2c | |||
| b648155170 | |||
| 59dc5299b1 | |||
| 357a63a4d9 | |||
| 94912c7542 | |||
| fae182b328 | |||
| 9ba2f3e33a | |||
| 5ca2bc5d64 | |||
| 442687b198 | |||
| 7e400d3e9c | |||
| e3ba739db5 | |||
| cd92a22f4d | |||
| d24ed98bcd | |||
| bcd224f534 | |||
| 1a93103e50 | |||
| 45ccf9d4be | |||
| 052a8307b3 | |||
| a0c0b9ea76 | |||
| 7485cf1a26 | |||
| 8931702f1b | |||
| 00fae3eb16 | |||
| 003e8e17be | |||
| edd9443d51 | |||
| b93a4c6792 | |||
| 30cf144090 | |||
| f17abef20a | |||
| 937438800e | |||
| 892fb6410c | |||
| 7008267e42 | |||
| 2e5e02472c | |||
| f9d37228cf | |||
| f48d52a489 | |||
| 7d8c8fa5bb | |||
| 96a739e22d | |||
| c3ec036009 | |||
| c7794e00f6 | |||
| 3316394f5c | |||
| c5d66989a6 | |||
| e6b886a511 | |||
| 9bdfb05ea6 | |||
| 52d02b32f7 | |||
| adff5a7705 | |||
| 60fb4090ff | |||
| dd28be0113 | |||
| 5a60bb8267 | |||
| 2749b710e6 | |||
| 55ddcde631 | |||
| 4d2bcfd167 |
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discord
|
||||||
|
url: https://discord.gg/pJsjuNCKRq
|
||||||
|
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||||
|
- name: Matrix
|
||||||
|
url: https://matrix.to/#/#audiobookshelf:matrix.org
|
||||||
|
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||||
@@ -11,7 +11,6 @@ ExecReload=/bin/kill -HUP $MAINPID
|
|||||||
Restart=always
|
Restart=always
|
||||||
User=audiobookshelf
|
User=audiobookshelf
|
||||||
Group=audiobookshelf
|
Group=audiobookshelf
|
||||||
PermissionsStartOnly=true
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons:not([class*="text-"]) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +44,10 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons-outlined:not([class*="text-"]) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Gentium Book Basic';
|
font-family: 'Gentium Book Basic';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@@ -20,42 +20,67 @@
|
|||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-enter-to, .slide-leave {
|
.slide-enter-to,
|
||||||
|
.slide-leave {
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-enter, .slide-leave-to {
|
.slide-enter,
|
||||||
|
.slide-leave-to {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.menu-enter, .menu-leave-active {
|
.menu-enter,
|
||||||
|
.menu-leave-active {
|
||||||
transform: translateY(-15px);
|
transform: translateY(-15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-enter-active {
|
.menu-enter-active {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-enter,
|
.menu-enter,
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter, .menux-leave-active {
|
.menux-enter,
|
||||||
|
.menux-leave-active {
|
||||||
transform: translateX(15px);
|
transform: translateX(15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter-active {
|
.menux-enter-active {
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-leave-active {
|
.menux-leave-active {
|
||||||
transition: all 0.1s;
|
transition: all 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menux-enter,
|
.menux-enter,
|
||||||
.menux-leave-active {
|
.menux-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.list-complete-item {
|
||||||
|
transition: all 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-complete-enter-from,
|
||||||
|
.list-complete-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-complete-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<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 id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
@@ -18,22 +18,28 @@
|
|||||||
<widgets-notification-widget class="hidden md:block" />
|
<widgets-notification-widget class="hidden md:block" />
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
|
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
|
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||||
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
@@ -41,13 +47,17 @@
|
|||||||
<span class="block truncate">{{ username }}</span>
|
<span class="block truncate">{{ username }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
||||||
<span class="material-icons text-gray-100">person</span>
|
<span class="material-icons text-xl text-gray-100">person</span>
|
||||||
</span>
|
</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="numLibraryItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
<div v-show="numMediaItemsSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||||
<h1 class="text-2xl px-4">{{ $getString('MessageItemsSelected', [numLibraryItemsSelected]) }}</h1>
|
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||||
|
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
|
{{ $strings.ButtonPlay }}
|
||||||
|
</ui-btn>
|
||||||
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -57,16 +67,16 @@
|
|||||||
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
<template v-if="userCanUpdate">
|
||||||
<ui-tooltip text="Edit" direction="bottom">
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
<ui-tooltip v-if="userCanDelete" :text="$strings.ButtonRemove" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
<ui-icon-btn :disabled="processingBatch" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
|
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom">
|
||||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,9 +87,7 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingBatchDelete: false,
|
totalEntities: 0
|
||||||
totalEntities: 0,
|
|
||||||
isAllSelected: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -107,11 +115,14 @@ export default {
|
|||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : 'err'
|
return this.user ? this.user.username : 'err'
|
||||||
},
|
},
|
||||||
numLibraryItemsSelected() {
|
numMediaItemsSelected() {
|
||||||
return this.selectedLibraryItems.length
|
return this.selectedMediaItems.length
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems
|
return this.$store.state.globals.selectedMediaItems
|
||||||
|
},
|
||||||
|
selectedMediaItemsArePlayable() {
|
||||||
|
return !this.selectedMediaItems.some((i) => !i.hasTracks)
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
return this.$store.state.user.user.mediaProgress || []
|
return this.$store.state.user.user.mediaProgress || []
|
||||||
@@ -127,8 +138,8 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedIsFinished() {
|
selectedIsFinished() {
|
||||||
// Find an item that is not finished, if none then all items finished
|
// Find an item that is not finished, if none then all items finished
|
||||||
return !this.selectedLibraryItems.find((libraryItemId) => {
|
return !this.selectedMediaItems.find((item) => {
|
||||||
var itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === libraryItemId)
|
const itemProgress = this.userMediaProgress.find((lip) => lip.libraryItemId === item.id)
|
||||||
return !itemProgress || !itemProgress.isFinished
|
return !itemProgress || !itemProgress.isFinished
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -149,18 +160,58 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelSelectionMode() {
|
async playSelectedItems() {
|
||||||
if (this.processingBatchDelete) return
|
this.$store.commit('setProcessingBatch', true)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
|
||||||
|
const libraryItemIds = this.selectedMediaItems.map((i) => i.id)
|
||||||
|
const libraryItems = await this.$axios
|
||||||
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
|
.then((res) => res.libraryItems)
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
|
console.error(errorMsg, error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!libraryItems.length) {
|
||||||
|
this.$store.commit('setProcessingBatch', false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueItems = []
|
||||||
|
libraryItems.forEach((item) => {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: item.id,
|
||||||
|
libraryId: item.libraryId,
|
||||||
|
episodeId: null,
|
||||||
|
title: item.media.metadata.title,
|
||||||
|
subtitle: item.media.metadata.authors.map((au) => au.name).join(', '),
|
||||||
|
caption: '',
|
||||||
|
duration: item.media.duration || null,
|
||||||
|
coverPath: item.media.coverPath || null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: queueItems[0].libraryItemId,
|
||||||
|
queueItems
|
||||||
|
})
|
||||||
|
this.$store.commit('setProcessingBatch', false)
|
||||||
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
|
},
|
||||||
|
cancelSelectionMode() {
|
||||||
|
if (this.processingBatch) return
|
||||||
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
this.isAllSelected = false
|
|
||||||
},
|
},
|
||||||
toggleBatchRead() {
|
toggleBatchRead() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
var newIsFinished = !this.selectedIsFinished
|
const newIsFinished = !this.selectedIsFinished
|
||||||
var updateProgressPayloads = this.selectedLibraryItems.map((lid) => {
|
const updateProgressPayloads = this.selectedMediaItems.map((item) => {
|
||||||
return {
|
return {
|
||||||
libraryItemId: lid,
|
libraryItemId: item.id,
|
||||||
isFinished: newIsFinished
|
isFinished: newIsFinished
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -170,7 +221,7 @@ export default {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch update success!')
|
this.$toast.success('Batch update success!')
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -180,26 +231,23 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
batchDeleteClick() {
|
batchDeleteClick() {
|
||||||
var audiobookText = this.numLibraryItemsSelected > 1 ? `these ${this.numLibraryItemsSelected} items` : 'this item'
|
const audiobookText = this.numMediaItemsSelected > 1 ? `these ${this.numMediaItemsSelected} items` : 'this item'
|
||||||
var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
|
const confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the items from Audiobookshelf`
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
this.processingBatchDelete = true
|
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/batch/delete`, {
|
.$post(`/api/items/batch/delete`, {
|
||||||
libraryItemIds: this.selectedLibraryItems
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch delete success!')
|
this.$toast.success('Batch delete success!')
|
||||||
this.processingBatchDelete = false
|
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch delete failed')
|
this.$toast.error('Batch delete failed')
|
||||||
console.error('Failed to batch delete', error)
|
console.error('Failed to batch delete', error)
|
||||||
this.processingBatchDelete = false
|
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ export default {
|
|||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.bookCoverWidth / baseSize
|
return this.bookCoverWidth / baseSize
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -100,15 +100,15 @@ export default {
|
|||||||
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
const indexOf = shelf.shelfStartIndex + entityShelfIndex
|
||||||
|
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
this.lastItemIndexSelected = indexOf
|
this.lastItemIndexSelected = indexOf
|
||||||
} else {
|
} else {
|
||||||
this.lastItemIndexSelected = -1
|
this.lastItemIndexSelected = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||||
var loopStart = indexOf
|
let loopStart = indexOf
|
||||||
var loopEnd = lastLastItemIndexSelected
|
let loopEnd = lastLastItemIndexSelected
|
||||||
if (indexOf > lastLastItemIndexSelected) {
|
if (indexOf > lastLastItemIndexSelected) {
|
||||||
loopStart = lastLastItemIndexSelected
|
loopStart = lastLastItemIndexSelected
|
||||||
loopEnd = indexOf
|
loopEnd = indexOf
|
||||||
@@ -117,12 +117,12 @@ export default {
|
|||||||
const flattenedEntitiesArray = []
|
const flattenedEntitiesArray = []
|
||||||
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
|
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
|
||||||
|
|
||||||
var isSelecting = false
|
let isSelecting = false
|
||||||
// If any items in this range is not selected then select all otherwise unselect all
|
// If any items in this range is not selected then select all otherwise unselect all
|
||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = flattenedEntitiesArray[i]
|
const thisEntity = flattenedEntitiesArray[i]
|
||||||
if (thisEntity) {
|
if (thisEntity) {
|
||||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -133,13 +133,23 @@ export default {
|
|||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = flattenedEntitiesArray[i]
|
const thisEntity = flattenedEntitiesArray[i]
|
||||||
if (thisEntity) {
|
if (thisEntity) {
|
||||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
const mediaItem = {
|
||||||
|
id: thisEntity.id,
|
||||||
|
mediaType: thisEntity.mediaType,
|
||||||
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
const mediaItem = {
|
||||||
|
id: entity.id,
|
||||||
|
mediaType: entity.mediaType,
|
||||||
|
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -395,8 +405,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('user_updated', this.userUpdated)
|
this.$root.socket.off('user_updated', this.userUpdated)
|
||||||
this.$root.socket.off('author_updated', this.authorUpdated)
|
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -119,14 +119,14 @@ export default {
|
|||||||
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
} else if (this.shelf.type === 'episode') {
|
} else if (this.shelf.type === 'episode') {
|
||||||
this.shelf.entities.forEach((ent) => {
|
this.shelf.entities.forEach((ent) => {
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,34 +2,55 @@
|
|||||||
<div class="w-full h-20 md:h-10 relative">
|
<div class="w-full h-20 md:h-10 relative">
|
||||||
<div class="flex md:hidden h-10 items-center">
|
<div class="flex md:hidden h-10 items-center">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonHome }}</p>
|
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
|
</svg>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonSeries }}</p>
|
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
|
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
|
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<template v-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<!-- Series books page -->
|
||||||
<p v-if="!selectedSeries" class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<template v-if="selectedSeries">
|
||||||
<div v-else class="items-center hidden md:flex w-full">
|
<p class="pl-2 font-book text-base md:text-lg">
|
||||||
<p class="pl-2 font-book text-lg">
|
|
||||||
{{ seriesName }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||||
<span class="font-mono">{{ numShowing }}</span>
|
<span class="font-mono">{{ numShowing }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||||
<ui-btn color="primary" small :loading="processingSeries" class="flex items-center ml-1 sm:ml-4" @click="markSeriesFinished">
|
<ui-btn v-if="!isBatchSelecting" color="primary" small :loading="processingSeries" class="items-center ml-1 sm:ml-4 hidden md:flex" @click="markSeriesFinished">
|
||||||
<div class="h-5 w-5">
|
<div class="h-5 w-5">
|
||||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
@@ -40,25 +61,32 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
|
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
<ui-btn v-if="isSeriesRemovedFromContinueListening && !isBatchSelecting" small :loading="processingSeries" @click="reAddSeriesToContinueListening" class="hidden md:block ml-2"> Re-Add Series to Continue Listening </ui-btn>
|
||||||
|
</template>
|
||||||
|
<!-- library & collections page -->
|
||||||
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||||
|
<p class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
<controls-library-filter-select v-if="isLibraryPage" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
<controls-library-sort-select v-if="isLibraryPage" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||||
<controls-library-filter-select v-if="isSeriesPage" v-model="seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||||
<controls-sort-select v-if="isSeriesPage" v-model="seriesSortBy" :descending.sync="seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
|
||||||
|
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- search page -->
|
||||||
<template v-else-if="page === 'search'">
|
<template v-else-if="page === 'search'">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- authors page -->
|
||||||
<template v-else-if="page === 'authors'">
|
<template v-else-if="page === 'authors'">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="userCanUpdate && authors && authors.length" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,28 +114,30 @@ export default {
|
|||||||
totalEntities: 0,
|
totalEntities: 0,
|
||||||
processingSeries: false,
|
processingSeries: false,
|
||||||
processingIssues: false,
|
processingIssues: false,
|
||||||
processingAuthors: false,
|
processingAuthors: false
|
||||||
seriesSortItems: [
|
|
||||||
{
|
|
||||||
text: 'Name',
|
|
||||||
value: 'name'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Number of Books',
|
|
||||||
value: 'numBooks'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Date Added',
|
|
||||||
value: 'addedAt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Total Duration',
|
|
||||||
value: 'totalDuration'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
seriesSortItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelName,
|
||||||
|
value: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelNumberOfBooks,
|
||||||
|
value: 'numBooks'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAddedAt,
|
||||||
|
value: 'addedAt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTotalDuration,
|
||||||
|
value: 'totalDuration'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
@@ -135,12 +165,21 @@ export default {
|
|||||||
isCollectionsPage() {
|
isCollectionsPage() {
|
||||||
return this.page === 'collections'
|
return this.page === 'collections'
|
||||||
},
|
},
|
||||||
|
isPlaylistsPage() {
|
||||||
|
return this.page === 'playlists'
|
||||||
|
},
|
||||||
isHomePage() {
|
isHomePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
isPodcastSearchPage() {
|
isPodcastSearchPage() {
|
||||||
return this.$route.name === 'library-library-podcast-search'
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
},
|
},
|
||||||
|
isPodcastLatestPage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
|
},
|
||||||
|
isAuthorsPage() {
|
||||||
|
return this.$route.name === 'library-library-authors'
|
||||||
|
},
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.totalEntities
|
return this.totalEntities
|
||||||
},
|
},
|
||||||
@@ -149,8 +188,12 @@ export default {
|
|||||||
if (!this.page) return this.$strings.LabelBooks
|
if (!this.page) return this.$strings.LabelBooks
|
||||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||||
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
if (this.isCollectionsPage) return this.$strings.LabelCollections
|
||||||
|
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
|
seriesId() {
|
||||||
|
return this.selectedSeries ? this.selectedSeries.id : null
|
||||||
|
},
|
||||||
seriesName() {
|
seriesName() {
|
||||||
return this.selectedSeries ? this.selectedSeries.name : null
|
return this.selectedSeries ? this.selectedSeries.name : null
|
||||||
},
|
},
|
||||||
@@ -161,41 +204,39 @@ export default {
|
|||||||
if (!this.seriesProgress) return []
|
if (!this.seriesProgress) return []
|
||||||
return this.seriesProgress.libraryItemIds || []
|
return this.seriesProgress.libraryItemIds || []
|
||||||
},
|
},
|
||||||
|
isBatchSelecting() {
|
||||||
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
|
},
|
||||||
isSeriesFinished() {
|
isSeriesFinished() {
|
||||||
return this.seriesProgress && !!this.seriesProgress.isFinished
|
return this.seriesProgress && !!this.seriesProgress.isFinished
|
||||||
},
|
},
|
||||||
|
isSeriesRemovedFromContinueListening() {
|
||||||
|
if (!this.seriesId) return false
|
||||||
|
return this.$store.getters['user/getIsSeriesRemovedFromContinueListening'](this.seriesId)
|
||||||
|
},
|
||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
},
|
},
|
||||||
isIssuesFilter() {
|
isIssuesFilter() {
|
||||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||||
},
|
|
||||||
seriesSortBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortBy', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesSortDesc: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesSortDesc', val)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
seriesFilterBy: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
this.$store.commit('libraries/setSeriesFilterBy', val)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
reAddSeriesToContinueListening() {
|
||||||
|
this.processingSeries = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Series re-added to continue listening')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to re-add series to continue listening', error)
|
||||||
|
this.$toast.error('Failed to re-add series to continue listening')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processingSeries = false
|
||||||
|
})
|
||||||
|
},
|
||||||
async matchAllAuthors() {
|
async matchAllAuthors() {
|
||||||
this.processingAuthors = true
|
this.processingAuthors = true
|
||||||
|
|
||||||
@@ -233,12 +274,13 @@ export default {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Removed library items with issues')
|
this.$toast.success('Removed library items with issues')
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
this.processingIssues = false
|
|
||||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove library items with issues', error)
|
console.error('Failed to remove library items with issues', error)
|
||||||
this.$toast.error('Failed to remove library items with issues')
|
this.$toast.error('Failed to remove library items with issues')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
this.processingIssues = false
|
this.processingIssues = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -273,10 +315,10 @@ export default {
|
|||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesSort() {
|
updateSeriesSort() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateSeriesFilter() {
|
updateSeriesFilter() {
|
||||||
this.$eventBus.$emit('series-sort-updated')
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateCollapseSeries() {
|
updateCollapseSeries() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
@@ -301,11 +343,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ export default {
|
|||||||
id: 'config-notifications',
|
id: 'config-notifications',
|
||||||
title: this.$strings.HeaderNotifications,
|
title: this.$strings.HeaderNotifications,
|
||||||
path: '/config/notifications'
|
path: '/config/notifications'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config-item-metadata-utils',
|
||||||
|
title: this.$strings.HeaderItemMetadataUtils,
|
||||||
|
path: '/config/item-metadata-utils'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||||
<!-- Clear filter only available on Library bookshelf -->
|
<!-- Clear filter only available on Library bookshelf -->
|
||||||
<div v-if="entityName === 'books'" class="flex justify-center mt-2">
|
<div v-if="entityName === 'books'" class="flex justify-center mt-2">
|
||||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">Clear Filter</ui-btn>
|
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,27 +85,28 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||||
},
|
},
|
||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return 'You have no series'
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
|
||||||
|
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return 'No Issues'
|
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
|
||||||
else if (this.filterName === 'Feed-open') return 'No RSS feeds are open'
|
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
|
||||||
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue])
|
||||||
}
|
}
|
||||||
return 'No results'
|
return this.$strings.MessageNoResults
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (!this.page) return 'books'
|
if (!this.page) return 'books'
|
||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
seriesSortBy() {
|
seriesSortBy() {
|
||||||
return this.$store.state.libraries.seriesSortBy
|
return this.$store.getters['user/getUserSetting']('seriesSortBy')
|
||||||
},
|
},
|
||||||
seriesSortDesc() {
|
seriesSortDesc() {
|
||||||
return this.$store.state.libraries.seriesSortDesc
|
return this.$store.getters['user/getUserSetting']('seriesSortDesc')
|
||||||
},
|
},
|
||||||
seriesFilterBy() {
|
seriesFilterBy() {
|
||||||
return this.$store.state.libraries.seriesFilterBy
|
return this.$store.getters['user/getUserSetting']('seriesFilterBy')
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
@@ -162,11 +163,11 @@ export default {
|
|||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 1.6
|
return this.bookWidth * 1.6
|
||||||
},
|
},
|
||||||
shelfPadding() {
|
shelfPadding() {
|
||||||
@@ -200,8 +201,8 @@ export default {
|
|||||||
// Includes margin
|
// Includes margin
|
||||||
return this.entityWidth + 24
|
return this.entityWidth + 24
|
||||||
},
|
},
|
||||||
selectedLibraryItems() {
|
selectedMediaItems() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
@@ -219,6 +220,8 @@ export default {
|
|||||||
this.$store.commit('showEditModal', entity)
|
this.$store.commit('showEditModal', entity)
|
||||||
} else if (this.entityName === 'collections') {
|
} else if (this.entityName === 'collections') {
|
||||||
this.$store.commit('globals/setEditCollection', entity)
|
this.$store.commit('globals/setEditCollection', entity)
|
||||||
|
} else if (this.entityName === 'playlists') {
|
||||||
|
this.$store.commit('globals/setEditPlaylist', entity)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedEntities() {
|
clearSelectedEntities() {
|
||||||
@@ -227,28 +230,28 @@ export default {
|
|||||||
},
|
},
|
||||||
selectEntity(entity, shiftKey) {
|
selectEntity(entity, shiftKey) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedLibraryItems.includes(entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
this.lastItemIndexSelected = indexOf
|
this.lastItemIndexSelected = indexOf
|
||||||
} else {
|
} else {
|
||||||
this.lastItemIndexSelected = -1
|
this.lastItemIndexSelected = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
if (shiftKey && lastLastItemIndexSelected >= 0) {
|
||||||
var loopStart = indexOf
|
let loopStart = indexOf
|
||||||
var loopEnd = lastLastItemIndexSelected
|
let loopEnd = lastLastItemIndexSelected
|
||||||
if (indexOf > lastLastItemIndexSelected) {
|
if (indexOf > lastLastItemIndexSelected) {
|
||||||
loopStart = lastLastItemIndexSelected
|
loopStart = lastLastItemIndexSelected
|
||||||
loopEnd = indexOf
|
loopEnd = indexOf
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSelecting = false
|
let isSelecting = false
|
||||||
// If any items in this range is not selected then select all otherwise unselect all
|
// If any items in this range is not selected then select all otherwise unselect all
|
||||||
for (let i = loopStart; i <= loopEnd; i++) {
|
for (let i = loopStart; i <= loopEnd; i++) {
|
||||||
const thisEntity = this.entities[i]
|
const thisEntity = this.entities[i]
|
||||||
if (thisEntity && !thisEntity.collapsedSeries) {
|
if (thisEntity && !thisEntity.collapsedSeries) {
|
||||||
if (!this.selectedLibraryItems.includes(thisEntity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
|
||||||
isSelecting = true
|
isSelecting = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -266,16 +269,28 @@ export default {
|
|||||||
const entityComponentRef = this.entityComponentRefs[i]
|
const entityComponentRef = this.entityComponentRefs[i]
|
||||||
if (thisEntity && entityComponentRef) {
|
if (thisEntity && entityComponentRef) {
|
||||||
entityComponentRef.selected = isSelecting
|
entityComponentRef.selected = isSelecting
|
||||||
this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting })
|
|
||||||
|
const mediaItem = {
|
||||||
|
id: thisEntity.id,
|
||||||
|
mediaType: thisEntity.mediaType,
|
||||||
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
|
}
|
||||||
|
console.log('Setting media item selected', mediaItem, 'Num Selected=', this.selectedMediaItems.length)
|
||||||
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('toggleLibraryItemSelected', entity.id)
|
const mediaItem = {
|
||||||
|
id: entity.id,
|
||||||
|
mediaType: entity.mediaType,
|
||||||
|
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newIsSelectionMode = !!this.selectedLibraryItems.length
|
const newIsSelectionMode = !!this.selectedMediaItems.length
|
||||||
if (this.isSelectionMode !== newIsSelectionMode) {
|
if (this.isSelectionMode !== newIsSelectionMode) {
|
||||||
this.isSelectionMode = newIsSelectionMode
|
this.isSelectionMode = newIsSelectionMode
|
||||||
this.updateBookSelectionMode(newIsSelectionMode)
|
this.updateBookSelectionMode(newIsSelectionMode)
|
||||||
@@ -301,11 +316,11 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||||
|
|
||||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -483,7 +498,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
var wasUpdated = this.checkUpdateSearchParams()
|
const wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
|
||||||
@@ -560,6 +575,33 @@ export default {
|
|||||||
this.executeRebuild()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
playlistAdded(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistAdded ${playlist.id}`, playlist)
|
||||||
|
this.resetEntities()
|
||||||
|
},
|
||||||
|
playlistUpdated(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistUpdated ${playlist.id}`, playlist)
|
||||||
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities[indexOf] = playlist
|
||||||
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
|
this.entityComponentRefs[indexOf].setEntity(playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playlistRemoved(playlist) {
|
||||||
|
if (this.entityName !== 'playlists') return
|
||||||
|
console.log(`[LazyBookshelf] playlistRemoved ${playlist.id}`, playlist)
|
||||||
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === playlist.id)
|
||||||
|
if (indexOf >= 0) {
|
||||||
|
this.entities = this.entities.filter((ent) => ent.id !== playlist.id)
|
||||||
|
this.totalEntities--
|
||||||
|
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||||
|
this.executeRebuild()
|
||||||
|
}
|
||||||
|
},
|
||||||
initSizeData(_bookshelf) {
|
initSizeData(_bookshelf) {
|
||||||
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
var bookshelf = _bookshelf || document.getElementById('bookshelf')
|
||||||
if (!bookshelf) {
|
if (!bookshelf) {
|
||||||
@@ -625,11 +667,9 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$eventBus.$on('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('socket_init', this.socketInit)
|
this.$eventBus.$on('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
this.$root.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
@@ -640,6 +680,9 @@ export default {
|
|||||||
this.$root.socket.on('collection_added', this.collectionAdded)
|
this.$root.socket.on('collection_added', this.collectionAdded)
|
||||||
this.$root.socket.on('collection_updated', this.collectionUpdated)
|
this.$root.socket.on('collection_updated', this.collectionUpdated)
|
||||||
this.$root.socket.on('collection_removed', this.collectionRemoved)
|
this.$root.socket.on('collection_removed', this.collectionRemoved)
|
||||||
|
this.$root.socket.on('playlist_added', this.playlistAdded)
|
||||||
|
this.$root.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
|
this.$root.socket.on('playlist_removed', this.playlistRemoved)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
@@ -651,11 +694,9 @@ export default {
|
|||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('series-sort-updated', this.seriesSortUpdated)
|
|
||||||
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('socket_init', this.socketInit)
|
this.$eventBus.$off('socket_init', this.socketInit)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
|
||||||
|
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
this.$root.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
@@ -666,6 +707,9 @@ export default {
|
|||||||
this.$root.socket.off('collection_added', this.collectionAdded)
|
this.$root.socket.off('collection_added', this.collectionAdded)
|
||||||
this.$root.socket.off('collection_updated', this.collectionUpdated)
|
this.$root.socket.off('collection_updated', this.collectionUpdated)
|
||||||
this.$root.socket.off('collection_removed', this.collectionRemoved)
|
this.$root.socket.off('collection_removed', this.collectionRemoved)
|
||||||
|
this.$root.socket.off('playlist_added', this.playlistAdded)
|
||||||
|
this.$root.socket.off('playlist_updated', this.playlistUpdated)
|
||||||
|
this.$root.socket.off('playlist_removed', this.playlistRemoved)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<h1 class="text-xl">{{ headerText }}</h1>
|
||||||
|
|
||||||
|
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
||||||
|
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" />
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
headerText: String,
|
||||||
|
description: String,
|
||||||
|
note: String,
|
||||||
|
showAddButton: Boolean
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clicked() {
|
||||||
|
this.$emit('clicked')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#settings-description a {
|
||||||
|
color: rgb(96 165 250);
|
||||||
|
}
|
||||||
|
#settings-description a:hover {
|
||||||
|
color: rgb(147 197 253);
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
|
#settings-description code {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgb(82, 82, 82);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- <div class="w-20 bg-bg h-full relative box-shadow-side z-40" style="min-width: 80px"> -->
|
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
||||||
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-40" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
|
|
||||||
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
|
||||||
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons">format_list_bulleted</span>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined">collections_bookmark</span>
|
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
|
|
||||||
@@ -71,6 +70,14 @@
|
|||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
|
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-icons text-2xl">warning</span>
|
||||||
|
|
||||||
@@ -143,6 +150,9 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.$route.name === 'library-library-authors'
|
||||||
},
|
},
|
||||||
|
isPlaylistsPage() {
|
||||||
|
return this.paramId === 'playlists'
|
||||||
|
},
|
||||||
libraryBookshelfPage() {
|
libraryBookshelfPage() {
|
||||||
return this.$route.name === 'library-library-bookshelf-id'
|
return this.$route.name === 'library-library-bookshelf-id'
|
||||||
},
|
},
|
||||||
@@ -173,6 +183,9 @@ export default {
|
|||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
showPlaylists() {
|
||||||
|
return this.$store.state.libraries.numUserPlaylists > 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||||
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<player-ui
|
<player-ui
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
@@ -297,6 +299,16 @@ export default {
|
|||||||
this.playerHandler.seek(e.seekTime)
|
this.playerHandler.seek(e.seekTime)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mediaSessionPreviousTrack() {
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.prevChapter()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mediaSessionNextTrack() {
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.nextChapter()
|
||||||
|
}
|
||||||
|
},
|
||||||
updateMediaSessionPlaybackState() {
|
updateMediaSessionPlaybackState() {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
|
||||||
@@ -330,8 +342,9 @@ export default {
|
|||||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||||
// navigator.mediaSession.setActionHandler('previoustrack')
|
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionPreviousTrack)
|
||||||
// navigator.mediaSession.setActionHandler('nexttrack')
|
const hasNextChapter = this.$refs.audioPlayer && this.$refs.audioPlayer.hasNextChapter
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', hasNextChapter ? this.mediaSessionNextTrack : null)
|
||||||
} else {
|
} else {
|
||||||
console.warn('Media session not available')
|
console.warn('Media session not available')
|
||||||
}
|
}
|
||||||
@@ -365,7 +378,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamReady() {
|
streamReady() {
|
||||||
console.log(`[STREAM-CONTAINER] Stream Ready`)
|
console.log(`[StreamContainer] Stream Ready`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setStreamReady()
|
this.$refs.audioPlayer.setStreamReady()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -13,10 +13,14 @@
|
|||||||
|
|
||||||
<!-- Search icon btn -->
|
<!-- 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">
|
<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">
|
||||||
|
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<span class="material-icons text-lg">search</span>
|
<span class="material-icons text-lg">search</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</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)">
|
<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)">
|
||||||
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<span class="material-icons text-lg">edit</span>
|
<span class="material-icons text-lg">edit</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
|
|||||||
@@ -410,6 +410,10 @@ export default {
|
|||||||
{
|
{
|
||||||
func: 'toggleFinished',
|
func: 'toggleFinished',
|
||||||
text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
|
text: this.itemIsFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func: 'openPlaylists',
|
||||||
|
text: this.$strings.LabelAddToPlaylist
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
@@ -448,6 +452,12 @@ export default {
|
|||||||
text: this.$strings.LabelAddToCollection
|
text: this.$strings.LabelAddToCollection
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.numTracks) {
|
||||||
|
items.push({
|
||||||
|
func: 'openPlaylists',
|
||||||
|
text: this.$strings.LabelAddToPlaylist
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -739,6 +749,10 @@ export default {
|
|||||||
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.store.commit('globals/setShowCollectionsModal', true)
|
this.store.commit('globals/setShowCollectionsModal', true)
|
||||||
},
|
},
|
||||||
|
openPlaylists() {
|
||||||
|
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
|
||||||
|
this.store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
createMoreMenu() {
|
createMoreMenu() {
|
||||||
if (!this.$refs.moreIcon) return
|
if (!this.$refs.moreIcon) return
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 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(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<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>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
|
</div>
|
||||||
|
<div v-show="isHovering && userCanUpdate" 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', 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="!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(200, 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>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
playlistMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playlist: null,
|
||||||
|
isSelectionMode: false,
|
||||||
|
selected: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
|
return this.width / 240
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist ? this.playlist.items || [] : []
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
isAlternativeBookshelfView() {
|
||||||
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(playlist) {
|
||||||
|
this.playlist = playlist
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.playlist) return
|
||||||
|
var router = this.$router || this.$nuxt.$router
|
||||||
|
router.push(`/playlist/${this.playlist.id}`)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.playlist)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.playlistMount) {
|
||||||
|
this.setEntity(this.playlistMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<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' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons">arrow_right</span>
|
<span class="material-icons text-2xl">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-black-400" role="option" @click="sublist = null">
|
||||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons">arrow_left</span>
|
<span class="material-icons text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate">Back</span>
|
<span class="font-normal ml-3 block truncate">Back</span>
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('No Series'))">
|
<li v-else-if="sublist === 'series'" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" role="option" @click="clickedSublistOption($encode('no-series'))">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">No Series</span>
|
<span class="font-normal block truncate py-2 text-xs text-white text-opacity-80">{{ $strings.MessageNoSeries }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template v-for="item in sublistItems">
|
<template v-for="item in sublistItems">
|
||||||
@@ -67,120 +67,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
sublist: null,
|
sublist: null
|
||||||
seriesItems: [
|
|
||||||
{
|
|
||||||
text: 'All',
|
|
||||||
value: 'all'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Genre',
|
|
||||||
value: 'genres',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Tag',
|
|
||||||
value: 'tags',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Authors',
|
|
||||||
value: 'authors',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Narrator',
|
|
||||||
value: 'narrators',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Language',
|
|
||||||
value: 'languages',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Series Progress',
|
|
||||||
value: 'progress',
|
|
||||||
sublist: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
bookItems: [
|
|
||||||
{
|
|
||||||
text: 'All',
|
|
||||||
value: 'all'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Genre',
|
|
||||||
value: 'genres',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Tag',
|
|
||||||
value: 'tags',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Series',
|
|
||||||
value: 'series',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Authors',
|
|
||||||
value: 'authors',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Narrator',
|
|
||||||
value: 'narrators',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Language',
|
|
||||||
value: 'languages',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Progress',
|
|
||||||
value: 'progress',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Missing',
|
|
||||||
value: 'missing',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Issues',
|
|
||||||
value: 'issues',
|
|
||||||
sublist: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'RSS Feed Open',
|
|
||||||
value: 'feed-open',
|
|
||||||
sublist: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
podcastItems: [
|
|
||||||
{
|
|
||||||
text: 'All',
|
|
||||||
value: 'all'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Genre',
|
|
||||||
value: 'genres',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Tag',
|
|
||||||
value: 'tags',
|
|
||||||
sublist: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Issues',
|
|
||||||
value: 'issues',
|
|
||||||
sublist: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -203,6 +90,130 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
||||||
},
|
},
|
||||||
|
seriesItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAll,
|
||||||
|
value: 'all'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelGenre,
|
||||||
|
value: 'genres',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTag,
|
||||||
|
value: 'tags',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAuthor,
|
||||||
|
value: 'authors',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelNarrator,
|
||||||
|
value: 'narrators',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLanguage,
|
||||||
|
value: 'languages',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSeriesProgress,
|
||||||
|
value: 'progress',
|
||||||
|
sublist: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bookItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAll,
|
||||||
|
value: 'all'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelGenre,
|
||||||
|
value: 'genres',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTag,
|
||||||
|
value: 'tags',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSeries,
|
||||||
|
value: 'series',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAuthor,
|
||||||
|
value: 'authors',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelNarrator,
|
||||||
|
value: 'narrators',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelLanguage,
|
||||||
|
value: 'languages',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelProgress,
|
||||||
|
value: 'progress',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelMissing,
|
||||||
|
value: 'missing',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTracks,
|
||||||
|
value: 'tracks',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonIssues,
|
||||||
|
value: 'issues',
|
||||||
|
sublist: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelRSSFeedOpen,
|
||||||
|
value: 'feed-open',
|
||||||
|
sublist: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
podcastItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAll,
|
||||||
|
value: 'all'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelGenre,
|
||||||
|
value: 'genres',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTag,
|
||||||
|
value: 'tags',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonIssues,
|
||||||
|
value: 'issues',
|
||||||
|
sublist: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
if (this.isPodcast) return this.podcastItems
|
if (this.isPodcast) return this.podcastItems
|
||||||
@@ -257,10 +268,92 @@ export default {
|
|||||||
return this.filterData.languages || []
|
return this.filterData.languages || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
return [
|
||||||
|
{
|
||||||
|
id: 'finished',
|
||||||
|
name: this.$strings.LabelFinished
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'in-progress',
|
||||||
|
name: this.$strings.LabelInProgress
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-started',
|
||||||
|
name: this.$strings.LabelNotStarted
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'not-finished',
|
||||||
|
name: this.$strings.LabelNotFinished
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'single',
|
||||||
|
name: this.$strings.LabelTracksSingleTrack
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'multi',
|
||||||
|
name: this.$strings.LabelTracksMultiTrack
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
missing() {
|
missing() {
|
||||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
return [
|
||||||
|
{
|
||||||
|
id: 'asin',
|
||||||
|
name: 'ASIN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'isbn',
|
||||||
|
name: 'ISBN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'subtitle',
|
||||||
|
name: this.$strings.LabelSubtitle
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'authors',
|
||||||
|
name: this.$strings.LabelAuthor
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'publishedYear',
|
||||||
|
name: this.$strings.LabelPublishYear
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'series',
|
||||||
|
name: this.$strings.LabelSeries
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'description',
|
||||||
|
name: this.$strings.LabelDescription
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'genres',
|
||||||
|
name: this.$strings.LabelGenres
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tags',
|
||||||
|
name: this.$strings.LabelTags
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'narrators',
|
||||||
|
name: this.$strings.LabelNarrator
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'publisher',
|
||||||
|
name: this.$strings.LabelPublisher
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'language',
|
||||||
|
name: this.$strings.LabelLanguage
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cover',
|
||||||
|
name: this.$strings.LabelCover
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
return (this[this.sublist] || []).map((item) => {
|
||||||
|
|||||||
@@ -56,31 +56,31 @@ export default {
|
|||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: 'Title',
|
text: this.$strings.LabelTitle,
|
||||||
value: 'media.metadata.title'
|
value: 'media.metadata.title'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author',
|
text: this.$strings.LabelAuthor,
|
||||||
value: 'media.metadata.author'
|
value: 'media.metadata.author'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: this.$strings.LabelAddedAt,
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Size',
|
text: this.$strings.LabelSize,
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '# of Episodes',
|
text: this.$strings.LabelNumberOfEpisodes,
|
||||||
value: 'media.numTracks'
|
value: 'media.numTracks'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: this.$strings.LabelFileBirthtime,
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'File Modified',
|
text: this.$strings.LabelFileModified,
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -92,35 +92,35 @@ export default {
|
|||||||
value: 'media.metadata.title'
|
value: 'media.metadata.title'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (First Last)',
|
text: this.$strings.LabelAuthorFirstLast,
|
||||||
value: 'media.metadata.authorName'
|
value: 'media.metadata.authorName'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Author (Last, First)',
|
text: this.$strings.LabelAuthorLastFirst,
|
||||||
value: 'media.metadata.authorNameLF'
|
value: 'media.metadata.authorNameLF'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Published Year',
|
text: this.$strings.LabelPublishYear,
|
||||||
value: 'media.metadata.publishedYear'
|
value: 'media.metadata.publishedYear'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Added At',
|
text: this.$strings.LabelAddedAt,
|
||||||
value: 'addedAt'
|
value: 'addedAt'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Size',
|
text: this.$strings.LabelSize,
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Duration',
|
text: this.$strings.LabelDuration,
|
||||||
value: 'media.duration'
|
value: 'media.duration'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: this.$strings.LabelFileBirthtime,
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'File Modified',
|
text: this.$strings.LabelFileModified,
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -129,7 +129,7 @@ export default {
|
|||||||
return [
|
return [
|
||||||
...this.bookItems,
|
...this.bookItems,
|
||||||
{
|
{
|
||||||
text: 'Sequence',
|
text: this.$strings.LabelSequence,
|
||||||
value: 'sequence'
|
value: 'sequence'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / (120 * 1.6 * 2)
|
||||||
|
},
|
||||||
|
itemCoverWidth() {
|
||||||
|
if (this.libraryItemCovers.length === 1) return this.width
|
||||||
|
return this.width / 2
|
||||||
|
},
|
||||||
|
libraryItemCovers() {
|
||||||
|
if (!this.items.length) return []
|
||||||
|
if (this.items.length === 1) return [this.items[0].libraryItem]
|
||||||
|
|
||||||
|
const covers = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let index = i % this.items.length
|
||||||
|
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
|
||||||
|
|
||||||
|
covers.push(this.items[index].libraryItem)
|
||||||
|
}
|
||||||
|
return covers
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -201,8 +201,8 @@ export default {
|
|||||||
this.loadingTags = true
|
this.loadingTags = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/tags`)
|
.$get(`/api/tags`)
|
||||||
.then((tags) => {
|
.then((res) => {
|
||||||
this.tags = tags
|
this.tags = res.tags
|
||||||
this.loadingTags = false
|
this.loadingTags = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateCover }}
|
{{ $strings.LabelUpdateCover }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateDetails }}
|
{{ $strings.LabelUpdateDetails }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ export default {
|
|||||||
return this.$store.state.globals.showBatchQuickMatchModal
|
return this.$store.state.globals.showBatchQuickMatchModal
|
||||||
},
|
},
|
||||||
selectedBookIds() {
|
selectedBookIds() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">add</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">add</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default {
|
|||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 50
|
default: 60
|
||||||
},
|
},
|
||||||
bgOpacity: {
|
bgOpacity: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<div class="flex pt-2 px-2">
|
<div class="flex pt-2 px-2">
|
||||||
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +136,7 @@ export default {
|
|||||||
})
|
})
|
||||||
if (result && result.updated) {
|
if (result && result.updated) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', result.author)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
@@ -157,7 +158,10 @@ export default {
|
|||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error('Author not found')
|
this.$toast.error('Author not found')
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
if (response.author.imagePath) {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
this.$store.commit('globals/showEditAuthorModal', response.author)
|
||||||
|
}
|
||||||
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info('No updates were made for Author')
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="flex-grow pr-2">
|
<div class="flex-grow pr-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">forward</span></ui-btn>
|
||||||
<div class="pl-2 flex items-center">
|
<div class="pl-2 flex items-center">
|
||||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
<transition-group name="list-complete" tag="div">
|
<transition-group name="list-complete" tag="div">
|
||||||
<template v-for="collection in sortedCollections">
|
<template v-for="collection in sortedCollections">
|
||||||
<modals-collections-user-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
<modals-collections-collection-item :key="collection.id" :collection="collection" :book-cover-aspect-ratio="bookCoverAspectRatio" class="list-complete-item" @add="addToCollection" @remove="removeFromCollection" @close="show = false" />
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,7 @@ export default {
|
|||||||
return this.$store.state.globals.showBatchCollectionModal
|
return this.$store.state.globals.showBatchCollectionModal
|
||||||
},
|
},
|
||||||
selectedBookIds() {
|
selectedBookIds() {
|
||||||
return this.$store.state.selectedLibraryItems || []
|
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
@@ -112,7 +112,6 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadCollections() {
|
loadCollections() {
|
||||||
if (!this.collections.length) {
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
.$get(`/api/libraries/${this.currentLibraryId}/collections`)
|
||||||
@@ -128,7 +127,6 @@ export default {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
|
||||||
},
|
},
|
||||||
removeFromCollection(collection) {
|
removeFromCollection(collection) {
|
||||||
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
if (!this.selectedLibraryItemId && !this.selectedBookIds.length) return
|
||||||
@@ -231,19 +229,3 @@ export default {
|
|||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.list-complete-item {
|
|
||||||
transition: all 0.8s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-complete-enter-from,
|
|
||||||
.list-complete-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-complete-leave-active {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
|
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
|
<div class="w-20 max-w-20 text-center">
|
||||||
|
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
|
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
|
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||||
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
collection: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isBookIncluded() {
|
||||||
|
return !!this.collection.isBookIncluded
|
||||||
|
},
|
||||||
|
books() {
|
||||||
|
return this.collection.books || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickNuxtLink() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickAdd() {
|
||||||
|
this.$emit('add', this.collection)
|
||||||
|
},
|
||||||
|
clickRem() {
|
||||||
|
this.$emit('remove', this.collection)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" :class="wrapperClass" @mouseover="mouseover" @mouseleave="mouseleave">
|
|
||||||
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
|
||||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
|
||||||
<div class="w-20 max-w-20 text-center">
|
|
||||||
<!-- <img src="/Logo.png" /> -->
|
|
||||||
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow overflow-hidden px-2">
|
|
||||||
<!-- <template v-if="isEditing">
|
|
||||||
<form @submit.prevent="submitUpdate">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex-grow pr-2">
|
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
|
||||||
</div>
|
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons -mt-px">forward</span></ui-btn>
|
|
||||||
<div class="pl-2 flex items-center">
|
|
||||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template> -->
|
|
||||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
|
||||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons pt-px">add</span></ui-btn>
|
|
||||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons pt-px">remove</span></ui-btn>
|
|
||||||
<!-- <span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
|
||||||
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
collection: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
highlight: Boolean,
|
|
||||||
bookCoverAspectRatio: Number
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isHovering: false,
|
|
||||||
isEditing: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isBookIncluded() {
|
|
||||||
return !!this.collection.isBookIncluded
|
|
||||||
},
|
|
||||||
wrapperClass() {
|
|
||||||
var classes = []
|
|
||||||
if (this.highlight) classes.push('bg-bg bg-opacity-60')
|
|
||||||
if (!this.isEditing) classes.push('cursor-pointer')
|
|
||||||
return classes.join(' ')
|
|
||||||
},
|
|
||||||
books() {
|
|
||||||
return this.collection.books || []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickNuxtLink() {
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
mouseover() {
|
|
||||||
if (this.isEditing) return
|
|
||||||
this.isHovering = true
|
|
||||||
},
|
|
||||||
mouseleave() {
|
|
||||||
this.isHovering = false
|
|
||||||
},
|
|
||||||
clickAdd() {
|
|
||||||
this.$emit('add', this.collection)
|
|
||||||
},
|
|
||||||
clickRem() {
|
|
||||||
this.$emit('remove', this.collection)
|
|
||||||
},
|
|
||||||
deleteClick() {
|
|
||||||
if (this.isEditing) return
|
|
||||||
this.$emit('delete', this.collection)
|
|
||||||
},
|
|
||||||
editClick() {
|
|
||||||
this.isEditing = true
|
|
||||||
this.isHovering = false
|
|
||||||
},
|
|
||||||
cancelEditing() {
|
|
||||||
this.isEditing = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
<div class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||||
<span class="material-icons">delete</span>
|
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||||
|
<span class="material-icons text-2xl">delete</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
||||||
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
||||||
><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input
|
><span class="material-icons text-2xl inline-block md:!hidden">upload</span></ui-file-input
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||||
@@ -301,8 +303,11 @@ export default {
|
|||||||
this.persistProvider()
|
this.persistProvider()
|
||||||
|
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
var results = await this.$axios.$get(`/api/search/covers?${searchQuery}`).catch((error) => {
|
const results = await this.$axios
|
||||||
|
.$get(`/api/search/covers?${searchQuery}`)
|
||||||
|
.then((res) => res.results)
|
||||||
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -306,13 +306,13 @@ export default {
|
|||||||
this.runSearch()
|
this.runSearch()
|
||||||
},
|
},
|
||||||
async runSearch() {
|
async runSearch() {
|
||||||
var searchQuery = this.getSearchQuery()
|
const searchQuery = this.getSearchQuery()
|
||||||
if (this.lastSearch === searchQuery) return
|
if (this.lastSearch === searchQuery) return
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
const searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
let results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 20000 }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -335,8 +335,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
},
|
},
|
||||||
init() {
|
initSelectedMatchUsage() {
|
||||||
this.clearSelectedMatch()
|
|
||||||
this.selectedMatchUsage = {
|
this.selectedMatchUsage = {
|
||||||
title: true,
|
title: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
@@ -360,6 +359,27 @@ export default {
|
|||||||
releaseDate: true
|
releaseDate: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load saved selected match from local storage
|
||||||
|
try {
|
||||||
|
let savedSelectedMatchUsage = localStorage.getItem('selectedMatchUsage')
|
||||||
|
if (!savedSelectedMatchUsage) return
|
||||||
|
savedSelectedMatchUsage = JSON.parse(savedSelectedMatchUsage)
|
||||||
|
|
||||||
|
for (const key in savedSelectedMatchUsage) {
|
||||||
|
if (this.selectedMatchUsage[key] !== undefined) {
|
||||||
|
this.selectedMatchUsage[key] = !!savedSelectedMatchUsage[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load saved selectedMatchUsage', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkboxToggled()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.clearSelectedMatch()
|
||||||
|
this.initSelectedMatchUsage()
|
||||||
|
|
||||||
if (this.libraryItem.id !== this.libraryItemId) {
|
if (this.libraryItem.id !== this.libraryItemId) {
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.hasSearched = false
|
this.hasSearched = false
|
||||||
@@ -465,11 +485,14 @@ export default {
|
|||||||
console.log('Match payload', updatePayload)
|
console.log('Match payload', updatePayload)
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
|
// Persist in local storage
|
||||||
|
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
|
||||||
|
|
||||||
if (updatePayload.metadata.cover) {
|
if (updatePayload.metadata.cover) {
|
||||||
var coverPayload = {
|
const coverPayload = {
|
||||||
url: updatePayload.metadata.cover
|
url: updatePayload.metadata.cover
|
||||||
}
|
}
|
||||||
var success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -483,8 +506,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updatePayload).length) {
|
if (Object.keys(updatePayload).length) {
|
||||||
var mediaUpdatePayload = updatePayload
|
const mediaUpdatePayload = updatePayload
|
||||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
|
||||||
console.error('Failed to update', error)
|
console.error('Failed to update', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -502,6 +525,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
clearSelectedMatch() {
|
clearSelectedMatch() {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max episodes to keep
|
Max episodes to keep
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
Max new episodes to download per check
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Make M4B Audiobook File</p>
|
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
|
||||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.</p>
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsMakeM4bDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
<div v-if="showMp3Split && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="showMp3Split && showExperimentalFeatures" class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Split M4B to MP3's</p>
|
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
|
||||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate multiple MP3's split by chapters with embedded metadata, cover image, and chapters.</p>
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsSplitM4bDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
<ui-btn :disabled="true">Not yet implemented</ui-btn>
|
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Embed Metadata</p>
|
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
|
||||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Embed metadata into audio files including cover image and chapters.</p>
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsEmbedMetadataDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-1 md:px-4 py-2 mb-4">
|
<div class="w-full h-full md:px-4 py-2 mb-4">
|
||||||
<div v-if="!showDirectoryPicker" class="w-full h-full py-4">
|
<div v-if="!showDirectoryPicker" class="w-full h-full md:py-4">
|
||||||
<div class="flex flex-wrap md:flex-nowrap -mx-1">
|
<div class="flex flex-wrap md:flex-nowrap -mx-1 mb-2">
|
||||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full py-4">
|
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||||
<span v-show="folders.length > 1" class="material-icons ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
@@ -140,3 +140,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 192px);
|
||||||
|
}
|
||||||
|
@media (max-device-width: 768px) {
|
||||||
|
.folders-container {
|
||||||
|
max-height: calc(80vh - 292px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-2 md:px-4 w-full text-sm pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||||
<span class="material-icons text-success">play_arrow</span>
|
<span class="material-icons text-2xl text-success">play_arrow</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||||
<span class="material-icons text-error">close</span>
|
<span class="material-icons text-2xl text-error">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
|
<div v-if="show" class="w-full h-full">
|
||||||
|
<div class="py-4 px-4">
|
||||||
|
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
|
||||||
|
<h1 v-else class="text-2xl">{{ $getString('LabelAddToPlaylistBatch', [selectedPlaylistItems.length]) }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
|
<transition-group name="list-complete" tag="div">
|
||||||
|
<template v-for="playlist in sortedPlaylists">
|
||||||
|
<modals-playlists-user-playlist-item :key="playlist.id" :playlist="playlist" class="list-complete-item" @add="addToPlaylist" @remove="removeFromPlaylist" @close="show = false" />
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
|
||||||
|
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-px bg-white bg-opacity-10" />
|
||||||
|
<form @submit.prevent="submitCreatePlaylist">
|
||||||
|
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||||
|
<div class="flex-grow px-2">
|
||||||
|
<ui-text-input v-model="newPlaylistName" :placeholder="$strings.PlaceholderNewPlaylist" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10">{{ $strings.ButtonCreate }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newPlaylistName: '',
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.loadPlaylists()
|
||||||
|
this.newPlaylistName = ''
|
||||||
|
} else {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showPlaylistsModal
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
if (!this.selectedPlaylistItems.length) return ''
|
||||||
|
if (this.isBatch) {
|
||||||
|
return this.$getString('MessageItemsSelected', [this.selectedPlaylistItems.length])
|
||||||
|
}
|
||||||
|
const selectedPlaylistItem = this.selectedPlaylistItems[0]
|
||||||
|
if (selectedPlaylistItem.episode) {
|
||||||
|
return selectedPlaylistItem.episode.title
|
||||||
|
}
|
||||||
|
return selectedPlaylistItem.libraryItem.media.metadata.title || ''
|
||||||
|
},
|
||||||
|
playlists() {
|
||||||
|
return this.$store.state.libraries.userPlaylists || []
|
||||||
|
},
|
||||||
|
sortedPlaylists() {
|
||||||
|
return this.playlists
|
||||||
|
.map((playlist) => {
|
||||||
|
const includesItem = !this.selectedPlaylistItems.some((item) => !this.checkIsItemInPlaylist(playlist, item))
|
||||||
|
|
||||||
|
return {
|
||||||
|
isItemIncluded: includesItem,
|
||||||
|
...playlist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.isItemIncluded ? -1 : 1))
|
||||||
|
},
|
||||||
|
isBatch() {
|
||||||
|
return this.selectedPlaylistItems.length > 1
|
||||||
|
},
|
||||||
|
selectedPlaylistItems() {
|
||||||
|
return this.$store.state.globals.selectedPlaylistItems || []
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkIsItemInPlaylist(playlist, item) {
|
||||||
|
if (item.episode) {
|
||||||
|
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id && i.episodeId === item.episode.id)
|
||||||
|
}
|
||||||
|
return playlist.items.some((i) => i.libraryItemId === item.libraryItem.id)
|
||||||
|
},
|
||||||
|
loadPlaylists() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/libraries/${this.currentLibraryId}/playlists`)
|
||||||
|
.then((data) => {
|
||||||
|
this.$store.commit('libraries/setUserPlaylists', data.results || [])
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to get playlists', error)
|
||||||
|
this.$toast.error('Failed to load user playlists')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeFromPlaylist(playlist) {
|
||||||
|
if (!this.selectedPlaylistItems.length) return
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Playlist item(s) removed')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove items from playlist', error)
|
||||||
|
this.$toast.error('Failed to remove playlist item(s)')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addToPlaylist(playlist) {
|
||||||
|
if (!this.selectedPlaylistItems.length) return
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
console.log(`Items added to playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Items added to playlist')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to add items to playlist', error)
|
||||||
|
this.$toast.error('Failed to add items to playlist')
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitCreatePlaylist() {
|
||||||
|
if (!this.newPlaylistName || !this.selectedPlaylistItems.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const itemObjects = this.selectedPlaylistItems.map((pi) => ({ libraryItemId: pi.libraryItem.id, episodeId: pi.episode ? pi.episode.id : null }))
|
||||||
|
const newPlaylist = {
|
||||||
|
items: itemObjects,
|
||||||
|
libraryId: this.currentLibraryId,
|
||||||
|
name: this.newPlaylistName
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/playlists', newPlaylist)
|
||||||
|
.then((data) => {
|
||||||
|
console.log('New playlist created', data)
|
||||||
|
this.$toast.success(`Playlist "${data.name}" created`)
|
||||||
|
this.processing = false
|
||||||
|
this.newPlaylistName = ''
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to create playlist', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex">
|
||||||
|
<div>
|
||||||
|
<covers-playlist-cover :items="items" :width="200" :height="200" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-4">
|
||||||
|
<ui-text-input-with-label v-model="newPlaylistName" :label="$strings.LabelName" class="mb-2" />
|
||||||
|
|
||||||
|
<ui-textarea-with-label v-model="newPlaylistDescription" :label="$strings.LabelDescription" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex">
|
||||||
|
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newPlaylistName: null,
|
||||||
|
newPlaylistDescription: null,
|
||||||
|
showImageUploader: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showEditPlaylistModal
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowEditPlaylistModal', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
playlist() {
|
||||||
|
return this.$store.state.globals.selectedPlaylist || {}
|
||||||
|
},
|
||||||
|
playlistName() {
|
||||||
|
return this.playlist.name
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.newPlaylistName = this.playlistName
|
||||||
|
this.newPlaylistDescription = this.playlist.description || ''
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
if (confirm(this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]))) {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove playlist', error)
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.newPlaylistName) {
|
||||||
|
return this.$toast.error('Playlist must have a name')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
var playlistUpdate = {
|
||||||
|
name: this.newPlaylistName,
|
||||||
|
description: this.newPlaylistDescription || null
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/playlists/${this.playlist.id}`, playlistUpdate)
|
||||||
|
.then((playlist) => {
|
||||||
|
console.log('Playlist Updated', playlist)
|
||||||
|
this.processing = false
|
||||||
|
this.show = false
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update playlist', error)
|
||||||
|
this.processing = false
|
||||||
|
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
|
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
|
||||||
|
<div class="w-16 max-w-16 text-center">
|
||||||
|
<covers-playlist-cover :items="items" :width="64" :height="64" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden px-2">
|
||||||
|
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
|
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
||||||
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
playlist: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isItemIncluded() {
|
||||||
|
return !!this.playlist.isItemIncluded
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickNuxtLink() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickAdd() {
|
||||||
|
this.$emit('add', this.playlist)
|
||||||
|
},
|
||||||
|
clickRem() {
|
||||||
|
this.$emit('remove', this.playlist)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||||
|
|||||||
@@ -4,27 +4,37 @@
|
|||||||
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||||
|
|
||||||
|
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl sm:text-2.5xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-icons text-lg text-warning">snooze</span>
|
<span class="material-icons text-lg text-warning">snooze</span>
|
||||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
|
||||||
<span class="material-icons text-2xl sm:text-2.5xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||||
|
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<div v-if="chapters.length" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span>
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<button v-if="playerQueueItems.length" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">queue_music</span>
|
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||||
|
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
||||||
</button>
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
||||||
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||||
@@ -224,13 +234,10 @@ export default {
|
|||||||
this.showChaptersModal = false
|
this.showChaptersModal = false
|
||||||
},
|
},
|
||||||
setUseChapterTrack() {
|
setUseChapterTrack() {
|
||||||
var useChapterTrack = !this.useChapterTrack
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
this.useChapterTrack = useChapterTrack
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(useChapterTrack)
|
|
||||||
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { useChapterTrack }).catch((err) => {
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||||
console.error('Failed to update settings', err)
|
|
||||||
})
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
},
|
},
|
||||||
checkUpdateChapterTrack() {
|
checkUpdateChapterTrack() {
|
||||||
@@ -301,7 +308,7 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
var _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
||||||
|
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
@@ -335,13 +342,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'audioplayer', meth: this.settingsUpdated })
|
|
||||||
this.init()
|
|
||||||
this.$eventBus.$on('player-hotkey', this.hotkey)
|
this.$eventBus.$on('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'audioplayer')
|
|
||||||
this.$eventBus.$off('player-hotkey', this.hotkey)
|
this.$eventBus.$off('player-hotkey', this.hotkey)
|
||||||
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-primary text-white">
|
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 right-4 z-20">
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,13 +92,18 @@ export default {
|
|||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
if (!this.ebookFile) return null
|
if (!this.ebookFile) return null
|
||||||
var itemRelPath = this.selectedLibraryItem.relPath
|
let filepath = ''
|
||||||
|
if (this.selectedLibraryItem.isFile) {
|
||||||
|
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
|
||||||
|
} else {
|
||||||
|
const itemRelPath = this.selectedLibraryItem.relPath
|
||||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
||||||
var relPath = this.ebookFile.metadata.relPath
|
const relPath = this.ebookFile.metadata.relPath
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
const relRelPath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
filepath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${relRelPath}`
|
}
|
||||||
|
return `/ebook/${this.libraryId}/${this.folderId}/${filepath}`
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="material-icons text-7xl">show_chart</span>
|
<span class="material-icons text-7xl">show_chart</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsOverall }} {{ useOverallHours ? $strings.LabelStatsHours : $strings.LabelStatsDays }}</p>
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
<a v-if="backup.serverVersion" :href="`/metadata/${$encodeUriPath(backup.path)}?token=${userToken}`" class="mx-1 pt-1 hover:text-opacity-100 text-opacity-70 text-white" download><span class="material-icons text-xl">download</span></a>
|
||||||
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||||
<span class="material-icons-outlined text-error">error_outline</span>
|
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
<span class="material-icons text-xl hover:text-error hover:text-opacity-100 text-opacity-70 text-white cursor-pointer mx-1" @click="deleteBackupClick(backup)">delete</span>
|
||||||
@@ -64,13 +64,11 @@ export default {
|
|||||||
showConfirmApply: false,
|
showConfirmApply: false,
|
||||||
selectedBackup: null,
|
selectedBackup: null,
|
||||||
isBackingUp: false,
|
isBackingUp: false,
|
||||||
processing: false
|
processing: false,
|
||||||
|
backups: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
backups() {
|
|
||||||
return this.$store.state.backups || []
|
|
||||||
},
|
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
@@ -96,9 +94,8 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/backups/${backup.id}`)
|
.$delete(`/api/backups/${backup.id}`)
|
||||||
.then((backups) => {
|
.then((data) => {
|
||||||
console.log('Backup deleted', backups)
|
this.setBackups(data.backups || [])
|
||||||
this.$store.commit('setBackups', backups)
|
|
||||||
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
|
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
@@ -117,10 +114,10 @@ export default {
|
|||||||
this.isBackingUp = true
|
this.isBackingUp = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post('/api/backups')
|
.$post('/api/backups')
|
||||||
.then((backups) => {
|
.then((data) => {
|
||||||
this.isBackingUp = false
|
this.isBackingUp = false
|
||||||
this.$toast.success(this.$strings.ToastBackupCreateSuccess)
|
this.$toast.success(this.$strings.ToastBackupCreateSuccess)
|
||||||
this.$store.commit('setBackups', backups)
|
this.setBackups(data.backups || [])
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.isBackingUp = false
|
this.isBackingUp = false
|
||||||
@@ -136,9 +133,8 @@ export default {
|
|||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post('/api/backups/upload', form)
|
.$post('/api/backups/upload', form)
|
||||||
.then((result) => {
|
.then((data) => {
|
||||||
console.log('Upload backup result', result)
|
this.setBackups(data.backups || [])
|
||||||
this.$store.commit('setBackups', result)
|
|
||||||
this.$toast.success(this.$strings.ToastBackupUploadSuccess)
|
this.$toast.success(this.$strings.ToastBackupUploadSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
@@ -148,9 +144,29 @@ export default {
|
|||||||
this.$toast.error(errorMessage)
|
this.$toast.error(errorMessage)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
setBackups(backups) {
|
||||||
|
backups.sort((a, b) => b.createdAt - a.createdAt)
|
||||||
|
this.backups = backups
|
||||||
|
},
|
||||||
|
loadBackups() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/backups')
|
||||||
|
.then((data) => {
|
||||||
|
this.setBackups(data.backups || [])
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load backups', error)
|
||||||
|
this.$toast.error('Failed to load backups')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.loadBackups()
|
||||||
if (this.$route.query.backup) {
|
if (this.$route.query.backup) {
|
||||||
this.$toast.success('Backup applied successfully')
|
this.$toast.success('Backup applied successfully')
|
||||||
this.$router.replace('/config')
|
this.$router.replace('/config')
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full bg-primary bg-opacity-40">
|
||||||
|
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
|
||||||
|
<p class="pr-4">{{ $strings.HeaderPlaylistItems }}</p>
|
||||||
|
|
||||||
|
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||||
|
</div>
|
||||||
|
<draggable v-model="itemsCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
|
<transition-group type="transition" :name="!drag ? 'playlist-item' : null">
|
||||||
|
<template v-for="(item, index) in itemsCopy">
|
||||||
|
<tables-playlist-item-table-row :key="index" :is-dragging="drag" :item="item" :playlist-id="playlistId" :book-cover-aspect-ratio="bookCoverAspectRatio" class="item" :class="drag ? '' : 'playlist-item-item'" @edit="editItem" />
|
||||||
|
</template>
|
||||||
|
</transition-group>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
draggable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
playlistId: String,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drag: false,
|
||||||
|
dragOptions: {
|
||||||
|
animation: 200,
|
||||||
|
group: 'description',
|
||||||
|
ghostClass: 'ghost'
|
||||||
|
},
|
||||||
|
itemsCopy: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
items: {
|
||||||
|
handler(newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
|
},
|
||||||
|
totalDuration() {
|
||||||
|
var _total = 0
|
||||||
|
this.items.forEach((item) => {
|
||||||
|
if (item.episode) _total += item.episode.duration
|
||||||
|
else _total += item.libraryItem.media.duration
|
||||||
|
})
|
||||||
|
return _total
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editItem(playlistItem) {
|
||||||
|
if (playlistItem.episode) {
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', playlist.episode)
|
||||||
|
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
|
||||||
|
} else {
|
||||||
|
const itemIds = this.items.map((i) => i.libraryItemId)
|
||||||
|
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||||
|
this.$store.commit('showEditModal', playlistItem.libraryItem)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
draggableUpdate() {
|
||||||
|
var playlistUpdate = {
|
||||||
|
items: this.itemsCopy.map((i) => ({ libraryItemId: i.libraryItemId, episodeId: i.episodeId }))
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/playlists/${this.playlistId}`, playlistUpdate)
|
||||||
|
.then((playlist) => {
|
||||||
|
console.log('Playlist updated', playlist)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update playlist', error)
|
||||||
|
this.$toast.error('Failed to save playlist items order')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.itemsCopy = this.items.map((i) => ({ ...i }))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.playlist-item-item {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item-enter-from,
|
||||||
|
.playlist-item-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-item-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderUsers }}</h1>
|
|
||||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddUser">
|
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<table id="accounts">
|
<table id="accounts">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -26,11 +19,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-sm">{{ user.type }}</td>
|
<td class="text-sm">{{ user.type }}</td>
|
||||||
<td class="hidden lg:table-cell">
|
<td class="hidden lg:table-cell">
|
||||||
<div v-if="usersOnline[user.id] && usersOnline[user.id].session && usersOnline[user.id].session.libraryItem && usersOnline[user.id].session.libraryItem.media">
|
<div v-if="usersOnline[user.id]">
|
||||||
<p class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
<p v-if="usersOnline[user.id].session && usersOnline[user.id].session.libraryItem" class="truncate text-xs">Listening: {{ usersOnline[user.id].session.libraryItem.media.metadata.title || '' }}</p>
|
||||||
</div>
|
<p v-else-if="usersOnline[user.id].mostRecent && usersOnline[user.id].mostRecent.media" class="truncate text-xs">Last: {{ usersOnline[user.id].mostRecent.media.metadata.title }}</p>
|
||||||
<div v-else-if="user.mostRecent">
|
|
||||||
<p class="truncate text-xs">Last: {{ user.mostRecent.metadata.title }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs font-mono hidden sm:table-cell">
|
<td class="text-xs font-mono hidden sm:table-cell">
|
||||||
@@ -81,7 +72,7 @@ export default {
|
|||||||
},
|
},
|
||||||
usersOnline() {
|
usersOnline() {
|
||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
this.$store.state.users.usersOnline.forEach((u) => (usermap[u.id] = u))
|
||||||
return usermap
|
return usermap
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -118,8 +109,8 @@ export default {
|
|||||||
loadUsers() {
|
loadUsers() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
this.users = users.sort((a, b) => {
|
this.users = res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
<span class="material-icons">play_arrow</span>
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="librariesTable" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLibraries }}</h1>
|
|
||||||
<div class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clickAddLibrary">
|
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
@@ -88,10 +82,10 @@ export default {
|
|||||||
})
|
})
|
||||||
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
|
||||||
if (currOrder !== newOrder) {
|
if (currOrder !== newOrder) {
|
||||||
this.$axios.$post('/api/libraries/order', libraryOrderData).then((libraries) => {
|
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
|
||||||
if (libraries && libraries.length) {
|
if (response.libraries && response.libraries.length) {
|
||||||
this.$toast.success('Library order saved', { timeout: 1500 })
|
this.$toast.success('Library order saved', { timeout: 1500 })
|
||||||
this.$store.commit('libraries/set', libraries)
|
this.$store.commit('libraries/set', response.libraries)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
|
<div v-if="item" class="flex h-16 md:h-20">
|
||||||
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
|
<div class="flex h-full items-center justify-center">
|
||||||
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow overflow-hidden max-w-48 md:max-w-md h-full flex items-center px-2 md:px-3">
|
||||||
|
<div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md">
|
||||||
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="truncate hover:underline text-sm md:text-base">{{ itemTitle }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
|
<template v-for="(author, index) in bookAuthors">
|
||||||
|
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs md:text-sm text-gray-400">{{ itemDuration }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-40 absolute top-0 -right-24 h-full transform transition-transform" :class="!isHovering ? 'translate-x-0' : translateDistance">
|
||||||
|
<div class="flex h-full items-center">
|
||||||
|
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
|
</ui-tooltip>
|
||||||
|
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||||
|
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||||
|
</div>
|
||||||
|
<div v-if="userCanDelete" class="mx-1">
|
||||||
|
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
playlistId: String,
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
isDragging: Boolean,
|
||||||
|
bookCoverAspectRatio: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isProcessingReadUpdate: false,
|
||||||
|
processingRemove: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isDragging: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.isHovering = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
translateDistance() {
|
||||||
|
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
||||||
|
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
||||||
|
return '-translate-x-24'
|
||||||
|
},
|
||||||
|
libraryItem() {
|
||||||
|
return this.item.libraryItem || {}
|
||||||
|
},
|
||||||
|
episode() {
|
||||||
|
return this.item.episode
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
if (this.episode) return []
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
|
itemTitle() {
|
||||||
|
if (this.episode) return this.episode.title
|
||||||
|
return this.mediaMetadata.title || ''
|
||||||
|
},
|
||||||
|
bookAuthors() {
|
||||||
|
if (this.episode) return []
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
|
itemDuration() {
|
||||||
|
if (this.episode) return this.$elapsedPretty(this.episode.duration)
|
||||||
|
return this.$elapsedPretty(this.media.duration)
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.libraryItem.isMissing
|
||||||
|
},
|
||||||
|
isInvalid() {
|
||||||
|
return this.libraryItem.isInvalid
|
||||||
|
},
|
||||||
|
isStreaming() {
|
||||||
|
return this.$store.getters['getIsMediaStreaming'](this.libraryItem.id, this.episodeId)
|
||||||
|
},
|
||||||
|
showPlayBtn() {
|
||||||
|
return !this.isMissing && !this.isInvalid && !this.isStreaming && (this.tracks.length || this.episode)
|
||||||
|
},
|
||||||
|
itemProgress() {
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, this.episodeId)
|
||||||
|
},
|
||||||
|
userIsFinished() {
|
||||||
|
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||||
|
},
|
||||||
|
coverSize() {
|
||||||
|
return this.$store.state.globals.isMobile ? 30 : 50
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
|
return this.coverSize
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
mouseover() {
|
||||||
|
if (this.isDragging) return
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
playClick() {
|
||||||
|
let queueItem = null
|
||||||
|
if (this.episode) {
|
||||||
|
queueItem = {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
libraryId: this.libraryItem.libraryId,
|
||||||
|
episodeId: this.episodeId,
|
||||||
|
title: this.itemTitle,
|
||||||
|
subtitle: this.mediaMetadata.title,
|
||||||
|
caption: '',
|
||||||
|
duration: this.media.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
queueItem = {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
libraryId: this.libraryItem.libraryId,
|
||||||
|
episodeId: null,
|
||||||
|
title: this.itemTitle,
|
||||||
|
subtitle: this.bookAuthors.map((au) => au.name).join(', '),
|
||||||
|
caption: '',
|
||||||
|
duration: this.media.duration || null,
|
||||||
|
coverPath: this.media.coverPath || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.libraryItem.id,
|
||||||
|
episodeId: this.episodeId,
|
||||||
|
queueItems: [queueItem]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.item)
|
||||||
|
},
|
||||||
|
toggleFinished() {
|
||||||
|
var updatePayload = {
|
||||||
|
isFinished: !this.userIsFinished
|
||||||
|
}
|
||||||
|
this.isProcessingReadUpdate = true
|
||||||
|
|
||||||
|
let routepath = `/api/me/progress/${this.libraryItem.id}`
|
||||||
|
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$patch(routepath, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.success(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedSuccess : this.$strings.ToastItemMarkedAsNotFinishedSuccess)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.isProcessingReadUpdate = false
|
||||||
|
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
this.processingRemove = true
|
||||||
|
|
||||||
|
let routepath = `/api/playlists/${this.playlistId}/item/${this.libraryItem.id}`
|
||||||
|
if (this.episodeId) routepath += `/${this.episodeId}`
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(routepath)
|
||||||
|
.then((updatedPlaylist) => {
|
||||||
|
if (!updatedPlaylist.items.length) {
|
||||||
|
console.log(`All items removed so playlist was removed`, updatedPlaylist)
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
|
||||||
|
} else {
|
||||||
|
console.log(`Item removed from playlist`, updatedPlaylist)
|
||||||
|
this.$toast.success('Item removed from playlist')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove item from playlist', error)
|
||||||
|
this.$toast.error('Failed to remove item from playlist')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processingRemove = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,18 +16,26 @@
|
|||||||
|
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
|
||||||
<span class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
||||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'playlist_add' }}</span>
|
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
||||||
</button>
|
</button> -->
|
||||||
|
|
||||||
|
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
||||||
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
|
||||||
|
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
@@ -123,6 +131,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickAddToPlaylist() {
|
||||||
|
this.$emit('addToPlaylist', this.episode)
|
||||||
|
},
|
||||||
clickedEpisode() {
|
clickedEpisode() {
|
||||||
this.$emit('view', this.episode)
|
this.$emit('view', this.episode)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="episode in episodesSorted">
|
<template v-for="episode in episodesSorted">
|
||||||
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" />
|
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
|
||||||
@@ -131,6 +131,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
addToPlaylist(episode) {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
addEpisodeToQueue(episode) {
|
addEpisodeToQueue(episode) {
|
||||||
const queueItem = {
|
const queueItem = {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
|
<nuxt-link v-if="to" :to="to" class="btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
|
||||||
<slot />
|
<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">
|
<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">
|
<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" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
||||||
<slot />
|
<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">
|
<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">
|
<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" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
|
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="material-icons">more_vert</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="menu">
|
||||||
|
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||||
|
<template v-for="(item, index) in items">
|
||||||
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
disabled: Boolean,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = !this.showMenu
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
clickAction(action) {
|
||||||
|
if (this.disabled) return
|
||||||
|
this.showMenu = false
|
||||||
|
this.$emit('action', action)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<span class="material-icons">expand_more</span>
|
<span class="material-icons text-2xl">expand_more</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<span class="block truncate">{{ label }}</span>
|
<span class="block truncate">{{ label }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<span class="material-icons text-gray-100" aria-label="User Account" role="button">person</span>
|
<span class="material-icons text-2xl text-gray-100" aria-label="User Account" role="button">person</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
|
||||||
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning mr-1" style="font-size: 1rem" @click.stop="editItem(item)">edit</span>
|
<span v-if="showEdit" class="material-icons text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
|
||||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
|
||||||
</div>
|
</div>
|
||||||
{{ item[textKey] }}
|
{{ item[textKey] }}
|
||||||
@@ -113,7 +113,10 @@ export default {
|
|||||||
if (this.searching) return
|
if (this.searching) return
|
||||||
this.currentSearch = this.textInput
|
this.currentSearch = this.textInput
|
||||||
this.searching = true
|
this.searching = true
|
||||||
var results = await this.$axios.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`).catch((error) => {
|
const results = await this.$axios
|
||||||
|
.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`)
|
||||||
|
.then((res) => res.results || res)
|
||||||
|
.catch((error) => {
|
||||||
console.error('Failed to get search results', error)
|
console.error('Failed to get search results', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
tooltip: null,
|
tooltip: null,
|
||||||
tooltipId: null,
|
tooltipId: null,
|
||||||
isShowing: false
|
isShowing: false,
|
||||||
|
hideTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -46,10 +47,12 @@ export default {
|
|||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
||||||
tooltip.id = this.tooltipId
|
tooltip.id = this.tooltipId
|
||||||
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
|
tooltip.addEventListener('mouseover', this.cancelHide);
|
||||||
|
tooltip.addEventListener('mouseleave', this.hideTooltip);
|
||||||
|
|
||||||
this.setTooltipPosition(tooltip)
|
this.setTooltipPosition(tooltip)
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isShowing = true
|
this.isShowing = true
|
||||||
},
|
},
|
||||||
hideTooltip() {
|
hideTooltip() {
|
||||||
@@ -102,11 +106,16 @@ export default {
|
|||||||
this.tooltip.remove()
|
this.tooltip.remove()
|
||||||
this.isShowing = false
|
this.isShowing = false
|
||||||
},
|
},
|
||||||
|
cancelHide() {
|
||||||
|
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
||||||
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
if (!this.isShowing) this.showTooltip()
|
if (!this.isShowing) this.showTooltip()
|
||||||
},
|
},
|
||||||
mouseleave() {
|
mouseleave() {
|
||||||
if (this.isShowing) this.hideTooltip()
|
if (this.isShowing) {
|
||||||
|
this.hideTimeout = setTimeout(this.hideTooltip, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -137,8 +137,24 @@ export default {
|
|||||||
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
author: (this.details.authors || []).map((au) => au.name).join(', ')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
|
if (mapType === 'append') {
|
||||||
|
if (key === 'tags') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
} else if (key === 'authors' || key === 'series') {
|
||||||
|
batchDetails[key].forEach((detail) => {
|
||||||
|
const existingDetail = this.details[key].find((_d) => _d.name.toLowerCase() == detail.name.toLowerCase().trim() || _d.id == detail.id)
|
||||||
|
if (!existingDetail) {
|
||||||
|
this.details[key].push({ ...detail })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (key === 'tags') {
|
if (key === 'tags') {
|
||||||
this.newTags = [...batchDetails.tags]
|
this.newTags = [...batchDetails.tags]
|
||||||
} else if (key === 'genres' || key === 'narrators') {
|
} else if (key === 'genres' || key === 'narrators') {
|
||||||
@@ -149,6 +165,7 @@ export default {
|
|||||||
this.details[key] = batchDetails[key]
|
this.details[key] = batchDetails[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
forceBlur() {
|
forceBlur() {
|
||||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||||
|
|||||||
@@ -101,35 +101,35 @@ export default {
|
|||||||
intervalOptions() {
|
intervalOptions() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: 'Custom daily/weekly',
|
text: this.$strings.LabelIntervalCustomDailyWeekly,
|
||||||
value: 'custom'
|
value: 'custom'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every day',
|
text: this.$strings.LabelIntervalEveryDay,
|
||||||
value: 'daily'
|
value: 'daily'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 12 hours',
|
text: this.$strings.LabelIntervalEvery12Hours,
|
||||||
value: '0 */12 * * *'
|
value: '0 */12 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 6 hours',
|
text: this.$strings.LabelIntervalEvery6Hours,
|
||||||
value: '0 */6 * * *'
|
value: '0 */6 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 2 hours',
|
text: this.$strings.LabelIntervalEvery2Hours,
|
||||||
value: '0 */2 * * *'
|
value: '0 */2 * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every hour',
|
text: this.$strings.LabelIntervalEveryHour,
|
||||||
value: '0 * * * *'
|
value: '0 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 30 minutes',
|
text: this.$strings.LabelIntervalEvery30Minutes,
|
||||||
value: '*/30 * * * *'
|
value: '*/30 * * * *'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Every 15 minutes',
|
text: this.$strings.LabelIntervalEvery15Minutes,
|
||||||
value: '*/15 * * * *'
|
value: '*/15 * * * *'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -101,14 +101,14 @@ export default {
|
|||||||
this.updateSelectionMode(this.isSelectionMode)
|
this.updateSelectionMode(this.isSelectionMode)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
|
|
||||||
this.items.forEach((ent) => {
|
this.items.forEach((ent) => {
|
||||||
var component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
|
let component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(ent.id)
|
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -82,14 +82,14 @@ export default {
|
|||||||
this.updateSelectionMode(this.isSelectionMode)
|
this.updateSelectionMode(this.isSelectionMode)
|
||||||
},
|
},
|
||||||
updateSelectionMode(val) {
|
updateSelectionMode(val) {
|
||||||
var selectedLibraryItems = this.$store.state.selectedLibraryItems
|
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
|
||||||
|
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
var component = this.$refs[`slider-item-${item.id}`]
|
let component = this.$refs[`slider-item-${item.id}`]
|
||||||
if (!component || !component.length) return
|
if (!component || !component.length) return
|
||||||
component = component[0]
|
component = component[0]
|
||||||
component.setSelectionMode(val)
|
component.setSelectionMode(val)
|
||||||
component.selected = selectedLibraryItems.includes(item.id)
|
component.selected = selectedMediaItems.some((i) => i.id === item.id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
|
|||||||
@@ -107,8 +107,17 @@ export default {
|
|||||||
author: this.details.author
|
author: this.details.author
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mapBatchDetails(batchDetails) {
|
mapBatchDetails(batchDetails, mapType = 'overwrite') {
|
||||||
for (const key in batchDetails) {
|
for (const key in batchDetails) {
|
||||||
|
if (mapType === 'append') {
|
||||||
|
if (key === 'tags') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.newTags = [...new Set(this.newTags.concat(batchDetails.tags))]
|
||||||
|
} else if (key === 'genres') {
|
||||||
|
// Concat and remove dupes
|
||||||
|
this.details[key] = [...new Set(this.details[key].concat(batchDetails[key]))]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (key === 'tags') {
|
if (key === 'tags') {
|
||||||
this.newTags = [...batchDetails.tags]
|
this.newTags = [...batchDetails.tags]
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
@@ -117,6 +126,7 @@ export default {
|
|||||||
this.details[key] = batchDetails[key]
|
this.details[key] = batchDetails[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
forceBlur() {
|
forceBlur() {
|
||||||
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
if (this.$refs.titleInput) this.$refs.titleInput.blur()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.$store.getters['getNumLibraryItemsSelected'] > 0
|
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
+55
-25
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
<modals-item-edit-modal />
|
<modals-item-edit-modal />
|
||||||
<modals-collections-add-create-modal />
|
<modals-collections-add-create-modal />
|
||||||
<modals-edit-collection-modal />
|
<modals-collections-edit-modal />
|
||||||
|
<modals-playlists-add-create-modal />
|
||||||
|
<modals-playlists-edit-modal />
|
||||||
<modals-podcast-edit-episode />
|
<modals-podcast-edit-episode />
|
||||||
<modals-podcast-view-episode />
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
@@ -40,9 +42,8 @@ export default {
|
|||||||
if (this.$store.state.showEditModal) {
|
if (this.$store.state.showEditModal) {
|
||||||
this.$store.commit('setShowEditModal', false)
|
this.$store.commit('setShowEditModal', false)
|
||||||
}
|
}
|
||||||
if (this.$store.state.selectedLibraryItems) {
|
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
}
|
|
||||||
this.updateBodyClass()
|
this.updateBodyClass()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -53,9 +54,12 @@ export default {
|
|||||||
isCasting() {
|
isCasting() {
|
||||||
return this.$store.state.globals.isCasting
|
return this.$store.state.globals.isCasting
|
||||||
},
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
isShowingSideRail() {
|
isShowingSideRail() {
|
||||||
if (!this.$route.name) return false
|
if (!this.$route.name) return false
|
||||||
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
|
return !this.$route.name.startsWith('config') && this.currentLibraryId
|
||||||
},
|
},
|
||||||
isShowingToolbar() {
|
isShowingToolbar() {
|
||||||
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
||||||
@@ -87,19 +91,19 @@ export default {
|
|||||||
this.socket.emit('auth', token)
|
this.socket.emit('auth', token)
|
||||||
|
|
||||||
if (!this.isFirstSocketConnection || this.socketConnectionToastId !== null) {
|
if (!this.isFirstSocketConnection || this.socketConnectionToastId !== null) {
|
||||||
this.updateSocketConnectionToast('Socket Connected', 'success', 5000)
|
this.updateSocketConnectionToast(this.$strings.ToastSocketConnected, 'success', 5000)
|
||||||
}
|
}
|
||||||
this.isFirstSocketConnection = false
|
this.isFirstSocketConnection = false
|
||||||
this.isSocketConnected = true
|
this.isSocketConnected = true
|
||||||
},
|
},
|
||||||
connectError() {
|
connectError() {
|
||||||
console.error('[SOCKET] connect error')
|
console.error('[SOCKET] connect error')
|
||||||
this.updateSocketConnectionToast('Socket Failed to Connect', 'error', null)
|
this.updateSocketConnectionToast(this.$strings.ToastSocketFailedToConnect, 'error', null)
|
||||||
},
|
},
|
||||||
disconnect() {
|
disconnect() {
|
||||||
console.log('[SOCKET] Disconnected')
|
console.log('[SOCKET] Disconnected')
|
||||||
this.isSocketConnected = false
|
this.isSocketConnected = false
|
||||||
this.updateSocketConnectionToast('Socket Disconnected', 'error', null)
|
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
|
||||||
},
|
},
|
||||||
reconnect() {
|
reconnect() {
|
||||||
console.error('[SOCKET] reconnected')
|
console.error('[SOCKET] reconnected')
|
||||||
@@ -132,14 +136,8 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (payload.backups && payload.backups.length) {
|
|
||||||
this.$store.commit('setBackups', payload.backups)
|
|
||||||
}
|
|
||||||
if (payload.usersOnline) {
|
if (payload.usersOnline) {
|
||||||
this.$store.commit('users/resetUsers')
|
this.$store.commit('users/setUsersOnline', payload.usersOnline)
|
||||||
payload.usersOnline.forEach((user) => {
|
|
||||||
this.$store.commit('users/updateUser', user)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$emit('socket_init')
|
this.$eventBus.$emit('socket_init')
|
||||||
@@ -174,7 +172,7 @@ export default {
|
|||||||
this.$store.commit('libraries/remove', library)
|
this.$store.commit('libraries/remove', library)
|
||||||
|
|
||||||
// When removed currently selected library then set next accessible library
|
// When removed currently selected library then set next accessible library
|
||||||
const currLibraryId = this.$store.state.libraries.currentLibraryId
|
const currLibraryId = this.currentLibraryId
|
||||||
if (currLibraryId === library.id) {
|
if (currLibraryId === library.id) {
|
||||||
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
||||||
if (nextLibrary) {
|
if (nextLibrary) {
|
||||||
@@ -213,7 +211,7 @@ export default {
|
|||||||
libraryItemRemoved(item) {
|
libraryItemRemoved(item) {
|
||||||
if (this.$route.name.startsWith('item')) {
|
if (this.$route.name.startsWith('item')) {
|
||||||
if (this.$route.params.id === item.id) {
|
if (this.$route.params.id === item.id) {
|
||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
this.$router.replace(`/library/${this.currentLibraryId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -282,35 +280,55 @@ export default {
|
|||||||
userUpdated(user) {
|
userUpdated(user) {
|
||||||
if (this.$store.state.user.user.id === user.id) {
|
if (this.$store.state.user.user.id === user.id) {
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
this.$store.commit('user/setSettings', user.settings)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userOnline(user) {
|
userOnline(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userOffline(user) {
|
userOffline(user) {
|
||||||
this.$store.commit('users/removeUser', user)
|
this.$store.commit('users/removeUserOnline', user)
|
||||||
},
|
},
|
||||||
userStreamUpdate(user) {
|
userStreamUpdate(user) {
|
||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userMediaProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateMediaProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
},
|
},
|
||||||
collectionAdded(collection) {
|
collectionAdded(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||||
},
|
},
|
||||||
collectionUpdated(collection) {
|
collectionUpdated(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
this.$store.commit('libraries/addUpdateCollection', collection)
|
this.$store.commit('libraries/addUpdateCollection', collection)
|
||||||
},
|
},
|
||||||
collectionRemoved(collection) {
|
collectionRemoved(collection) {
|
||||||
|
if (this.currentLibraryId !== collection.libraryId) return
|
||||||
if (this.$route.name.startsWith('collection')) {
|
if (this.$route.name.startsWith('collection')) {
|
||||||
if (this.$route.params.id === collection.id) {
|
if (this.$route.params.id === collection.id) {
|
||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`)
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/collections`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$store.commit('libraries/removeCollection', collection)
|
this.$store.commit('libraries/removeCollection', collection)
|
||||||
},
|
},
|
||||||
|
playlistAdded(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
},
|
||||||
|
playlistUpdated(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
},
|
||||||
|
playlistRemoved(playlist) {
|
||||||
|
if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
|
||||||
|
|
||||||
|
if (this.$route.name.startsWith('playlist')) {
|
||||||
|
if (this.$route.params.id === playlist.id) {
|
||||||
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/playlists`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||||
|
},
|
||||||
rssFeedOpen(data) {
|
rssFeedOpen(data) {
|
||||||
this.$store.commit('feeds/addFeed', data)
|
this.$store.commit('feeds/addFeed', data)
|
||||||
},
|
},
|
||||||
@@ -333,6 +351,9 @@ export default {
|
|||||||
this.$toast.info(toast)
|
this.$toast.info(toast)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
adminMessageEvt(message) {
|
||||||
|
this.$toast.info(message)
|
||||||
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
this.socket = this.$nuxtSocket({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
@@ -345,6 +366,7 @@ export default {
|
|||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
console.log('Socket initialized')
|
console.log('Socket initialized')
|
||||||
|
|
||||||
|
// Pre-defined socket events
|
||||||
this.socket.on('connect', this.connect)
|
this.socket.on('connect', this.connect)
|
||||||
this.socket.on('connect_error', this.connectError)
|
this.socket.on('connect_error', this.connectError)
|
||||||
this.socket.on('disconnect', this.disconnect)
|
this.socket.on('disconnect', this.disconnect)
|
||||||
@@ -353,6 +375,7 @@ export default {
|
|||||||
this.socket.io.on('reconnect_error', this.reconnectError)
|
this.socket.io.on('reconnect_error', this.reconnectError)
|
||||||
this.socket.io.on('reconnect_failed', this.reconnectFailed)
|
this.socket.io.on('reconnect_failed', this.reconnectFailed)
|
||||||
|
|
||||||
|
// Event received after authorizing socket
|
||||||
this.socket.on('init', this.init)
|
this.socket.on('init', this.init)
|
||||||
|
|
||||||
// Stream Listeners
|
// Stream Listeners
|
||||||
@@ -382,11 +405,16 @@ export default {
|
|||||||
this.socket.on('user_stream_update', this.userStreamUpdate)
|
this.socket.on('user_stream_update', this.userStreamUpdate)
|
||||||
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
this.socket.on('user_item_progress_updated', this.userMediaProgressUpdate)
|
||||||
|
|
||||||
// User Collection Listeners
|
// Collection Listeners
|
||||||
this.socket.on('collection_added', this.collectionAdded)
|
this.socket.on('collection_added', this.collectionAdded)
|
||||||
this.socket.on('collection_updated', this.collectionUpdated)
|
this.socket.on('collection_updated', this.collectionUpdated)
|
||||||
this.socket.on('collection_removed', this.collectionRemoved)
|
this.socket.on('collection_removed', this.collectionRemoved)
|
||||||
|
|
||||||
|
// User Playlist Listeners
|
||||||
|
this.socket.on('playlist_added', this.playlistAdded)
|
||||||
|
this.socket.on('playlist_updated', this.playlistUpdated)
|
||||||
|
this.socket.on('playlist_removed', this.playlistRemoved)
|
||||||
|
|
||||||
// Scan Listeners
|
// Scan Listeners
|
||||||
this.socket.on('scan_start', this.scanStart)
|
this.socket.on('scan_start', this.scanStart)
|
||||||
this.socket.on('scan_complete', this.scanComplete)
|
this.socket.on('scan_complete', this.scanComplete)
|
||||||
@@ -403,6 +431,8 @@ export default {
|
|||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
|
|
||||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||||
|
|
||||||
|
this.socket.on('admin_message', this.adminMessageEvt)
|
||||||
},
|
},
|
||||||
showUpdateToast(versionData) {
|
showUpdateToast(versionData) {
|
||||||
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
var ignoreVersion = localStorage.getItem('ignoreVersion')
|
||||||
@@ -472,9 +502,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Batch selecting
|
// Batch selecting
|
||||||
if (this.$store.getters['getNumLibraryItemsSelected'] && name === 'Escape') {
|
if (this.$store.getters['globals/getIsBatchSelectingMediaItems'] && name === 'Escape') {
|
||||||
// ESCAPE key cancels batch selection
|
// ESCAPE key cancels batch selection
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||||||
import LazyBookCard from '@/components/cards/LazyBookCard'
|
import LazyBookCard from '@/components/cards/LazyBookCard'
|
||||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||||
|
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -15,6 +16,7 @@ export default {
|
|||||||
getComponentClass() {
|
getComponentClass() {
|
||||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||||
|
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
async mountEntityCard(index) {
|
async mountEntityCard(index) {
|
||||||
@@ -30,7 +32,7 @@ export default {
|
|||||||
shelfEl.appendChild(bookComponent.$el)
|
shelfEl.appendChild(bookComponent.$el)
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
bookComponent.setSelectionMode(true)
|
bookComponent.setSelectionMode(true)
|
||||||
if (this.selectedLibraryItems.includes(bookComponent.libraryItemId) || this.isSelectAll) {
|
if (this.selectedMediaItems.some(i => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
|
||||||
bookComponent.selected = true
|
bookComponent.selected = true
|
||||||
} else {
|
} else {
|
||||||
bookComponent.selected = false
|
bookComponent.selected = false
|
||||||
@@ -87,7 +89,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
instance.setSelectionMode(true)
|
instance.setSelectionMode(true)
|
||||||
if (instance.libraryItemId && this.selectedLibraryItems.includes(instance.libraryItemId) || this.isSelectAll) {
|
if (instance.libraryItemId && this.selectedMediaItems.some(i => i.id === instance.libraryItemId) || this.isSelectAll) {
|
||||||
instance.selected = true
|
instance.selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,10 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
enabled: false,
|
offline: false,
|
||||||
|
cacheAssets: false,
|
||||||
|
preCaching: [],
|
||||||
|
runtimeCaching: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Generated
+2873
-2957
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.4",
|
"version": "2.2.9",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export default {
|
|||||||
if (localStorage.getItem('token')) {
|
if (localStorage.getItem('token')) {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
|
this.$store.commit('libraries/setUserPlaylists', [])
|
||||||
|
this.$store.commit('libraries/setCollections', [])
|
||||||
this.$router.push('/login')
|
this.$router.push('/login')
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="bg-bg page overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="flex items-center py-4 max-w-7xl mx-auto">
|
<div class="flex items-center py-4 px-2 md:px-0 max-w-7xl mx-auto">
|
||||||
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
<nuxt-link :to="`/item/${libraryItem.id}`" class="hover:underline">
|
||||||
<h1 class="text-xl">{{ title }}</h1>
|
<h1 class="text-lg lg:text-xl">{{ title }}</h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<button class="w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click="editItem">
|
<button class="w-7 h-7 flex items-center justify-center mx-4 hover:scale-110 duration-100 transform text-gray-200 hover:text-white" @click="editItem">
|
||||||
<span class="material-icons text-base">edit</span>
|
<span class="material-icons text-base">edit</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<p class="text-base">{{ $strings.LabelDuration }}:</p>
|
<p class="text-base hidden md:block">{{ $strings.LabelDuration }}:</p>
|
||||||
<p class="text-base font-mono ml-8">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap-reverse justify-center py-4">
|
<div class="flex flex-wrap-reverse justify-center py-4 px-2">
|
||||||
<div class="w-full max-w-3xl py-4">
|
<div class="w-full max-w-3xl py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 hidden lg:block" />
|
||||||
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||||
<div class="w-40" />
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mb-3 py-1">
|
<div class="flex items-center mb-3 py-1">
|
||||||
<div class="flex-grow" />
|
<div class="w-12 hidden lg:block" />
|
||||||
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg' : 'primary'" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
|
||||||
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
|
||||||
<ui-btn color="success" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
|
<div class="flex-grow" />
|
||||||
<div class="w-40" />
|
<ui-btn v-if="hasChanges" small class="mx-2" @click.stop="resetChapters">{{ $strings.ButtonReset }}</ui-btn>
|
||||||
|
<ui-btn v-if="hasChanges" color="success" :disabled="!hasChanges" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="showShiftTimes" class="flex mb-4">
|
<div v-if="showShiftTimes" class="flex mb-4">
|
||||||
<div class="w-12"></div>
|
<div class="w-12 hidden lg:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
|
<p class="text-sm mb-1 font-semibold pr-2">{{ $strings.LabelTimeToShift }}</p>
|
||||||
@@ -42,32 +45,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
|
<p class="text-xs py-1.5 text-gray-300 max-w-md">{{ $strings.NoteChapterEditorTimes }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40"></div>
|
<div class="w-32 hidden lg:block" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="w-12"></div>
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
|
||||||
<div class="w-32 px-2">{{ $strings.LabelStart }}</div>
|
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-2">{{ $strings.LabelStart }}</div>
|
||||||
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
|
<div class="flex-grow px-2">{{ $strings.LabelTitle }}</div>
|
||||||
<div class="w-40"></div>
|
<div class="w-32"></div>
|
||||||
</div>
|
</div>
|
||||||
<template v-for="chapter in newChapters">
|
<template v-for="chapter in newChapters">
|
||||||
<div :key="chapter.id" class="flex py-1">
|
<div :key="chapter.id" class="flex py-1">
|
||||||
<div class="w-12">#{{ chapter.id + 1 }}</div>
|
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
|
||||||
<div class="w-32 px-1">
|
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-1">
|
||||||
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
||||||
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input v-model="chapter.title" class="text-xs" />
|
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 px-2 py-1">
|
<div class="w-32 min-w-32 px-2 py-1">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
|
||||||
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
|
||||||
<span class="material-icons-outlined text-base">remove</span>
|
<span class="material-icons-outlined text-base">remove</span>
|
||||||
</button>
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
|
||||||
@@ -75,11 +80,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
|
||||||
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
|
||||||
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-icons-outlined text-base">pause</span>
|
||||||
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
<span v-else class="material-icons-outlined text-base">play_arrow</span>
|
||||||
</button>
|
</button>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
|
||||||
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
|
||||||
@@ -92,8 +99,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-xl py-4">
|
<div class="w-full max-w-xl py-4 px-2">
|
||||||
<p class="text-lg mb-4 font-semibold py-1">{{ $strings.HeaderAudioTracks }}</p>
|
<div class="flex items-center mb-4 py-1">
|
||||||
|
<p class="text-lg font-semibold">{{ $strings.HeaderAudioTracks }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn>
|
||||||
|
<ui-tooltip :text="$strings.MessageSetChaptersFromTracksDescription" direction="top" class="flex items-center mx-1 cursor-default">
|
||||||
|
<span class="material-icons-outlined text-xl text-gray-200">info</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
|
||||||
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
<div class="flex-grow">{{ $strings.LabelFilename }}</div>
|
||||||
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
<div class="w-20">{{ $strings.LabelDuration }}</div>
|
||||||
@@ -173,8 +187,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center pt-2">
|
<div class="flex items-center pt-2">
|
||||||
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
|
||||||
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top">
|
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
|
||||||
<span class="material-icons-outlined">info</span>
|
<span class="material-icons-outlined text-xl text-gray-200">info</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
|
||||||
@@ -186,6 +200,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, app, redirect, from }) {
|
async asyncData({ store, params, app, redirect, from }) {
|
||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
@@ -228,7 +244,8 @@ export default {
|
|||||||
showFindChaptersModal: false,
|
showFindChaptersModal: false,
|
||||||
chapterData: null,
|
chapterData: null,
|
||||||
showSecondInputs: false,
|
showSecondInputs: false,
|
||||||
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES']
|
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
|
||||||
|
hasChanges: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -270,6 +287,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setChaptersFromTracks() {
|
||||||
|
let currentStartTime = 0
|
||||||
|
let index = 0
|
||||||
|
const chapters = []
|
||||||
|
for (const track of this.audioTracks) {
|
||||||
|
chapters.push({
|
||||||
|
id: index++,
|
||||||
|
title: path.basename(track.metadata.filename, path.extname(track.metadata.filename)),
|
||||||
|
start: currentStartTime,
|
||||||
|
end: currentStartTime + track.duration
|
||||||
|
})
|
||||||
|
currentStartTime += track.duration
|
||||||
|
}
|
||||||
|
this.newChapters = chapters
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
|
},
|
||||||
shiftChapterTimes() {
|
shiftChapterTimes() {
|
||||||
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
if (!this.shiftAmount || isNaN(this.shiftAmount) || this.newChapters.length <= 1) {
|
||||||
return
|
return
|
||||||
@@ -300,7 +334,6 @@ export default {
|
|||||||
this.$store.commit('showEditModal', this.libraryItem)
|
this.$store.commit('showEditModal', this.libraryItem)
|
||||||
},
|
},
|
||||||
addChapter(chapter) {
|
addChapter(chapter) {
|
||||||
console.log('Add chapter', chapter)
|
|
||||||
const newChapter = {
|
const newChapter = {
|
||||||
id: chapter.id + 1,
|
id: chapter.id + 1,
|
||||||
start: chapter.start,
|
start: chapter.start,
|
||||||
@@ -315,22 +348,41 @@ export default {
|
|||||||
this.checkChapters()
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
checkChapters() {
|
checkChapters() {
|
||||||
var previousStart = 0
|
let previousStart = 0
|
||||||
|
let hasChanges = this.newChapters.length !== this.chapters.length
|
||||||
|
|
||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
this.newChapters[i].id = i
|
this.newChapters[i].id = i
|
||||||
this.newChapters[i].start = Number(this.newChapters[i].start)
|
this.newChapters[i].start = Number(this.newChapters[i].start)
|
||||||
|
this.newChapters[i].title = (this.newChapters[i].title || '').trim()
|
||||||
|
|
||||||
if (i === 0 && this.newChapters[i].start !== 0) {
|
if (i === 0 && this.newChapters[i].start !== 0) {
|
||||||
this.newChapters[i].error = 'First chapter must start at 0'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorFirstNotZero
|
||||||
} else if (this.newChapters[i].start <= previousStart && i > 0) {
|
} else if (this.newChapters[i].start <= previousStart && i > 0) {
|
||||||
this.newChapters[i].error = 'Invalid start time must be >= previous chapter start time'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorStartLtPrev
|
||||||
} else if (this.newChapters[i].start >= this.mediaDuration) {
|
} else if (this.newChapters[i].start >= this.mediaDuration) {
|
||||||
this.newChapters[i].error = 'Invalid start time must be < duration'
|
this.newChapters[i].error = this.$strings.MessageChapterErrorStartGteDuration
|
||||||
} else {
|
} else {
|
||||||
this.newChapters[i].error = null
|
this.newChapters[i].error = null
|
||||||
}
|
}
|
||||||
previousStart = this.newChapters[i].start
|
previousStart = this.newChapters[i].start
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingChapter = this.chapters[i]
|
||||||
|
if (existingChapter) {
|
||||||
|
const { start, end, title } = this.newChapters[i]
|
||||||
|
if (start !== existingChapter.start || end !== existingChapter.end || title !== existingChapter.title) {
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasChanges = hasChanges
|
||||||
},
|
},
|
||||||
playChapter(chapter) {
|
playChapter(chapter) {
|
||||||
console.log('Play Chapter', chapter.id)
|
console.log('Play Chapter', chapter.id)
|
||||||
@@ -349,8 +401,6 @@ export default {
|
|||||||
const audioTrack = this.tracks.find((at) => {
|
const audioTrack = this.tracks.find((at) => {
|
||||||
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
|
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
|
||||||
})
|
})
|
||||||
console.log('audio track', audioTrack)
|
|
||||||
|
|
||||||
this.selectedChapter = chapter
|
this.selectedChapter = chapter
|
||||||
this.isLoadingChapter = true
|
this.isLoadingChapter = true
|
||||||
|
|
||||||
@@ -365,7 +415,6 @@ export default {
|
|||||||
if (this.$isDev) {
|
if (this.$isDev) {
|
||||||
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
src = `http://localhost:3333${this.$config.routerBasePath}${src}`
|
||||||
}
|
}
|
||||||
console.log('src', src)
|
|
||||||
|
|
||||||
audioEl.src = src
|
audioEl.src = src
|
||||||
audioEl.id = 'chapter-audio'
|
audioEl.id = 'chapter-audio'
|
||||||
@@ -409,11 +458,11 @@ export default {
|
|||||||
|
|
||||||
for (let i = 0; i < this.newChapters.length; i++) {
|
for (let i = 0; i < this.newChapters.length; i++) {
|
||||||
if (this.newChapters[i].error) {
|
if (this.newChapters[i].error) {
|
||||||
this.$toast.error('Chapters have errors')
|
this.$toast.error(this.$strings.ToastChaptersHaveErrors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newChapters[i].title) {
|
if (!this.newChapters[i].title) {
|
||||||
this.$toast.error('Chapters must have titles')
|
this.$toast.error(this.$strings.ToastChaptersMustHaveTitles)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,22 +509,25 @@ export default {
|
|||||||
|
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
applyChapterData() {
|
applyChapterData() {
|
||||||
var index = 0
|
let index = 0
|
||||||
this.newChapters = this.chapterData.chapters
|
this.newChapters = this.chapterData.chapters
|
||||||
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
|
||||||
.map((chap) => {
|
.map((chap) => {
|
||||||
var chapEnd = Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000)
|
|
||||||
return {
|
return {
|
||||||
id: index++,
|
id: index++,
|
||||||
start: chap.startOffsetMs / 1000,
|
start: chap.startOffsetMs / 1000,
|
||||||
end: chapEnd,
|
end: Math.min(this.mediaDuration, (chap.startOffsetMs + chap.lengthMs) / 1000),
|
||||||
title: chap.title
|
title: chap.title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
this.chapterData = null
|
this.chapterData = null
|
||||||
|
|
||||||
|
this.checkChapters()
|
||||||
},
|
},
|
||||||
findChapters() {
|
findChapters() {
|
||||||
if (!this.asinInput) {
|
if (!this.asinInput) {
|
||||||
@@ -509,11 +561,20 @@ export default {
|
|||||||
this.$toast.error('Failed to find chapters')
|
this.$toast.error('Failed to find chapters')
|
||||||
this.showFindChaptersModal = false
|
this.showFindChaptersModal = false
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
resetChapters() {
|
||||||
|
const payload = {
|
||||||
|
message: this.$strings.MessageResetChaptersConfirm,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.initChapters()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
type: 'yesNo'
|
||||||
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
}
|
||||||
this.asinInput = this.mediaMetadata.asin || null
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
initChapters() {
|
||||||
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
this.newChapters = this.chapters.map((c) => ({ ...c }))
|
||||||
if (!this.newChapters.length) {
|
if (!this.newChapters.length) {
|
||||||
this.newChapters = [
|
this.newChapters = [
|
||||||
@@ -525,6 +586,13 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
this.checkChapters()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
|
||||||
|
this.asinInput = this.mediaMetadata.asin || null
|
||||||
|
this.initChapters()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.destroyAudioEl()
|
this.destroyAudioEl()
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default {
|
|||||||
cancelEncodeClick() {
|
cancelEncodeClick() {
|
||||||
this.isCancelingEncode = true
|
this.isCancelingEncode = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/encode-m4b/${this.libraryItemId}/cancel`)
|
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Encode canceled')
|
this.$toast.success('Encode canceled')
|
||||||
})
|
})
|
||||||
@@ -262,7 +262,7 @@ export default {
|
|||||||
encodeM4bClick() {
|
encodeM4bClick() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/encode-m4b/${this.libraryItemId}`)
|
.$post(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Ab m4b merge started')
|
console.log('Ab m4b merge started')
|
||||||
})
|
})
|
||||||
@@ -287,7 +287,7 @@ export default {
|
|||||||
updateAudioFileMetadata() {
|
updateAudioFileMetadata() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/items/${this.libraryItemId}/audio-metadata?tone=1`)
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?tone=1`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio metadata encode started')
|
console.log('Audio metadata encode started')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,14 +2,25 @@
|
|||||||
<div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
<div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<div class="border border-white border-opacity-10 max-w-7xl mx-auto mb-10 mt-5">
|
<div class="border border-white border-opacity-10 max-w-7xl mx-auto mb-10 mt-5">
|
||||||
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
<div class="flex items-center px-4 py-4 cursor-pointer" @click="openMapOptions = !openMapOptions" @mousedown.prevent @mouseup.prevent>
|
||||||
<span class="material-icons">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
<span class="material-icons text-2xl">{{ openMapOptions ? 'expand_less' : 'expand_more' }}</span>
|
||||||
|
|
||||||
<p class="ml-4 text-gray-200 text-lg">Map details</p>
|
<p class="ml-4 text-gray-200 text-lg">{{ $strings.HeaderMapDetails }}</p>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<div class="w-64 flex">
|
||||||
|
<button class="w-32 h-8 rounded-l-md shadow-md border border-gray-600" :class="!isMapOverwrite ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'overwrite'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelOverwrite }}</p>
|
||||||
|
</button>
|
||||||
|
<button class="w-32 h-8 rounded-r-md shadow-md border border-gray-600" :class="!isMapAppend ? 'bg-bg text-white/30' : 'bg-primary'" @click.stop.prevent="mapDetailsType = 'append'">
|
||||||
|
<p class="text-sm">{{ $strings.LabelAppend }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden">
|
<div class="overflow-hidden">
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div v-if="openMapOptions" class="flex flex-wrap">
|
<div v-if="openMapOptions" class="flex flex-wrap">
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
|
||||||
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,13 +29,13 @@
|
|||||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||||
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.series" />
|
<ui-checkbox v-model="selectedBatchUsage.series" />
|
||||||
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="seriesItems" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
<ui-checkbox v-model="selectedBatchUsage.genres" />
|
||||||
@@ -38,15 +49,15 @@
|
|||||||
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
<ui-checkbox v-model="selectedBatchUsage.narrators" />
|
||||||
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
<ui-checkbox v-model="selectedBatchUsage.publisher" />
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.language" />
|
<ui-checkbox v-model="selectedBatchUsage.language" />
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center px-4 w-1/2">
|
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
|
||||||
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
<ui-checkbox v-model="selectedBatchUsage.explicit" />
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<ui-checkbox
|
<ui-checkbox
|
||||||
@@ -91,11 +102,16 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, redirect, app }) {
|
async asyncData({ store, redirect, app }) {
|
||||||
if (!store.state.selectedLibraryItems.length) {
|
if (!store.state.globals.selectedMediaItems.length) {
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
var libraryItems = await app.$axios.$post(`/api/items/batch/get`, { libraryItemIds: store.state.selectedLibraryItems }).catch((error) => {
|
|
||||||
var errorMsg = error.response.data || 'Failed to get items'
|
const libraryItemIds = store.state.globals.selectedMediaItems.map((i) => i.id)
|
||||||
|
const libraryItems = await app.$axios
|
||||||
|
.$post(`/api/items/batch/get`, { libraryItemIds })
|
||||||
|
.then((res) => res.libraryItems)
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response.data || 'Failed to get items'
|
||||||
console.error(errorMsg, error)
|
console.error(errorMsg, error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -109,10 +125,10 @@ export default {
|
|||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
libraryItemCopies: [],
|
libraryItemCopies: [],
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
newSeriesNames: [],
|
|
||||||
newTagItems: [],
|
newTagItems: [],
|
||||||
newGenreItems: [],
|
newGenreItems: [],
|
||||||
newNarratorItems: [],
|
newNarratorItems: [],
|
||||||
|
mapDetailsType: 'overwrite',
|
||||||
batchDetails: {
|
batchDetails: {
|
||||||
subtitle: null,
|
subtitle: null,
|
||||||
authors: null,
|
authors: null,
|
||||||
@@ -137,10 +153,17 @@ export default {
|
|||||||
language: false,
|
language: false,
|
||||||
explicit: false
|
explicit: false
|
||||||
},
|
},
|
||||||
|
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
|
||||||
openMapOptions: false
|
openMapOptions: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMapOverwrite() {
|
||||||
|
return this.mapDetailsType === 'overwrite'
|
||||||
|
},
|
||||||
|
isMapAppend() {
|
||||||
|
return this.mapDetailsType === 'append'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
@@ -153,9 +176,6 @@ export default {
|
|||||||
tagItems() {
|
tagItems() {
|
||||||
return this.tags.concat(this.newTagItems)
|
return this.tags.concat(this.newTagItems)
|
||||||
},
|
},
|
||||||
seriesItems() {
|
|
||||||
return [...this.existingSeriesNames, ...this.newSeriesNames]
|
|
||||||
},
|
|
||||||
narratorItems() {
|
narratorItems() {
|
||||||
return [...this.narrators, ...this.newNarratorItems]
|
return [...this.narrators, ...this.newNarratorItems]
|
||||||
},
|
},
|
||||||
@@ -214,13 +234,15 @@ export default {
|
|||||||
mapBatchDetails() {
|
mapBatchDetails() {
|
||||||
this.blurBatchForm()
|
this.blurBatchForm()
|
||||||
|
|
||||||
var batchMapPayload = {}
|
const batchMapPayload = {}
|
||||||
for (const key in this.selectedBatchUsage) {
|
for (const key in this.selectedBatchUsage) {
|
||||||
if (this.selectedBatchUsage[key]) {
|
if (!this.selectedBatchUsage[key]) continue
|
||||||
|
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
|
||||||
|
|
||||||
if (key === 'series') {
|
if (key === 'series') {
|
||||||
// Map string of series to series objects
|
// Map string of series to series objects
|
||||||
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
batchMapPayload[key] = this.batchDetails[key].map((seItem) => {
|
||||||
var existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
const existingSeries = this.series.find((se) => se.name.toLowerCase() === seItem.toLowerCase().trim())
|
||||||
if (existingSeries) {
|
if (existingSeries) {
|
||||||
return existingSeries
|
return existingSeries
|
||||||
} else {
|
} else {
|
||||||
@@ -234,11 +256,10 @@ export default {
|
|||||||
batchMapPayload[key] = this.batchDetails[key]
|
batchMapPayload[key] = this.batchDetails[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.libraryItemCopies.forEach((li) => {
|
this.libraryItemCopies.forEach((li) => {
|
||||||
var ref = this.getEditFormRef(li.id)
|
const ref = this.getEditFormRef(li.id)
|
||||||
ref.mapBatchDetails(batchMapPayload)
|
ref.mapBatchDetails(batchMapPayload, this.mapDetailsType)
|
||||||
})
|
})
|
||||||
this.$toast.success('Details mapped')
|
this.$toast.success('Details mapped')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,15 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
||||||
|
<span class="material-icons text-xl">edit</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
<ui-context-menu-dropdown :items="contextMenuItems" class="mx-px" @action="contextMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl">
|
||||||
@@ -32,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
<div v-show="processing" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,6 +54,11 @@ export default {
|
|||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If collection is a different library then set library as current
|
||||||
|
if (collection.libraryId !== store.state.libraries.currentLibraryId) {
|
||||||
|
await store.dispatch('libraries/fetch', collection.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
store.commit('libraries/addUpdateCollection', collection)
|
store.commit('libraries/addUpdateCollection', collection)
|
||||||
return {
|
return {
|
||||||
collectionId: collection.id
|
collectionId: collection.id
|
||||||
@@ -59,8 +66,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingRemove: false,
|
processing: false
|
||||||
collectionCopy: {}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -88,7 +94,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
streaming() {
|
streaming() {
|
||||||
return !!this.playableBooks.find((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
return !!this.playableBooks.some((b) => b.id === this.$store.getters['getLibraryItemIdStreaming'])
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return this.playableBooks.length
|
return this.playableBooks.length
|
||||||
@@ -98,26 +104,66 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.$strings.MessagePlaylistCreateFromCollection,
|
||||||
|
action: 'create-playlist'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.userCanDelete) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'delete') {
|
||||||
|
this.removeClick()
|
||||||
|
} else if (action === 'create-playlist') {
|
||||||
|
this.createPlaylistFromCollection()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createPlaylistFromCollection() {
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/playlists/collection/${this.collectionId}`)
|
||||||
|
.then((playlist) => {
|
||||||
|
if (playlist) {
|
||||||
|
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess)
|
||||||
|
this.$router.push(`/playlist/${playlist.id}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || this.$strings.ToastPlaylistCreateFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$store.commit('globals/setEditCollection', this.collection)
|
this.$store.commit('globals/setEditCollection', this.collection)
|
||||||
},
|
},
|
||||||
removeClick() {
|
removeClick() {
|
||||||
if (confirm(`Are you sure you want to remove collection "${this.collectionName}"?`)) {
|
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||||
this.processingRemove = true
|
this.processing = true
|
||||||
var collectionName = this.collectionName
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/collections/${this.collection.id}`)
|
.$delete(`/api/collections/${this.collection.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.processingRemove = false
|
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
|
||||||
this.$toast.success(`Collection "${collectionName}" Removed`)
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove collection', error)
|
console.error('Failed to remove collection', error)
|
||||||
this.processingRemove = false
|
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||||
this.$toast.error(`Failed to remove collection`)
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+15
-4
@@ -3,7 +3,7 @@
|
|||||||
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
||||||
<div class="configContent" :class="`page-${currentPage}`">
|
<div class="configContent" :class="`page-${currentPage}`">
|
||||||
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
||||||
<span class="material-icons cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
<span class="material-icons text-2xl cursor-pointer" @click.stop.prevent="showMore">more_vert</span>
|
||||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
@@ -42,10 +42,21 @@ export default {
|
|||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
currentPage() {
|
currentPage() {
|
||||||
if (!this.$route.name) return 'Settings'
|
if (!this.$route.name) return this.$strings.HeaderSettings
|
||||||
var routeName = this.$route.name.split('-')
|
var routeName = this.$route.name.split('-')
|
||||||
if (routeName.length > 0) return routeName.slice(1).join('-')
|
if (routeName.length > 0) {
|
||||||
return 'Settings'
|
const pageName = routeName.slice(1).join('-')
|
||||||
|
if (pageName === 'log') return this.$strings.HeaderLogs
|
||||||
|
else if (pageName === 'backups') return this.$strings.HeaderBackups
|
||||||
|
else if (pageName === 'libraries') return this.$strings.HeaderLibraries
|
||||||
|
else if (pageName === 'notifications') return this.$strings.HeaderNotifications
|
||||||
|
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
||||||
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
|
}
|
||||||
|
return this.$strings.HeaderSettings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderBackups }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-base mb-2 text-gray-300">{{ $strings.MessageBackupsDescription }} <span class="font-mono text-gray-100">/metadata/items</span> & <span class="font-mono text-gray-100">/metadata/authors</span>.</p>
|
|
||||||
<p class="text-base mb-4 text-gray-300">{{ $strings.MessageBackupsNote }}</p>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
<ui-toggle-switch v-model="enableBackups" small :disabled="updatingServerSettings" @input="updateBackupsSettings" />
|
||||||
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
|
<ui-tooltip :text="$strings.LabelBackupsEnableAutomaticBackupsHelp">
|
||||||
@@ -17,7 +10,7 @@
|
|||||||
|
|
||||||
<div v-if="enableBackups" class="mb-6">
|
<div v-if="enableBackups" class="mb-6">
|
||||||
<div class="flex items-center pl-6">
|
<div class="flex items-center pl-6">
|
||||||
<span class="material-icons-outlined text-black-50">schedule</span>
|
<span class="material-icons-outlined text-2xl text-black-50">schedule</span>
|
||||||
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
<p class="text-gray-100 px-2">{{ scheduleDescription }}</p>
|
||||||
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
<span class="material-icons text-lg text-black-50 hover:text-yellow-500 cursor-pointer" @click="showCronBuilder = !showCronBuilder">edit</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,9 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
|
<app-settings-content :header-text="$strings.HeaderSettings">
|
||||||
<div class="mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderSettings }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lg:flex">
|
<div class="lg:flex">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
@@ -15,7 +11,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +21,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +31,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +53,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,7 +63,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +89,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsParseSubtitles }}
|
{{ $strings.LabelSettingsParseSubtitles }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +99,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsFindCovers }}
|
{{ $strings.LabelSettingsFindCovers }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -117,7 +113,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +123,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +133,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +143,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +153,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsDisableWatcher }}
|
{{ $strings.LabelSettingsDisableWatcher }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +168,7 @@
|
|||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsExperimentalFeatures }}
|
{{ $strings.LabelSettingsExperimentalFeatures }}
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -183,7 +179,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsEnableEReader }}
|
{{ $strings.LabelSettingsEnableEReader }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,22 +189,23 @@
|
|||||||
<ui-tooltip text="Tone library for metadata">
|
<ui-tooltip text="Tone library for metadata">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
Use Tone library for metadata
|
Use Tone library for metadata
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeCache">{{ $strings.ButtonPurgeAllCache }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isPurgingCache" @click.stop="purgeCache">{{ $strings.ButtonPurgeAllCache }}</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click.stop="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isPurgingCache" @click.stop="purgeItemsCache">{{ $strings.ButtonPurgeItemsCache }}</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">{{ $strings.ButtonRemoveAllLibraryItems }}</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="mr-2 text-xs md:text-sm" :loading="isResettingLibraryItems" @click="resetLibraryItems">{{ $strings.ButtonRemoveAllLibraryItems }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm font-book text-yellow-400">
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageGenres }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!genres.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoGenres }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(genre, index) in genres">
|
||||||
|
<div :key="genre" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingGenre !== genre" class="text-sm md:text-base text-gray-100">{{ genre }}</p>
|
||||||
|
<ui-text-input v-else v-model="newGenreName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingGenre !== genre">
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editClick(genre)" />
|
||||||
|
<ui-icon-btn v-if="editingGenre !== genre" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeClick(genre)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
genres: [],
|
||||||
|
editingGenre: null,
|
||||||
|
newGenreName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newGenreName = ''
|
||||||
|
this.editingGenre = null
|
||||||
|
},
|
||||||
|
removeClick(genre) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove genre "${genre}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeGenre(genre)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
editClick(genre) {
|
||||||
|
this.newGenreName = genre
|
||||||
|
this.editingGenre = genre
|
||||||
|
},
|
||||||
|
saveClick() {
|
||||||
|
this.newGenreName = this.newGenreName.trim()
|
||||||
|
if (!this.newGenreName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingGenre === this.newGenreName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const genreNameExists = this.genres.find((g) => g !== this.editingGenre && g === this.newGenreName)
|
||||||
|
const genreNameExistsOfDifferentCase = !genreNameExists ? this.genres.find((g) => g !== this.editingGenre && g.toLowerCase() === this.newGenreName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName])
|
||||||
|
if (genreNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameGenreMergeNote}</span>`
|
||||||
|
} else if (genreNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameGenre()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameGenre() {
|
||||||
|
this.loading = true
|
||||||
|
let _newGenreName = this.newGenreName
|
||||||
|
let _editingGenre = this.editingGenre
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
genre: _editingGenre,
|
||||||
|
newGenre: _newGenreName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/genres/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.genreMerged) {
|
||||||
|
this.genres = this.genres.filter((g) => g !== _newGenreName)
|
||||||
|
}
|
||||||
|
this.genres = this.genres.map((g) => {
|
||||||
|
if (g === _editingGenre) return _newGenreName
|
||||||
|
return g
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename genre', error)
|
||||||
|
this.$toast.error('Failed to rename genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeGenre(genre) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/genres/${this.$encode(genre)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.genres = this.genres.filter((g) => g !== genre)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove genre', error)
|
||||||
|
this.$toast.error('Failed to remove genre')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/genres')
|
||||||
|
.then((data) => {
|
||||||
|
this.genres = (data.genres || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load genres', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="'Item Metadata Utils'">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/tags" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 mt-6 mb-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageTags }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link to="/config/item-metadata-utils/genres" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p>{{ $strings.HeaderManageGenres }}</p>
|
||||||
|
<span class="material-icons">arrow_forward</span>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</app-settings-content>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
init() {}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8 relative" style="min-height: 200px">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<h1 class="text-xl mx-2">{{ $strings.HeaderManageTags }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!tags.length && !loading" class="text-center py-8 text-lg">{{ $strings.MessageNoTags }}</p>
|
||||||
|
|
||||||
|
<div class="border border-white/10">
|
||||||
|
<template v-for="(tag, index) in tags">
|
||||||
|
<div :key="tag" class="w-full p-2 flex items-center text-gray-400 hover:text-white" :class="{ 'bg-primary/20': index % 2 === 0 }">
|
||||||
|
<p v-if="editingTag !== tag" class="text-sm md:text-base text-gray-100">{{ tag }}</p>
|
||||||
|
<ui-text-input v-else v-model="newTagName" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<template v-if="editingTag !== tag">
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||||
|
<ui-icon-btn v-if="editingTag !== tag" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
tags: [],
|
||||||
|
editingTag: null,
|
||||||
|
newTagName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
cancelEditClick() {
|
||||||
|
this.newTagName = ''
|
||||||
|
this.editingTag = null
|
||||||
|
},
|
||||||
|
removeTagClick(tag) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to remove tag "${tag}" from all items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeTag(tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
saveTagClick() {
|
||||||
|
this.newTagName = this.newTagName.trim()
|
||||||
|
if (!this.newTagName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editingTag === this.newTagName) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagNameExists = this.tags.find((t) => t !== this.editingTag && t === this.newTagName)
|
||||||
|
const tagNameExistsOfDifferentCase = !tagNameExists ? this.tags.find((t) => t !== this.editingTag && t.toLowerCase() === this.newTagName.toLowerCase()) : null
|
||||||
|
|
||||||
|
let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName])
|
||||||
|
if (tagNameExists) {
|
||||||
|
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameTagMergeNote}</span>`
|
||||||
|
} else if (tagNameExistsOfDifferentCase) {
|
||||||
|
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.renameTag()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
renameTag() {
|
||||||
|
this.loading = true
|
||||||
|
let _newTagName = this.newTagName
|
||||||
|
let _editingTag = this.editingTag
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
tag: _editingTag,
|
||||||
|
newTag: _newTagName
|
||||||
|
}
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/tags/rename', payload)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
if (res.tagMerged) {
|
||||||
|
this.tags = this.tags.filter((t) => t !== _newTagName)
|
||||||
|
}
|
||||||
|
this.tags = this.tags.map((t) => {
|
||||||
|
if (t === _editingTag) return _newTagName
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
this.cancelEditClick()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to rename tag', error)
|
||||||
|
this.$toast.error('Failed to rename tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeTag(tag) {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/tags/${this.$encode(tag)}`)
|
||||||
|
.then((res) => {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [res.numItemsUpdated]))
|
||||||
|
this.tags = this.tags.filter((t) => t !== tag)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove tag', error)
|
||||||
|
this.$toast.error('Failed to remove tag')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
editTagClick(tag) {
|
||||||
|
this.newTagName = tag
|
||||||
|
this.editingTag = tag
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$get('/api/tags')
|
||||||
|
.then((data) => {
|
||||||
|
this.tags = (data.tags || []).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load tags', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<app-settings-content :header-text="$strings.HeaderLibraries" show-add-button @clicked="setShowLibraryModal">
|
||||||
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
<tables-library-libraries-table @showLibraryModal="setShowLibraryModal" />
|
||||||
|
</app-settings-content>
|
||||||
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLibraryStats }}: {{ currentLibraryName }}</h1>
|
<app-settings-content :header-text="$strings.HeaderLibraryStats + ': ' + currentLibraryName">
|
||||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||||
|
|
||||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
+19
-20
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderLogs">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderLogs }}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between mb-2 place-items-end">
|
<div class="flex justify-between mb-2 place-items-end">
|
||||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@
|
|||||||
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
|
<p class="text-xl text-gray-200 mb-2">{{ $strings.MessageNoLogs }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,20 +35,6 @@ export default {
|
|||||||
searchText: null,
|
searchText: null,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
|
logColors: ['yellow-200', 'gray-400', 'info', 'warning', 'error', 'red-800', 'blue-400'],
|
||||||
logLevels: [
|
|
||||||
{
|
|
||||||
value: 1,
|
|
||||||
text: 'Debug'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
text: 'Info'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 3,
|
|
||||||
text: 'Warn'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
loadedLogs: []
|
loadedLogs: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -66,6 +49,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
logLevels() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
text: this.$strings.LabelLogLevelDebug
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
text: this.$strings.LabelLogLevelInfo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
text: this.$strings.LabelLogLevelWarn
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
logLevelItems() {
|
logLevelItems() {
|
||||||
if (process.env.NODE_ENV === 'production') return this.logLevels
|
if (process.env.NODE_ENV === 'production') return this.logLevels
|
||||||
this.logLevels.unshift({ text: 'Trace', value: 0 })
|
this.logLevels.unshift({ text: 'Trace', value: 0 })
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-3 md:p-8 mb-2 max-w-3xl mx-auto">
|
<app-settings-content :header-text="$strings.HeaderAppriseNotificationSettings" :description="$strings.MessageAppriseDescription">
|
||||||
<h2 class="text-xl font-semibold mb-4">{{ $strings.HeaderAppriseNotificationSettings }}</h2>
|
|
||||||
<p class="mb-6 text-gray-200">
|
|
||||||
In order to use this feature you will need to have an instance of <a href="https://github.com/caronc/apprise-api" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">Apprise API</a> running or an api that will handle those same requests. <br />The Apprise API Url should be the full URL path to send the notification, e.g., if your API instance is served at
|
|
||||||
<span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337</span> then you would put <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">http://192.168.1.1:8337/notify</span>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<ui-text-input-with-label ref="apiUrlInput" v-model="appriseApiUrl" :disabled="savingSettings" label="Apprise API Url" class="mb-2" />
|
<ui-text-input-with-label ref="apiUrlInput" v-model="appriseApiUrl" :disabled="savingSettings" label="Apprise API Url" class="mb-2" />
|
||||||
|
|
||||||
@@ -44,7 +38,7 @@
|
|||||||
<template v-for="notification in notifications">
|
<template v-for="notification in notifications">
|
||||||
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
|
<cards-notification-card :key="notification.id" :notification="notification" @update="updateSettings" @edit="editNotification" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" @update="updateSettings" />
|
<modals-notification-edit-modal v-model="showEditModal" :notification="selectedNotification" :notification-data="notificationData" @update="updateSettings" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<app-settings-content :header-text="$strings.HeaderListeningSessions">
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">{{ $strings.HeaderListeningSessions }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mb-2">
|
<div class="flex justify-end mb-2">
|
||||||
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
|
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
|
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
|
||||||
</div>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" @removedSession="removedSession" />
|
||||||
</div>
|
</div>
|
||||||
@@ -65,10 +61,10 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ params, redirect, app }) {
|
||||||
var users = await app.$axios
|
const users = await app.$axios
|
||||||
.$get('/api/users')
|
.$get('/api/users')
|
||||||
.then((users) => {
|
.then((res) => {
|
||||||
return users.sort((a, b) => {
|
return res.users.sort((a, b) => {
|
||||||
return a.createdAt - b.createdAt
|
return a.createdAt - b.createdAt
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -88,6 +84,7 @@ export default {
|
|||||||
numPages: 0,
|
numPages: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
|
itemsPerPage: 10,
|
||||||
userFilter: null,
|
userFilter: null,
|
||||||
selectedUser: '',
|
selectedUser: '',
|
||||||
processingGoToTimestamp: false
|
processingGoToTimestamp: false
|
||||||
@@ -101,7 +98,7 @@ export default {
|
|||||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||||
},
|
},
|
||||||
userItems() {
|
userItems() {
|
||||||
var userItems = [{ value: '', text: 'All Users' }]
|
var userItems = [{ value: '', text: this.$strings.LabelAllUsers }]
|
||||||
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||||
},
|
},
|
||||||
filteredUserUsername() {
|
filteredUserUsername() {
|
||||||
@@ -112,6 +109,16 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removedSession() {
|
removedSession() {
|
||||||
|
// If on last page and this was the last session then load prev page
|
||||||
|
if (this.currentPage == this.numPages - 1) {
|
||||||
|
const newTotal = this.total - 1
|
||||||
|
const newNumPages = Math.ceil(newTotal / this.itemsPerPage)
|
||||||
|
if (newNumPages < this.numPages) {
|
||||||
|
this.prevPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.loadSessions(this.currentPage)
|
this.loadSessions(this.currentPage)
|
||||||
},
|
},
|
||||||
async clickCurrentTime(session) {
|
async clickCurrentTime(session) {
|
||||||
@@ -208,7 +215,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async loadSessions(page) {
|
async loadSessions(page) {
|
||||||
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
||||||
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => {
|
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
<div>
|
||||||
<h1 class="text-xl">{{ $strings.HeaderYourStats }}</h1>
|
<app-settings-content :header-text="$strings.HeaderYourStats">
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
||||||
@@ -62,8 +61,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
|
||||||
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default {
|
|||||||
numPages: 0,
|
numPages: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
|
itemsPerPage: 10,
|
||||||
processingGoToTimestamp: false
|
processingGoToTimestamp: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,6 +100,16 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removedSession() {
|
removedSession() {
|
||||||
|
// If on last page and this was the last session then load prev page
|
||||||
|
if (this.currentPage == this.numPages - 1) {
|
||||||
|
const newTotal = this.total - 1
|
||||||
|
const newNumPages = Math.ceil(newTotal / this.itemsPerPage)
|
||||||
|
if (newNumPages < this.numPages) {
|
||||||
|
this.prevPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.loadSessions(this.currentPage)
|
this.loadSessions(this.currentPage)
|
||||||
},
|
},
|
||||||
async clickCurrentTime(session) {
|
async clickCurrentTime(session) {
|
||||||
@@ -191,7 +202,7 @@ export default {
|
|||||||
return 'Unknown'
|
return 'Unknown'
|
||||||
},
|
},
|
||||||
async loadSessions(page) {
|
async loadSessions(page) {
|
||||||
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=10`).catch((err) => {
|
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=${this.itemsPerPage}`).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<app-settings-content :header-text="$strings.HeaderUsers" show-add-button @clicked="setShowUserModal">
|
||||||
<tables-users-table />
|
<tables-users-table />
|
||||||
|
</app-settings-content>
|
||||||
|
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
selectedAccount: null,
|
||||||
|
showAccountModal: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {},
|
methods: {
|
||||||
|
setShowUserModal(selectedAccount) {
|
||||||
|
this.selectedAccount = selectedAccount
|
||||||
|
this.showAccountModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<template v-if="!isVideo">
|
<template v-if="!isVideo">
|
||||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
<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">
|
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
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">, </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">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
<template v-for="(narrator, index) in narrators">
|
<template v-for="(narrator, index) in narrators">
|
||||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
<template v-for="(genre, index) in genres">
|
<template v-for="(genre, index) in genres">
|
||||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||||
@@ -129,20 +129,20 @@
|
|||||||
<!-- Icon buttons -->
|
<!-- Icon buttons -->
|
||||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
||||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
|
||||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
|
<ui-tooltip v-if="showQueueBtn" :text="isQueued ? $strings.ButtonQueueRemoveItem : $strings.ButtonQueueAddItem" direction="top">
|
||||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_add'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" :bg-color="isQueued ? 'primary' : 'success bg-opacity-60'" class="mx-0.5" :class="isQueued ? 'text-success' : ''" @click="queueBtnClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
|
||||||
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
|
<span class="material-icons text-2xl -ml-2 pr-2 text-white">auto_stories</span>
|
||||||
{{ $strings.ButtonRead }}
|
{{ $strings.ButtonRead }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
@@ -158,6 +158,10 @@
|
|||||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="!isPodcast && tracks.length" :text="$strings.LabelYourPlaylists" direction="top">
|
||||||
|
<ui-icon-btn icon="playlist_add" class="mx-0.5" outlined @click="playlistsClick" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- Only admin or root user can download new episodes -->
|
<!-- Only admin or root user can download new episodes -->
|
||||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||||
@@ -608,6 +612,10 @@ export default {
|
|||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||||
},
|
},
|
||||||
|
playlistsClick() {
|
||||||
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem }])
|
||||||
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
},
|
||||||
clickRSSFeed() {
|
clickRSSFeed() {
|
||||||
this.showRssFeedModal = true
|
this.showRssFeedModal = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
this.authors = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors`).catch((error) => {
|
this.authors = await this.$axios
|
||||||
|
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
|
||||||
|
.then((response) => response.authors)
|
||||||
|
.catch((error) => {
|
||||||
console.error('Failed to load authors', error)
|
console.error('Failed to load authors', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, query, store, app, redirect }) {
|
async asyncData({ params, query, store, redirect }) {
|
||||||
var libraryId = params.library
|
var libraryId = params.library
|
||||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!libraryData) {
|
if (!libraryData) {
|
||||||
@@ -15,18 +15,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set series sort by
|
// Set series sort by
|
||||||
if (params.id === 'series') {
|
if (query.filter || query.sort || query.desc) {
|
||||||
console.log('Series page', query)
|
const isSeries = params.id === 'series'
|
||||||
if (query.sort) {
|
const settingsUpdate = {
|
||||||
store.commit('libraries/setSeriesSortBy', query.sort)
|
[isSeries ? 'seriesFilterBy' : 'filterBy']: query.filter || undefined,
|
||||||
store.commit('libraries/setSeriesSortDesc', !!query.desc)
|
[isSeries ? 'seriesSortBy' : 'orderBy']: query.sort || undefined,
|
||||||
|
[isSeries ? 'seriesSortDesc' : 'orderDesc']: query.desc == '0' ? false : query.desc == '1' ? true : undefined
|
||||||
}
|
}
|
||||||
if (query.filter) {
|
store.dispatch('user/updateUserSettings', settingsUpdate)
|
||||||
console.log('has filter', query.filter)
|
|
||||||
store.commit('libraries/setSeriesFilterBy', query.filter)
|
|
||||||
}
|
|
||||||
} else if (query.filter) {
|
|
||||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect podcast libraries
|
// Redirect podcast libraries
|
||||||
|
|||||||
@@ -4,29 +4,41 @@
|
|||||||
|
|
||||||
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
<div class="w-full max-w-3xl mx-auto py-4">
|
<div class="w-full max-w-3xl mx-auto py-4">
|
||||||
<p class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</p>
|
<p class="text-xl mb-2 font-semibold px-4 md:px-0">{{ $strings.HeaderLatestEpisodes }}</p>
|
||||||
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
<template v-for="(episode, index) in episodesMapped">
|
<template v-for="(episode, index) in episodesMapped">
|
||||||
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
|
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
|
||||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" />
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
|
||||||
<div class="flex-grow pl-4 max-w-2xl">
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
|
<!-- mobile -->
|
||||||
|
<div class="flex md:hidden mb-2">
|
||||||
|
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
|
||||||
|
<div class="flex-grow px-2">
|
||||||
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
|
||||||
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- desktop -->
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<nuxt-link :to="`/item/${episode.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ episode.podcast.metadata.title }}</nuxt-link>
|
||||||
|
|
||||||
<p class="font-semibold mb-2">{{ episode.title }}</p>
|
<p class="text-xs text-gray-300 mb-1">{{ $dateDistanceFromNow(episode.publishedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="font-semibold mb-2 text-sm md:text-base">{{ episode.title }}</p>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||||
<span v-if="episodeIdStreaming === episode.id" class="material-icons" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
<span v-if="episodeIdStreaming === episode.id" class="material-icons text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
|
||||||
<span v-else class="material-icons text-success">play_arrow</span>
|
<span v-else class="material-icons text-2xl text-success">play_arrow</span>
|
||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
|
||||||
<span class="material-icons-outlined">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||||
this.$store.commit('user/setUser', user)
|
this.$store.commit('user/setUser', user)
|
||||||
|
|
||||||
|
this.$store.dispatch('user/loadUserSettings')
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
|
||||||
|
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||||
|
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 200px">
|
||||||
|
<div class="relative" style="height: fit-content">
|
||||||
|
<covers-playlist-cover :items="playlistItems" :width="200" :height="200" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
|
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
|
||||||
|
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
|
||||||
|
{{ playlistName }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
|
||||||
|
<span v-show="!streaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
|
</ui-btn>
|
||||||
|
|
||||||
|
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
|
||||||
|
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-8 max-w-2xl">
|
||||||
|
<p class="text-base text-gray-100">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlistId" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, app, redirect, route }) {
|
||||||
|
if (!store.state.user.user) {
|
||||||
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
|
}
|
||||||
|
var playlist = await app.$axios.$get(`/api/playlists/${params.id}`).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!playlist) {
|
||||||
|
return redirect('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If playlist is a different library then set library as current
|
||||||
|
if (playlist.libraryId !== store.state.libraries.currentLibraryId) {
|
||||||
|
await store.dispatch('libraries/fetch', playlist.libraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.commit('libraries/addUpdateUserPlaylist', playlist)
|
||||||
|
return {
|
||||||
|
playlistId: playlist.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processingRemove: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
playlistItems() {
|
||||||
|
return this.playlist.items || []
|
||||||
|
},
|
||||||
|
playlistName() {
|
||||||
|
return this.playlist.name || ''
|
||||||
|
},
|
||||||
|
description() {
|
||||||
|
return this.playlist.description || ''
|
||||||
|
},
|
||||||
|
playlist() {
|
||||||
|
return this.$store.getters['libraries/getPlaylist'](this.playlistId) || {}
|
||||||
|
},
|
||||||
|
playableItems() {
|
||||||
|
return this.playlistItems.filter((item) => {
|
||||||
|
const libraryItem = item.libraryItem
|
||||||
|
if (libraryItem.isMissing || libraryItem.isInvalid) return false
|
||||||
|
if (item.episode) return item.episode.audioFile
|
||||||
|
return libraryItem.media.tracks.length
|
||||||
|
})
|
||||||
|
},
|
||||||
|
streaming() {
|
||||||
|
return !!this.playableItems.find((i) => this.$store.getters['getIsMediaStreaming'](i.libraryItemId, i.episodeId))
|
||||||
|
},
|
||||||
|
showPlayButton() {
|
||||||
|
return this.playableItems.length
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
userCanDelete() {
|
||||||
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClick() {
|
||||||
|
this.$store.commit('globals/setEditPlaylist', this.playlist)
|
||||||
|
},
|
||||||
|
removeClick() {
|
||||||
|
if (confirm(`Are you sure you want to remove playlist "${this.playlistName}"?`)) {
|
||||||
|
this.processingRemove = true
|
||||||
|
var playlistName = this.playlistName
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/playlists/${this.playlist.id}`)
|
||||||
|
.then(() => {
|
||||||
|
this.processingRemove = false
|
||||||
|
this.$toast.success(`Playlist "${playlistName}" Removed`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove playlist', error)
|
||||||
|
this.processingRemove = false
|
||||||
|
this.$toast.error(`Failed to remove playlist`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickPlay() {
|
||||||
|
const queueItems = []
|
||||||
|
|
||||||
|
// Playlist queue will start at the first unfinished item
|
||||||
|
// if all items are finished then entire playlist is queued
|
||||||
|
const itemsWithProgress = this.playableItems.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
progress: this.$store.getters['user/getUserMediaProgress'](item.libraryItemId, item.episodeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasUnfinishedItems = itemsWithProgress.some((i) => !i.progress || !i.progress.isFinished)
|
||||||
|
if (!hasUnfinishedItems) {
|
||||||
|
console.warn('All items in playlist are finished - starting at first item')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < itemsWithProgress.length; i++) {
|
||||||
|
const playlistItem = itemsWithProgress[i]
|
||||||
|
if (!hasUnfinishedItems || !playlistItem.progress || !playlistItem.progress.isFinished) {
|
||||||
|
const libraryItem = playlistItem.libraryItem
|
||||||
|
if (playlistItem.episode) {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: libraryItem.libraryId,
|
||||||
|
episodeId: playlistItem.episode.id,
|
||||||
|
title: playlistItem.episode.title,
|
||||||
|
subtitle: libraryItem.media.metadata.title,
|
||||||
|
caption: '',
|
||||||
|
duration: playlistItem.episode.duration || null,
|
||||||
|
coverPath: libraryItem.media.coverPath || null
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
queueItems.push({
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: libraryItem.libraryId,
|
||||||
|
episodeId: null,
|
||||||
|
title: libraryItem.media.metadata.title,
|
||||||
|
subtitle: libraryItem.media.metadata.authors.map((au) => au.name).join(', '),
|
||||||
|
caption: '',
|
||||||
|
duration: libraryItem.media.duration || null,
|
||||||
|
coverPath: libraryItem.media.coverPath || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queueItems.length >= 0) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: queueItems[0].libraryItemId,
|
||||||
|
episodeId: queueItems[0].episodeId,
|
||||||
|
queueItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<!-- Picker display -->
|
<!-- Picker display -->
|
||||||
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
|
||||||
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
|
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
|
||||||
<p class="text-center text-sm my-5">or</p>
|
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
|
||||||
<div class="w-full max-w-xl mx-auto">
|
<div class="w-full max-w-xl mx-auto">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const languageCodeMap = {
|
|||||||
'de': 'Deutsch',
|
'de': 'Deutsch',
|
||||||
'en-us': 'English',
|
'en-us': 'English',
|
||||||
// 'es': 'Español',
|
// 'es': 'Español',
|
||||||
|
'fr': 'Français',
|
||||||
'hr': 'Hrvatski',
|
'hr': 'Hrvatski',
|
||||||
'it': 'Italiano',
|
'it': 'Italiano',
|
||||||
'pl': 'Polski',
|
'pl': 'Polski',
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { formatDistance, format, addDays, isDate } from 'date-fns'
|
|||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
Vue.prototype.$eventBus = new Vue()
|
|
||||||
|
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
@@ -30,23 +28,26 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => {
|
|||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
||||||
if (typeof input !== 'string') {
|
if (typeof filename !== 'string') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
// Most file systems use number of bytes for max filename
|
||||||
const MAX_FILENAME_LEN = 240
|
// to support most filesystems we will use max of 255 bytes in utf-16
|
||||||
|
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
|
||||||
|
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
|
||||||
|
const MAX_FILENAME_BYTES = 255
|
||||||
|
|
||||||
var replacement = ''
|
const replacement = ''
|
||||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
const illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
const controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||||
var reservedRe = /^\.+$/
|
const reservedRe = /^\.+$/
|
||||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||||
var windowsTrailingRe = /[\. ]+$/
|
const windowsTrailingRe = /[\. ]+$/
|
||||||
var lineBreaks = /[\n\r]/g
|
const lineBreaks = /[\n\r]/g
|
||||||
|
|
||||||
var sanitized = input
|
sanitized = filename
|
||||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||||
.replace(illegalRe, replacement)
|
.replace(illegalRe, replacement)
|
||||||
.replace(controlRe, replacement)
|
.replace(controlRe, replacement)
|
||||||
@@ -55,13 +56,25 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
|||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
|
||||||
|
// Check if basename is too many bytes
|
||||||
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
|
const basename = Path.basename(sanitized, ext)
|
||||||
|
const extByteLength = Buffer.byteLength(ext, 'utf16le')
|
||||||
|
const basenameByteLength = Buffer.byteLength(basename, 'utf16le')
|
||||||
|
if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) {
|
||||||
|
const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength
|
||||||
|
let totalBytes = 0
|
||||||
|
let trimmedBasename = ''
|
||||||
|
|
||||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
// Add chars until max bytes is reached
|
||||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
for (const char of basename) {
|
||||||
var ext = Path.extname(sanitized)
|
totalBytes += Buffer.byteLength(char, 'utf16le')
|
||||||
var basename = Path.basename(sanitized, ext)
|
if (totalBytes > MaxBytesForBasename) break
|
||||||
basename = basename.slice(0, basename.length - lenToRemove)
|
else trimmedBasename += char
|
||||||
sanitized = basename + ext
|
}
|
||||||
|
|
||||||
|
trimmedBasename = trimmedBasename.trim()
|
||||||
|
sanitized = trimmedBasename + ext
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
@@ -94,13 +107,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
|
|
||||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(str).then(() => {
|
navigator.clipboard.writeText(str).then(() => {
|
||||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||||
resolve(true)
|
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('Clipboard copy failed', str, err)
|
console.error('Clipboard copy failed', str, err)
|
||||||
resolve(false)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
@@ -146,6 +157,7 @@ export {
|
|||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
app.$decode = decode
|
app.$decode = decode
|
||||||
app.$encode = encode
|
app.$encode = encode
|
||||||
|
inject('eventBus', new Vue())
|
||||||
inject('isDev', process.env.NODE_ENV !== 'production')
|
inject('isDev', process.env.NODE_ENV !== 'production')
|
||||||
|
|
||||||
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
store.commit('setRouterBasePath', app.$config.routerBasePath)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user