Moving settings to be user specific, adding playbackRate setting, update playbackRate picker to go up to 3x

This commit is contained in:
Mark Cooper 2021-08-23 18:31:04 -05:00
parent 2548aba840
commit f83c5dd440
22 changed files with 247 additions and 103 deletions

View File

@ -27,7 +27,7 @@
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
<span class="material-icons text-3xl">forward_10</span>
</div>
<controls-playback-speed-control v-model="playbackRate" @change="updatePlaybackRate" />
<controls-playback-speed-control v-model="playbackRate" @change="playbackRateChanged" />
</template>
<template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
@ -89,7 +89,7 @@ export default {
},
computed: {
token() {
return this.$store.getters.getToken
return this.$store.getters['user/getToken']
},
totalDurationPretty() {
return this.$secondsToTimestamp(this.totalDuration)
@ -130,12 +130,22 @@ export default {
},
updatePlaybackRate(playbackRate) {
if (this.audioEl) {
console.log('UpdatePlaybackRate', playbackRate)
this.audioEl.playbackRate = playbackRate
try {
this.audioEl.playbackRate = playbackRate
this.audioEl.defaultPlaybackRate = playbackRate
} catch (error) {
console.error('Update playback rate failed', error)
}
} else {
console.error('No Audio El updatePlaybackRate')
}
},
playbackRateChanged(playbackRate) {
this.updatePlaybackRate(playbackRate)
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err)
})
},
mousemoveTrack(e) {
var offsetX = e.offsetX
var time = (offsetX / this.trackWidth) * this.totalDuration
@ -355,7 +365,8 @@ export default {
this.hlsInstance = new Hls(hlsOptions)
var audio = this.$refs.audio
audio.volume = this.volume
audio.playbackRate = this.playbackRate
audio.defaultPlaybackRate = this.playbackRate
this.hlsInstance.attachMedia(audio)
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
// console.log('[HLS] MEDIA ATTACHED')
@ -410,17 +421,27 @@ export default {
this.set(this.url, startTime, true)
},
init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
this.audioEl = this.$refs.audio
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
} else {
console.error('Track not loaded', this.$refs)
}
},
settingsUpdated(settings) {
if (settings.playbackRate && this.playbackRate !== settings.playbackRate) {
this.updatePlaybackRate(settings.playbackRate)
}
}
},
mounted() {
// this.$nextTick(this.init)
this.$store.commit('user/addSettingsListener', { id: 'audioplayer', meth: this.settingsUpdated })
this.init()
},
beforeDestroy() {
this.$store.commit('user/removeSettingsListener', 'audioplayer')
}
}
</script>

View File

