Add batch read/not read update, Update tooltip positions

This commit is contained in:
Mark Cooper 2021-09-16 08:37:09 -05:00
parent 7859d7a502
commit b2ab5730f5
10 changed files with 134 additions and 53 deletions

View File

@ -39,10 +39,15 @@
<ui-btn small class="text-sm mx-2" @click="toggleSelectAll">{{ isAllSelected ? 'Select None' : 'Select All' }}</ui-btn>
<div class="flex-grow" />
<ui-tooltip v-if="userCanUpdate" :text="`Mark as ${selectedIsRead ? 'Not Read' : 'Read'}`" direction="bottom">
<ui-read-icon-btn :is-read="selectedIsRead" @click="toggleBatchRead" class="mx-1.5" />
</ui-tooltip>
<template v-if="userCanUpdate">
<ui-btn v-show="!processingBatchDelete" color="warning" small class="mx-2" @click="batchEditClick"><span class="material-icons text-gray-200 pt-1">edit</span></ui-btn>
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
<!-- <ui-btn v-show="!processingBatchDelete" color="warning" small class="mx-2 w-10 h-10" :padding-y="0" :padding-x="0" @click="batchEditClick"><span class="material-icons text-gray-200 text-base">edit</span></ui-btn> -->
</template>
<ui-btn v-if="userCanDelete" color="error" small class="mx-2" :loading="processingBatchDelete" @click="batchDeleteClick"><span class="material-icons text-gray-200 pt-1">delete</span></ui-btn>
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
<!-- <ui-btn v-if="userCanDelete" color="error" small class="mx-2" :loading="processingBatchDelete" @click="batchDeleteClick"><span class="material-icons text-gray-200 pt-1">delete</span></ui-btn> -->
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
</div>
</div>
@ -78,6 +83,9 @@ export default {
isAllSelected() {
return this.audiobooksShowing.length === this.selectedAudiobooks.length
},
userAudiobooks() {
return this.$store.state.user.user.audiobooks || {}
},
audiobooksShowing() {
return this.$store.getters['audiobooks/getFiltered']()
},
@ -86,6 +94,13 @@ export default {
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
selectedIsRead() {
// Find an audiobook that is not read, if none then all audiobooks read
return !this.selectedAudiobooks.find((ab) => {
var userAb = this.userAudiobooks[ab]
return !userAb || !userAb.isRead
})
}
},
methods: {
@ -108,8 +123,31 @@ export default {
this.$store.commit('setSelectedAudiobooks', audiobookIds)
}
},
toggleBatchRead() {
var newIsRead = !this.selectedIsRead
var updateProgressPayloads = this.selectedAudiobooks.map((ab) => {
return {
audiobookId: ab,
isRead: newIsRead
}
})
this.$axios
.patch(`/api/user/audiobooks`, updateProgressPayloads)
.then(() => {
this.$toast.success('Batch update success!')
this.$store.commit('setProcessingBatch', false)
this.$store.commit('setSelectedAudiobooks', [])
})
.catch((error) => {
this.$toast.error('Batch update failed')
console.error('Failed to batch update read/not read', error)
this.$store.commit('setProcessingBatch', false)
})
},
batchDeleteClick() {
if (confirm(`Are you sure you want to delete these ${this.numAudiobooksSelected} audiobook(s)?`)) {
var audiobookText = this.numAudiobooksSelected > 1 ? `these ${this.numAudiobooksSelected} audiobooks` : 'this audiobook'
var confirmMsg = `Are you sure you want to remove ${audiobookText}?\n\n*Does not delete your files, only removes the audiobooks from AudioBookshelf`
if (confirm(confirmMsg)) {
this.processingBatchDelete = true
this.$store.commit('setProcessingBatch', true)
this.$axios

View File

@ -1,5 +1,5 @@
<template>
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
<button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :class="className" @click="clickBtn">
<span class="material-icons icon-text">{{ icon }}</span>
</button>
</template>
@ -8,12 +8,22 @@
export default {
props: {
icon: String,
disabled: Boolean
disabled: Boolean,
bgColor: {
type: String,
default: 'primary'
}
},
data() {
return {}
},
computed: {},
computed: {
className() {
var classes = []
classes.push(`bg-${this.bgColor}`)
return classes.join(' ')
}
},
methods: {
clickBtn(e) {
if (this.disabled) {

View File

@ -96,7 +96,7 @@ export default {
}
this.isFocused = false
if (this.input !== this.textInput) {
var val = this.$cleanString(this.textInput) || null
var val = this.textInput ? this.textInput.trim() : null
this.input = val
if (val && !this.items.includes(val)) {
this.$emit('newItem', val)
@ -105,7 +105,7 @@ export default {
}, 50)
},
submitForm() {
var val = this.$cleanString(this.textInput) || null
var val = this.textInput ? this.textInput.trim() : null
this.input = val
if (val && !this.items.includes(val)) {
this.$emit('newItem', val)
@ -116,7 +116,7 @@ export default {
var newValue = this.input === item ? null : item
this.textInput = null
this.currentSearch = null
this.input = this.$cleanString(newValue) || null
this.input = this.textInput ? this.textInput.trim() : null
if (this.$refs.input) this.$refs.input.blur()
}
},

View File

@ -7,32 +7,6 @@
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<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-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
<!-- <svg v-if="!isRead" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 482.204 482.204" xml:space="preserve" fill="currentColor">
<path
d="M83.127,344.477c54.602,1.063,101.919,9.228,136.837,23.613c0.596,0.244,1.227,0.366,1.852,0.366
c0.95,0,1.895-0.279,2.706-0.822c1.349-0.902,2.158-2.418,2.158-4.041l0.019-261.017c0-1.992-1.215-3.783-3.066-4.519
L85.019,42.899c-1.496-0.596-3.193-0.411-4.527,0.494c-1.334,0.906-2.133,2.413-2.133,4.025v292.197
C78.359,342.264,80.479,344.425,83.127,344.477z"
/>
<path
d="M480.244,89.256c-1.231-0.917-2.824-1.198-4.297-0.759l-49.025,14.657
c-2.06,0.616-3.471,2.51-3.471,4.659v252.151c0,0,0.218,3.978-3.97,3.978c-4.796,0-7.946,0-7.946,0
c-39.549,0-113.045,4.105-160.93,31.6l-9.504,5.442l-9.503-5.442c-47.886-27.494-121.381-31.6-160.93-31.6c0,0-8.099,0-10.142,0
c-1.891,0-1.775-2.272-1.775-2.271V107.813c0-2.149-1.411-4.043-3.47-4.659L6.256,88.497c-1.473-0.439-3.066-0.158-4.298,0.759
S0,91.619,0,93.155v305.069c0,1.372,0.581,2.681,1.597,3.604c1.017,0.921,2.375,1.372,3.741,1.236
c14.571-1.429,37.351-3.131,63.124-3.131c56.606,0,102.097,8.266,131.576,23.913c4.331,2.272,29.441,15.803,41.065,15.803
c11.624,0,36.733-13.53,41.063-15.803c29.48-15.647,74.971-23.913,131.577-23.913c25.771,0,48.553,1.702,63.123,3.131
c1.367,0.136,2.725-0.315,3.742-1.236c1.016-0.923,1.596-2.231,1.596-3.604V93.155C482.203,91.619,481.476,90.173,480.244,89.256z
"
/>
<path
d="M257.679,367.634c0.812,0.543,1.757,0.822,2.706,0.822c0.626,0,1.256-0.122,1.853-0.366
c34.917-14.386,82.235-22.551,136.837-23.613c2.648-0.052,4.769-2.213,4.769-4.861V47.418c0-1.613-0.799-3.12-2.133-4.025
c-1.334-0.904-3.031-1.09-4.528-0.494L258.569,98.057c-1.851,0.736-3.065,2.527-3.065,4.519l0.019,261.017
C255.521,365.216,256.331,366.732,257.679,367.634z"
/>
</svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M19 2H6c-1.206 0-3 .799-3 3v14c0 2.201 1.794 3 3 3h15v-2H6.012C5.55 19.988 5 19.806 5 19c0-.101.009-.191.024-.273.112-.576.584-.717.988-.727H21V4a2 2 0 0 0-2-2zm0 9-2-1-2 1V4h4v7z" /></svg> -->
</div>
</button>
</template>

View File

@ -31,30 +31,54 @@ export default {
updateText() {
if (this.tooltip) {
this.tooltip.innerHTML = this.text
this.setTooltipPosition(this.tooltip)
}
},
getTextWidth() {
var styles = {
'font-size': '0.75rem'
}
var size = this.$calculateTextSize(this.text, styles)
console.log('Text Size', size.width, size.height)
return size.width
},
createTooltip() {
if (!this.$refs.box) return
var tooltip = document.createElement('div')
tooltip.className = 'absolute px-2 bg-black bg-opacity-90 py-1 text-white pointer-events-none text-xs rounded shadow-lg'
tooltip.style.zIndex = 100
tooltip.innerHTML = this.text
this.setTooltipPosition(tooltip)
this.tooltip = tooltip
},
setTooltipPosition(tooltip) {
var boxChow = this.$refs.box.getBoundingClientRect()
var shouldMount = !tooltip.isConnected
// Calculate size of tooltip
if (shouldMount) document.body.appendChild(tooltip)
var { width, height } = tooltip.getBoundingClientRect()
if (shouldMount) tooltip.remove()
var top = 0
var left = 0
if (this.direction === 'right') {
top = boxChow.top
top = boxChow.top - height / 2 + boxChow.height / 2
left = boxChow.left + boxChow.width + 4
} else if (this.direction === 'bottom') {
top = boxChow.top + boxChow.height + 4
left = boxChow.left
left = boxChow.left - width / 2 + boxChow.width / 2
} else if (this.direction === 'top') {
top = boxChow.top - 24
left = boxChow.left
top = boxChow.top - height - 4
left = boxChow.left - width / 2 + boxChow.width / 2
} else if (this.direction === 'left') {
top = boxChow.top - height / 2 + boxChow.height / 2
left = boxChow.left - width - 4
}
var tooltip = document.createElement('div')
tooltip.className = 'absolute px-2 bg-black bg-opacity-90 py-1 text-white pointer-events-none text-xs rounded shadow-lg'
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
tooltip.style.zIndex = 100
tooltip.innerHTML = this.text
this.tooltip = tooltip
},
showTooltip() {
if (!this.tooltip) {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.1.9",
"version": "1.1.10",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -69,7 +69,7 @@
Invalid Parts <span class="text-sm">({{ invalidParts.length }})</span>
</p>
<div>
<p v-for="part in invalidParts" :key="part" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
<p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
</div>
</div>

View File

@ -38,13 +38,26 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}`
}
Vue.prototype.$cleanString = (str) => {
if (!str) return ''
Vue.prototype.$calculateTextSize = (text, styles = {}) => {
const el = document.createElement('p')
// No longer necessary to replace accented chars, full utf-8 charset is supported
// replace accented characters: https://stackoverflow.com/a/49901740/7431543
// str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
return str.trim()
let attr = 'margin:0px;opacity:1;position:absolute;top:100px;left:100px;z-index:99;'
for (const key in styles) {
if (styles[key] && String(styles[key]).length > 0) {
attr += `${key}:${styles[key]};`
}
}
el.setAttribute('style', attr)
el.innerText = text
document.body.appendChild(el)
const boundingBox = el.getBoundingClientRect()
el.remove()
return {
height: boundingBox.height,
width: boundingBox.width
}
}
function isClickedOutsideEl(clickEvent, elToCheckOutside, ignoreSelectors = [], ignoreElems = []) {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.1.9",
"version": "1.1.10",
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
"main": "index.js",
"scripts": {

View File

@ -37,6 +37,8 @@ class ApiController {
this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))
this.router.patch('/user/audiobook/:id', this.updateUserAudiobookProgress.bind(this))
this.router.patch('/user/audiobooks', this.batchUpdateUserAudiobooksProgress.bind(this))
this.router.patch('/user/password', this.userChangePassword.bind(this))
this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
this.router.get('/users', this.getUsers.bind(this))
@ -271,6 +273,26 @@ class ApiController {
res.sendStatus(200)
}
async batchUpdateUserAudiobooksProgress(req, res) {
var abProgresses = req.body
if (!abProgresses || !abProgresses.length) {
return res.sendStatus(500)
}
var shouldUpdate = false
abProgresses.forEach((progress) => {
var wasUpdated = req.user.updateAudiobookProgress(progress.audiobookId, progress)
if (wasUpdated) shouldUpdate = true
})
if (shouldUpdate) {
await this.db.updateEntity('user', req.user)
this.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
}
res.sendStatus(200)
}
userChangePassword(req, res) {
this.auth.userChangePassword(req, res)
}