From 5f69339a27dabb8540c14d75f96667849fcd3cac Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 16 Sep 2021 08:37:09 -0500 Subject: [PATCH] Add batch read/not read update, Update tooltip positions --- client/components/app/Appbar.vue | 44 ++++++++++++++++++++++++-- client/components/ui/IconBtn.vue | 16 ++++++++-- client/components/ui/InputDropdown.vue | 6 ++-- client/components/ui/ReadIconBtn.vue | 26 --------------- client/components/ui/Tooltip.vue | 42 ++++++++++++++++++------ client/package.json | 2 +- client/pages/audiobook/_id/index.vue | 2 +- client/plugins/init.client.js | 25 +++++++++++---- package.json | 2 +- server/ApiController.js | 22 +++++++++++++ 10 files changed, 134 insertions(+), 53 deletions(-) diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index 5a12c88c..cdf60b4b 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -39,10 +39,15 @@ {{ isAllSelected ? 'Select None' : 'Select All' }}
+ + + - delete + + close
@@ -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 diff --git a/client/components/ui/IconBtn.vue b/client/components/ui/IconBtn.vue index 9a0f277a..0b4fee25 100644 --- a/client/components/ui/IconBtn.vue +++ b/client/components/ui/IconBtn.vue @@ -1,5 +1,5 @@ @@ -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) { diff --git a/client/components/ui/InputDropdown.vue b/client/components/ui/InputDropdown.vue index 1a90946d..ee66cfbe 100644 --- a/client/components/ui/InputDropdown.vue +++ b/client/components/ui/InputDropdown.vue @@ -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() } }, diff --git a/client/components/ui/ReadIconBtn.vue b/client/components/ui/ReadIconBtn.vue index 40fd1325..d8141c33 100644 --- a/client/components/ui/ReadIconBtn.vue +++ b/client/components/ui/ReadIconBtn.vue @@ -7,32 +7,6 @@ - diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue index 48bdcfb7..a3372826 100644 --- a/client/components/ui/Tooltip.vue +++ b/client/components/ui/Tooltip.vue @@ -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) { diff --git a/client/package.json b/client/package.json index 008af1d8..f559c257 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.1.9", + "version": "1.1.10", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/client/pages/audiobook/_id/index.vue b/client/pages/audiobook/_id/index.vue index bcf2187f..7597236f 100644 --- a/client/pages/audiobook/_id/index.vue +++ b/client/pages/audiobook/_id/index.vue @@ -69,7 +69,7 @@ Invalid Parts ({{ invalidParts.length }})

-

{{ part.filename }}: {{ part.error }}

+

{{ part.filename }}: {{ part.error }}

diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index fec1527a..a75fb641 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -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 = []) { diff --git a/package.json b/package.json index 80a763f0..bca428c6 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/server/ApiController.js b/server/ApiController.js index e46f2e07..423f7ec3 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -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) }