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' }}
+
+
+
- edit
+
+
- 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)
}