Add: More menu on book card #173, Fix: Download worker ffmpeg logs

This commit is contained in:
advplyr 2021-11-09 20:30:44 -06:00
parent 7141f70aa5
commit 54570a3b27
10 changed files with 295 additions and 54 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="relative">
<div ref="wrapper" class="relative">
<!-- New Book Flag -->
<div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl z-20">
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
@ -8,13 +8,13 @@
<div class="absolute -bottom-4 left-0 triangle-right" />
</div>
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @click.stop>
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer">
<div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
<covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
<!-- Hidden SM and DOWN -->
<div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center">
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
@ -33,6 +33,11 @@
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<!-- More Icon -->
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
@ -43,7 +48,7 @@
<div
v-if="showSmallEBookIcon"
class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
@click.stop.prevent="clickReadEBook"
>
<!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
@ -64,6 +69,9 @@
</template>
<script>
import Vue from 'vue'
import MoreMenu from '@/components/widgets/MoreMenu'
export default {
props: {
audiobook: {
@ -86,7 +94,10 @@ export default {
},
data() {
return {
isHovering: false
isHovering: false,
isMoreMenuOpen: false,
isProcessingReadUpdate: false,
rescanning: false
}
},
computed: {
@ -220,6 +231,46 @@ export default {
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
userIsRoot() {
return this.$store.getters['user/getIsRoot']
},
moreMenuItems() {
var items = [
{
func: 'toggleRead',
text: `Mark as ${this.userIsRead ? 'Not Read' : 'Read'}`
}
]
if (this.userCanUpdate) {
if (this.hasTracks) {
items.push({
func: 'showEditModalTracks',
text: 'Tracks'
})
}
items.push({
func: 'showEditModalMatch',
text: 'Match'
})
}
if (this.userCanDownload) {
items.push({
func: 'showEditModalDownload',
text: 'Download'
})
}
if (this.userIsRoot) {
items.push({
func: 'rescan',
text: 'Re-Scan'
})
}
return items
}
},
methods: {
@ -248,6 +299,103 @@ export default {
},
clickReadEBook() {
this.$store.commit('showEReader', this.audiobook)
},
toggleRead() {
// More menu func
var updatePayload = {
isRead: !this.userIsRead
}
this.isProcessingReadUpdate = true
this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
})
},
audiobookScanComplete(result) {
this.rescanning = false
if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
} else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete audiobook was updated`)
} else if (result === 'UPTODATE') {
this.$toast.success(`Re-Scan complete audiobook was up to date`)
} else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete audiobook was removed`)
}
},
rescan() {
this.rescanning = true
this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete)
this.$root.socket.emit('scan_audiobook', this.audiobookId)
},
showEditModalTracks() {
// More menu func
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'tracks' })
},
showEditModalMatch() {
// More menu func
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'match' })
},
showEditModalDownload() {
// More menu func
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
},
createMoreMenu() {
if (!this.$refs.moreIcon) return
var ComponentClass = Vue.extend(MoreMenu)
var _this = this
var instance = new ComponentClass({
propsData: {
items: this.moreMenuItems
},
created() {
this.$on('action', (func) => {
if (_this[func]) _this[func]()
})
this.$on('close', () => {
_this.isMoreMenuOpen = false
})
}
})
instance.$mount()
var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
var el = instance.$el
var elHeight = this.moreMenuItems.length * 28 + 2
var elWidth = 130
var bottomOfIcon = wrapperBox.top + wrapperBox.height
var rightOfIcon = wrapperBox.left + wrapperBox.width
var elTop = bottomOfIcon
var elLeft = rightOfIcon
if (bottomOfIcon + elHeight > window.innerHeight - 100) {
elTop = wrapperBox.top - elHeight
elLeft = wrapperBox.left
}
if (rightOfIcon + elWidth > window.innerWidth - 100) {
elLeft = rightOfIcon - elWidth
}
el.style.top = elTop + 'px'
el.style.left = elLeft + 'px'
this.isMoreMenuOpen = true
document.body.appendChild(el)
},
clickShowMore() {
this.createMoreMenu()
}
}
}

View File