@ -43,7 +43,7 @@ export default {
return this.$route.name !== 'index'
},
user() {
return this.$store.state.user
return this.$store.state.user.user
},
username() {
return this.user ? this.user.username : 'err'

View File

@ -32,13 +32,13 @@ export default {
},
computed: {
userAudiobooks() {
return this.$store.state.user ? this.$store.state.user.audiobooks || {} : {}
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
},
audiobooks() {
return this.$store.state.audiobooks.audiobooks
},
filterOrderKey() {
return this.$store.getters['settings/getFilterOrderKey']
return this.$store.getters['user/getFilterOrderKey']
}
},
methods: {
@ -100,7 +100,7 @@ export default {
},
mounted() {
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
this.$store.commit('settings/addListener', { id: 'bookshelf', meth: this.settingsUpdated })
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
this.$store.dispatch('audiobooks/load')
this.init()
@ -108,7 +108,7 @@ export default {
},
beforeDestroy() {
this.$store.commit('audiobooks/removeListener', 'bookshelf')
this.$store.commit('settings/removeListener', 'bookshelf')
this.$store.commit('user/removeSettingsListener', 'bookshelf')
window.removeEventListener('resize', this.resize)
}
}

View File

@ -14,7 +14,8 @@
export default {
data() {
return {
settings: {}
settings: {},
hasInit: false
}
},
computed: {
@ -30,15 +31,24 @@ export default {
this.saveSettings()
},
saveSettings() {
// Send to server
this.$store.commit('settings/setSettings', this.settings)
this.$store.commit('user/setSettings', this.settings) // Immediate update
this.$store.dispatch('user/updateUserSettings', this.settings)
},
init() {
this.settings = { ...this.$store.state.settings.settings }
this.settings = { ...this.$store.state.user.settings }
},
settingsUpdated(settings) {
for (const key in settings) {
this.settings[key] = settings[key]
}
}
},
mounted() {
this.init()
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
},
beforeDestroy() {
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
}
}
</script>

View File

@ -22,6 +22,7 @@
export default {
data() {
return {
audioPlayerReady: false,
lastServerUpdateSentSeconds: 0,
stream: null
}
@ -32,7 +33,7 @@ export default {
return 'Logo.png'
},
user() {
return this.$store.state.user
return this.$store.state.user.user
},
isLoading() {
if (!this.streamAudiobook) return false
@ -63,6 +64,7 @@ export default {
},
methods: {
audioPlayerMounted() {
this.audioPlayerReady = true
if (this.stream) {
console.log('[STREAM-CONTAINER] audioPlayerMounted w/ Stream', this.stream)
this.openStream()
@ -104,7 +106,7 @@ export default {
if (this.$refs.audioPlayer) {
console.log('[STREAM-CONTAINER] streamOpen', stream)
this.openStream()
} else {
} else if (this.audioPlayerReady) {
console.error('No Audio Ref')
}
},

View File

@ -8,12 +8,24 @@
<div class="arrow-down" />
</div>
<div class="w-full h-full no-scroll flex">
<template v-for="(rate, index) in rates">
<div :key="rate" class="flex items-center justify-center border-black-300 w-11 hover:bg-black hover:bg-opacity-10 cursor-pointer" :class="index < rates.length - 1 ? 'border-r' : ''" style="min-width: 44px; max-width: 44px" @click="set(rate)">
<p class="text-xs text-center font-mono">{{ rate.toFixed(1) }}<span class="text-sm"></span></p>
<div class="w-full h-full no-scroll flex px-7 relative overflow-hidden">
<div class="absolute left-0 top-0 h-full w-7 border-r border-black-300 bg-black-300 rounded-l-lg flex items-center justify-center cursor-pointer" :class="rateIndex === 0 ? 'bg-black-400 text-gray-400' : 'hover:bg-black-200'" @mousedown.prevent @mouseup.prevent @click="leftArrowClick">
<span class="material-icons" style="font-size: 1.2rem">chevron_left</span>
</div>
<div class="overflow-hidden relative" style="width: 220px">
<div class="flex items-center h-full absolute top-0 left-0 transition-transform duration-100" :style="{ transform: `translateX(${xPos}px)` }">
<template v-for="rate in rates">
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border-r" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
<div class="w-full h-full flex justify-center items-center">
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm"></span></p>
</div>
</div>
</template>
</div>
</template>
</div>
<div class="absolute top-0 right-0 h-full w-7 bg-black-300 rounded-r-lg flex items-center justify-center cursor-pointer" :class="rateIndex === rates.length - numVisible ? 'bg-black-400 text-gray-400' : 'hover:bg-black-200'" @mousedown.prevent @mouseup.prevent @click="rightArrowClick">
<span class="material-icons" style="font-size: 1.2rem">chevron_right</span>
</div>
</div>
</div>
</div>
@ -29,7 +41,9 @@ export default {
},
data() {
return {
showMenu: false
showMenu: false,
rateIndex: 1,
numVisible: 5
}
},
computed: {
@ -42,7 +56,10 @@ export default {
}
},
rates() {
return [0.5, 0.8, 1.0, 1.3, 1.5, 2.0]
return [0.25, 0.5, 0.8, 1, 1.3, 1.5, 2, 2.5, 3]
},
xPos() {
return -1 * this.rateIndex * 44
}
},
methods: {
@ -55,6 +72,12 @@ export default {
this.playbackRate = newPlaybackRate
if (hasChanged) this.$emit('change', newPlaybackRate)
this.showMenu = false
},
leftArrowClick() {
this.rateIndex = Math.max(0, this.rateIndex - 4)
},
rightArrowClick() {
this.rateIndex = Math.min(this.rates.length - this.numVisible, this.rateIndex + 4)
}
},
mounted() {}

View File

@ -92,7 +92,7 @@ export default {
return this.audiobook ? this.audiobook.book || {} : {}
},
userAudiobook() {
return this.$store.getters['getUserAudiobook'](this.audiobookId)
return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
},
userProgress() {
return this.userAudiobook ? this.userAudiobook.progress : 0

View File

@ -24,13 +24,13 @@ export default {
},
computed: {
user() {
return this.$store.state.user
return this.$store.state.user.user
}
},
methods: {
connect() {
console.log('[SOCKET] Connected')
var token = this.$store.getters.getToken
var token = this.$store.getters['user/getToken']
this.socket.emit('auth', token)
},
connectError() {},
@ -49,7 +49,8 @@ export default {
}
}
if (payload.user) {
this.$store.commit('setUser', payload.user)
this.$store.commit('user/setUser', payload.user)
this.$store.commit('user/setSettings', payload.user.settings)
}
},
streamOpen(stream) {
@ -92,8 +93,9 @@ export default {
this.$store.commit('setScanProgress', progress)
},
userUpdated(user) {
if (this.$store.state.user.id === user.id) {
this.$store.commit('setUser', user)
if (this.$store.state.user.user.id === user.id) {
this.$store.commit('user/setUser', user)
this.$store.commit('user/setSettings', user.settings)
}
},
initializeSocket() {
@ -139,7 +141,7 @@ export default {
}
},
beforeMount() {
if (!this.$store.state.user) {
if (!this.$store.state.user.user) {
this.$router.replace(`/login?redirect=${this.$route.path}`)
}
},

View File

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

View File

@ -43,7 +43,7 @@ export default {
},
computed: {
user() {
return this.$store.state.user || null
return this.$store.state.user.user || null
},
username() {
return this.user.username

View File

@ -66,7 +66,7 @@ export default {
draggable
},
async asyncData({ store, params, app, redirect, route }) {
if (!store.state.user) {
if (!store.state.user.user) {
return redirect(`/login?redirect=${route.path}`)
}
var audiobook = await app.$axios.$get(`/api/audiobook/${params.id}`).catch((error) => {

View File

@ -63,7 +63,7 @@
<script>
export default {
async asyncData({ store, params, app, redirect, route }) {
if (!store.state.user) {
if (!store.state.user.user) {
return redirect(`/login?redirect=${route.path}`)
}
var audiobook = await app.$axios.$get(`/api/audiobook/${params.id}`).catch((error) => {
@ -163,7 +163,7 @@ export default {
return this.book.description || 'No Description'
},
userAudiobooks() {
return this.$store.state.user ? this.$store.state.user.audiobooks || {} : {}
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
},
userAudiobook() {
return this.userAudiobooks[this.audiobookId] || null

View File

@ -49,7 +49,7 @@ export default {
},
computed: {
user() {
return this.$store.state.user
return this.$store.state.user.user
}
},
methods: {
@ -71,7 +71,7 @@ export default {
} else if (authRes.error) {
this.error = authRes.error
} else {
this.$store.commit('setUser', authRes.user)
this.$store.commit('user/setUser', authRes.user)
}
this.processing = false
},
@ -90,7 +90,7 @@ export default {
}
})
.then((res) => {
this.$store.commit('setUser', res.user)
this.$store.commit('user/setUser', res.user)
this.processing = false
})
.catch((error) => {

View File

@ -4,7 +4,7 @@ export default function ({ $axios, store }) {
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return
}
var bearerToken = store.state.user ? store.state.user.token : null
var bearerToken = store.state.user.user ? store.state.user.user.token : null
// console.log('Bearer token', bearerToken)
if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}`

View File

@ -13,7 +13,7 @@ export const state = () => ({
export const getters = {
getFiltered: (state, getters, rootState) => () => {
var filtered = state.audiobooks
var settings = rootState.settings.settings || {}
var settings = rootState.user.settings || {}
var filterBy = settings.filterBy || ''
var searchGroups = ['genres', 'tags', 'series']
@ -27,7 +27,7 @@ export const getters = {
return filtered
},
getFilteredAndSorted: (state, getters, rootState) => () => {
var settings = rootState.settings.settings
var settings = rootState.user.settings
var direction = settings.orderDesc ? 'desc' : 'asc'
var filtered = getters.getFiltered()

View File

@ -1,6 +1,5 @@
export const state = () => ({
user: null,
streamAudiobook: null,
showEditModal: false,
selectedAudiobook: null,
@ -10,26 +9,11 @@ export const state = () => ({
developerMode: false
})
export const getters = {
getToken: (state) => {
return state.user ? state.user.token : null
},
getUserAudiobook: (state) => (audiobookId) => {
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
}
}
export const getters = {}
export const actions = {
}
export const actions = {}
export const mutations = {
setUser(state, user) {
state.user = user
if (user.token) {
localStorage.setItem('token', user.token)
}
},
setStreamAudiobook(state, audiobook) {
state.playOnLoad = true
state.streamAudiobook = audiobook

View File

@ -1,39 +0,0 @@
export const state = () => ({
settings: {
orderBy: 'book.title',
orderDesc: false,
filterBy: 'all'
},
listeners: []
})
export const getters = {
getFilterOrderKey: (state) => {
return Object.values(state.settings).join('-')
}
}
export const actions = {
}
export const mutations = {
setSettings(state, settings) {
state.settings = {
...settings
}
state.listeners.forEach((listener) => {
listener.meth()
})
},
addListener(state, listener) {
var index = state.listeners.findIndex(l => l.id === listener.id)
if (index >= 0) state.listeners.splice(index, 1, listener)
else state.listeners.push(listener)
},
removeListener(state, listenerId) {
state.listeners = state.listeners.filter(l => l.id !== listenerId)
}
}

81
client/store/user.js Normal file
View File

@ -0,0 +1,81 @@
export const state = () => ({
user: null,
settings: {
orderBy: 'book.title',
orderDesc: false,
filterBy: 'all',
playbackRate: 1
},
settingsListeners: []
})
export const getters = {
getToken: (state) => {
return state.user ? state.user.token : null
},
getUserAudiobook: (state) => (audiobookId) => {
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
},
getUserSetting: (state) => (key) => {
return state.settings ? state.settings[key] || null : null
},
getFilterOrderKey: (state) => {
return Object.values(state.settings).join('-')
}
}
export const actions = {
updateUserSettings({ commit }, payload) {
var updatePayload = {
...payload
}
return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => {
if (result.success) {
commit('setSettings', result.settings)
console.log('Settings updated', result.settings)
return true
} else {
return false
}
}).catch((error) => {
console.error('Failed to update settings', error)
return false
})
}
}
export const mutations = {
setUser(state, user) {
state.user = user
if (user && user.token) {
localStorage.setItem('token', user.token)
} else if (user) {
localStorage.removeItem('token')
}
},
setSettings(state, settings) {
if (!settings) return
var hasChanges = false
for (const key in settings) {
if (state.settings[key] !== settings[key]) {
hasChanges = true
state.settings[key] = settings[key]
}
}
if (hasChanges) {
state.settingsListeners.forEach((listener) => {
listener.meth(state.settings)
})
}
},
addSettingsListener(state, listener) {
var index = state.settingsListeners.findIndex(l => l.id === listener.id)
if (index >= 0) state.settingsListeners.splice(index, 1, listener)
else state.settingsListeners.push(listener)
},
removeSettingsListener(state, listenerId) {
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
}
}

View File

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

View File

@ -1,5 +1,6 @@
const express = require('express')
const Logger = require('./Logger')
const { isObject } = require('./utils/index')
class ApiController {
constructor(db, scanner, auth, streamManager, rssFeeds, emitter) {
@ -32,6 +33,7 @@ class ApiController {
this.router.get('/users', this.getUsers.bind(this))
this.router.delete('/user/audiobook/:id', this.resetUserAudiobookProgress.bind(this))
this.router.patch('/user/password', this.userChangePassword.bind(this))
this.router.patch('/user/settings', this.userUpdateSettings.bind(this))
this.router.post('/authorize', this.authorize.bind(this))
@ -185,6 +187,21 @@ class ApiController {
res.json(feed)
}
async userUpdateSettings(req, res) {
var settingsUpdate = req.body
if (!settingsUpdate || !isObject(settingsUpdate)) {
return res.sendStatus(500)
}
var madeUpdates = req.user.updateSettings(settingsUpdate)
if (madeUpdates) {
await this.db.updateEntity('user', req.user)
}
return res.json({
success: true,
settings: req.user.settings
})
}
getGenres(req, res) {
res.json({
genres: this.db.getGenres()

View File

@ -8,12 +8,22 @@ class User {
this.token = null
this.createdAt = null
this.audiobooks = null
this.settings = {}
if (user) {
this.construct(user)
}
}
getDefaultUserSettings() {
return {
orderBy: 'book.title',
orderDesc: false,
filterBy: 'all',
playbackRate: 1
}
}
toJSON() {
return {
id: this.id,
@ -23,7 +33,8 @@ class User {
stream: this.stream,
token: this.token,
audiobooks: this.audiobooks,
createdAt: this.createdAt
createdAt: this.createdAt,
settings: this.settings
}
}
@ -35,7 +46,8 @@ class User {
stream: this.stream,
token: this.token,
audiobooks: this.audiobooks,
createdAt: this.createdAt
createdAt: this.createdAt,
settings: this.settings
}
}
@ -48,6 +60,7 @@ class User {
this.token = user.token
this.audiobooks = user.audiobooks || null
this.createdAt = user.createdAt
this.settings = user.settings || this.getDefaultUserSettings()
}
updateAudiobookProgress(stream) {
@ -64,6 +77,32 @@ class User {
this.audiobooks[stream.audiobookId].currentTime = stream.clientCurrentTime
}
// Returns Boolean If update was made
updateSettings(settings) {
if (!this.settings) {
this.settings = { ...settings }
return true
}
var madeUpdates = false
for (const key in this.settings) {
if (settings[key] !== undefined && this.settings[key] !== settings[key]) {
this.settings[key] = settings[key]
madeUpdates = true
}
}
// Check if new settings update has keys not currently in user settings
for (const key in settings) {
if (settings[key] !== undefined && this.settings[key] === undefined) {
this.settings[key] = settings[key]
madeUpdates = true
}
}
return madeUpdates
}
resetAudiobookProgress(audiobookId) {
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
return false

View File

@ -40,4 +40,8 @@ const cleanString = (str) => {
}
return cleaned
}
module.exports.cleanString = cleanString
module.exports.cleanString = cleanString
module.exports.isObject = (val) => {
return val !== null && typeof val === 'object'
}