@ -132,7 +132,7 @@ export default {
if (tab.id === 'download' && this.isMissing) return false
if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true
if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true
if (tab.id === 'match' && this.showExperimentalFeatures) return true
if (tab.id === 'match' && this.userCanUpdate && this.showExperimentalFeatures) return true
return false
})
},

View File

@ -1,5 +1,5 @@
<template>
<div class="relative w-full" v-click-outside="clickOutside">
<div class="relative w-full" v-click-outside="clickOutsideObj">
<p class="text-sm font-semibold">{{ label }}</p>
<button type="button" :disabled="disabled" class="relative w-full border border-gray-500 rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" :class="small ? 'h-9' : 'h-10'" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
@ -41,6 +41,11 @@ export default {
},
data() {
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
},
showMenu: false
}
},
@ -65,7 +70,7 @@ export default {
if (this.disabled) return
this.showMenu = !this.showMenu
},
clickOutside() {
clickedOutside() {
this.showMenu = false
},
clickedOption(itemValue) {

View File

@ -1,5 +1,5 @@
<template>
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutside">
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutsideObj">
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
@ -26,6 +26,11 @@
export default {
data() {
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
},
showMenu: false,
disabled: false
}
@ -61,7 +66,7 @@ export default {
if (this.disabled) return
this.showMenu = !this.showMenu
},
clickOutside() {
clickedOutside() {
this.showMenu = false
},
selectLibrary(library) {

View File

@ -0,0 +1,50 @@
<template>
<div class="absolute w-32 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
<template v-for="(item, index) in items">
<div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click="clickAction(item.func)">
<p>{{ item.text }}</p>
</div>
</template>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
}
},
data() {
return {
clickOutsideObj: {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
}
}
},
computed: {},
methods: {
clickAction(func) {
this.$emit('action', func)
this.close()
},
clickedOutside(e) {
this.close()
},
close() {
this.$emit('close')
// destroy the vue listeners, etc
this.$destroy()
// remove the element from the DOM
this.$el.parentNode.removeChild(this.$el)
}
},
mounted() {},
beforeDestroy() {}
}
</script>

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.4.10",
"version": "1.6.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -13298,6 +13298,11 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"v-click-outside": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-3.1.2.tgz",
"integrity": "sha512-gMdRqfRE6m6XU6SiFi3dyBlFB2MWogiXpof8Aa3LQysrl9pzTndqp/iEaAphLoadaQUFnQ0ec6fLLaxr7LiY6A=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -22,6 +22,7 @@
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.7",
"nuxt-socket-io": "^1.1.18",
"v-click-outside": "^3.1.2",
"vue-pdf": "^4.3.0",
"vue-toastification": "^1.7.11",
"vuedraggable": "^2.24.3"
@ -30,4 +31,4 @@
"@nuxtjs/tailwindcss": "^4.2.1",
"postcss": "^8.3.6"
}
}
}

View File

@ -1,6 +1,9 @@
import Vue from 'vue'
import vClickOutside from 'v-click-outside'
import { formatDistance, format } from 'date-fns'
Vue.directive('click-outside', vClickOutside.directive)
Vue.prototype.$eventBus = new Vue()
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
@ -73,42 +76,41 @@ Vue.prototype.$calculateTextSize = (text, styles = {}) => {
}
}
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
const isDOMElement = (element) => {
return element instanceof Element || element instanceof HTMLDocument
}
// function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {
// const isDOMElement = (element) => {
// return element instanceof Element || element instanceof HTMLDocument
// }
const clickedEl = clickEvent.srcElement
const didClickOnIgnoredEl = ignoreElems.filter((el) => el).some((element) => element.contains(clickedEl) || element.isEqualNode(clickedEl))
const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map((selector) => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false
// const clickedEl = clickEvent.srcElement
// const didClickOnIgnoredEl = ignoreElems.filter((el) => el).some((element) => element.contains(clickedEl) || element.isEqualNode(clickedEl))
// const didClickOnIgnoredSelector = ignoreSelectors.length ? ignoreSelectors.map((selector) => clickedEl.closest(selector)).reduce((curr, accumulator) => curr && accumulator, true) : false
if (isDOMElement(elToCheckOutside) && !elToCheckOutside.contains(clickedEl) && !didClickOnIgnoredEl && !didClickOnIgnoredSelector) {
return true
}
// if (isDOMElement(elToCheckOutside) && !elToCheckOutside.contains(clickedEl) && !didClickOnIgnoredEl && !didClickOnIgnoredSelector) {
// return true
// }
// return false
// }
return false
}
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
let vm = vnode.context;
let callback = binding.value;
if (typeof callback !== 'function') {
console.error('Invalid callback', binding)
return
}
el['__click_outside__'] = (ev) => {
if (isClickedOutsideEl(ev, el)) {
callback.call(vm, ev)
}
}
document.addEventListener('click', el['__click_outside__'], false)
},
unbind: function (el, binding, vnode) {
document.removeEventListener('click', el['__click_outside__'], false)
delete el['__click_outside__']
}
})
// Vue.directive('click-outside', {
// bind: function (el, binding, vnode) {
// let vm = vnode.context;
// let callback = binding.value;
// if (typeof callback !== 'function') {
// console.error('Invalid callback', binding)
// return
// }
// el['__click_outside__'] = (ev) => {
// if (isClickedOutsideEl(ev, el)) {
// callback.call(vm, ev)
// }
// }
// document.addEventListener('click', el['__click_outside__'], false)
// },
// unbind: function (el, binding, vnode) {
// document.removeEventListener('click', el['__click_outside__'], false)
// delete el['__click_outside__']
// }
// })
Vue.prototype.$sanitizeFilename = (input, replacement = '') => {
if (typeof input !== 'string') {

View File

@ -285,6 +285,10 @@ class DownloadManager {
if (!download.isTimedOut) {
this.sendResult(download, message)
}
} else if (message.type === 'FFMPEG') {
if (Logger[message.level]) {
Logger[message.level](message.log)
}
}
} else {
Logger.error('Invalid worker message', message)

View File

@ -5,10 +5,12 @@ if (process.env.FFMPEG_PATH) {
}
const { parentPort, workerData } = require("worker_threads")
const Logger = require('../Logger')
Logger.info('[DownloadWorker] Starting Worker...')
parentPort.postMessage({
type: 'FFMPEG',
level: 'debug',
log: '[DownloadWorker] Starting Worker...'
})
const ffmpegCommand = Ffmpeg()
const startTime = Date.now()
@ -27,25 +29,45 @@ var isKilled = false
async function runFfmpeg() {
var success = await new Promise((resolve) => {
ffmpegCommand.on('start', (command) => {
Logger.info('[DownloadWorker] FFMPEG concat started with command: ' + command)
parentPort.postMessage({
type: 'FFMPEG',
level: 'info',
log: '[DownloadWorker] FFMPEG concat started with command: ' + command
})
})
ffmpegCommand.on('stderr', (stdErrline) => {
Logger.info(stdErrline)
parentPort.postMessage({
type: 'FFMPEG',
level: 'error',
log: '[DownloadWorker] Ffmpeg Stderr: ' + stdErrline
})
})
ffmpegCommand.on('error', (err, stdout, stderr) => {
if (err.message && err.message.includes('SIGKILL')) {
// This is an intentional SIGKILL
Logger.info('[DownloadWorker] User Killed singleAudio')
parentPort.postMessage({
type: 'FFMPEG',
level: 'info',
log: '[DownloadWorker] User Killed worker'
})
} else {
Logger.error('[DownloadWorker] Ffmpeg Err', err.message)
parentPort.postMessage({
type: 'FFMPEG',
level: 'error',
log: '[DownloadWorker] Ffmpeg Err: ' + err.message
})
}
resolve(false)
})
ffmpegCommand.on('end', (stdout, stderr) => {
Logger.info('[DownloadWorker] singleAudio ended')
parentPort.postMessage({
type: 'FFMPEG',
level: 'info',
log: '[DownloadWorker] worker ended'
})
resolve(true)
})
ffmpegCommand.run()
@ -62,7 +84,6 @@ async function runFfmpeg() {
parentPort.on('message', (message) => {
if (message === 'STOP') {
Logger.info('[DownloadWorker] Requested a hard stop')
isKilled = true
ffmpegCommand.kill()
}