mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-26 19:21:39 +00:00
Merge e9389d7ea9 into 30cf144090
This commit is contained in:
commit
6738ee66db
9 changed files with 184 additions and 6 deletions
|
|
@ -71,6 +71,14 @@
|
|||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isGlobalSharing" :to="`/social`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="isSocialPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<span class="material-icons-outlined">connect_without_contact</span>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Social</p>
|
||||
|
||||
<div v-show="isSocialPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||
<span class="material-icons text-2xl">warning</span>
|
||||
|
||||
|
|
@ -143,6 +151,9 @@ export default {
|
|||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
},
|
||||
isSocialPage() {
|
||||
return this.$route.name === 'social'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
|
|
@ -152,6 +163,9 @@ export default {
|
|||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||
},
|
||||
isGlobalSharing() {
|
||||
return this.$store.getters['getServerSetting']('sharedListeningStats')
|
||||
},
|
||||
showingIssues() {
|
||||
if (!this.$route.query) return false
|
||||
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
||||
|
|
@ -182,4 +196,4 @@ export default {
|
|||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,18 @@
|
|||
|
||||
<div class="w-full h-px bg-white/10 my-4" />
|
||||
|
||||
<div v-if="isGlobalSharing" class="flex items-center py-1">
|
||||
<ui-toggle-switch v-model="isUserSharing" @input="changeSharingSetting" />
|
||||
<ui-tooltip :text="$strings.ShareStatsHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.ShareStats }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="isGlobalSharing" class="w-full h-px bg-primary my-4" />
|
||||
|
||||
<p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
|
||||
<form v-if="!isGuest" @submit.prevent="submitChangePassword">
|
||||
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" />
|
||||
|
|
@ -47,6 +59,7 @@ export default {
|
|||
newPassword: null,
|
||||
confirmPassword: null,
|
||||
changingPassword: false,
|
||||
isUserSharing: null,
|
||||
selectedLanguage: ''
|
||||
}
|
||||
},
|
||||
|
|
@ -68,9 +81,15 @@ export default {
|
|||
},
|
||||
isGuest() {
|
||||
return this.usertype === 'guest'
|
||||
},
|
||||
isGlobalSharing() {
|
||||
return this.$store.getters['getServerSetting']('sharedListeningStats')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.isUserSharing = this.$store.getters['user/getUserSetting']('shareListeningActivity')
|
||||
},
|
||||
updateLocalLanguage(lang) {
|
||||
this.$setLanguageCode(lang)
|
||||
},
|
||||
|
|
@ -119,10 +138,14 @@ export default {
|
|||
this.$toast.error('Api call failed')
|
||||
this.changingPassword = false
|
||||
})
|
||||
},
|
||||
changeSharingSetting() {
|
||||
this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: this.isUserSharing})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.selectedLanguage = this.$languageCodes.current
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@
|
|||
<p class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.sharedListeningStats" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sharedListeningStats', val)" />
|
||||
<ui-tooltip :text="$strings.EnableSharedStatsHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.EnableSharedStats}}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
|
||||
</div>
|
||||
|
|
@ -425,4 +435,4 @@ export default {
|
|||
this.initServerSettings()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
93
client/pages/social.vue
Normal file
93
client/pages/social.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div class="w-full h-full overflow-y-auto px-2 py-18 md:p-8">
|
||||
<div v-if="this.listeningStats" class="flex flex-col md:flex-row">
|
||||
<div class="w-200 my-6 mx-auto">
|
||||
<div class="grid grid-cols-2 content-between">
|
||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.Social }}</h1>
|
||||
<div class="flex py-1 justify-end">
|
||||
<ui-tooltip :text="$strings.ShareStatsHelp">
|
||||
<p class="pl-4">
|
||||
{{ $strings.ShareStats }}
|
||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
<ui-toggle-switch v-model="isUserSharing" @input="changeSharingSetting" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!users.length">No Other User Sessions</p>
|
||||
<template v-for="(user, num) in users">
|
||||
<div :key="user.username" class="w-full">
|
||||
<div class="flex items-center mb-1">
|
||||
<nuxt-link :to="`/item/${user.latest.progress.libraryItemId}`">
|
||||
<ui-tooltip direction="left" :text="Math.round(user.latest.progress.progress * 100) + '%'">
|
||||
<covers-book-cover class="absolute left-0 right-0" :library-item="user.latest.item" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</ui-tooltip>
|
||||
</nuxt-link>
|
||||
<p class="text-xs pl-2 text-white text-opacity-50">{{ "Last listened to " + user.latest.item.media.metadata.title + " " + $dateDistanceFromNow(user.latest.progress.lastUpdate) + " for " + $elapsedPrettyExtended(user.latest.progress.duration) }}</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-lg pl-2 font-book text-white">{{ user.username }}</p>
|
||||
<widgets-online-indicator class="mx-2" :value="!!usersOnline[user.id]" />
|
||||
</div>
|
||||
<div v-if="num+1 < users.length" class="flex border-t border-white border-opacity-10 my-3"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="text-xl text-center">There is no listening activity to display</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Cover from '../components/modals/item/tabs/Cover.vue'
|
||||
export default {
|
||||
components: { Cover },
|
||||
data() {
|
||||
return {
|
||||
listeningStats: null,
|
||||
isUserSharing: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
bookCoverWidth() {
|
||||
return 70
|
||||
},
|
||||
users() {
|
||||
if (!this.listeningStats) return []
|
||||
return this.listeningStats.sort(function(a, b) {return b.latest.progress.lastUpdate - a.latest.progress.lastUpdate})
|
||||
},
|
||||
usersOnline() {
|
||||
var usermap = {}
|
||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
||||
return usermap
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.getUserSharing()
|
||||
this.loadStats()
|
||||
},
|
||||
getUserSharing() {
|
||||
this.isUserSharing = this.$store.getters['user/getUserSetting']('shareListeningActivity')
|
||||
},
|
||||
async loadStats() {
|
||||
let listeningStats = await this.$axios.$get(`/api/social`).catch((err) => {
|
||||
console.error('Failed to load shared user listening sesions', err)
|
||||
return []
|
||||
})
|
||||
this.listeningStats = listeningStats
|
||||
},
|
||||
changeSharingSetting() {
|
||||
this.$store.dispatch('user/updateUserSettings', { shareListeningActivity: this.isUserSharing})
|
||||
this.loadStats()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -472,6 +472,11 @@
|
|||
"PlaceholderNewCollection": "New collection name",
|
||||
"PlaceholderNewFolderPath": "New folder path",
|
||||
"PlaceholderSearch": "Search..",
|
||||
"EnableSharedStatsHelp": "Allow users to share latest listening activity with other users on an opt-in basis",
|
||||
"EnableSharedStats": "Shared Listening Stats",
|
||||
"ShareStatsHelp": "Share your latest listening activity with other users in the social tab",
|
||||
"ShareStats": "Share Listening Activity",
|
||||
"Social": "Social",
|
||||
"ToastAccountUpdateFailed": "Failed to update account",
|
||||
"ToastAccountUpdateSuccess": "Account updated",
|
||||
"ToastAuthorImageRemoveFailed": "Failed to remove image",
|
||||
|
|
@ -538,4 +543,4 @@
|
|||
"WeekdayThursday": "Thursday",
|
||||
"WeekdayTuesday": "Tuesday",
|
||||
"WeekdayWednesday": "Wednesday"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,6 +242,34 @@ class MiscController {
|
|||
})
|
||||
res.json(tags)
|
||||
}
|
||||
// GET: api/social
|
||||
async getSocialStats(req, res) {
|
||||
if (!this.db.serverSettings.sharedListeningStats) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
var filteredUsers = this.db.users.filter(c => c.settings.shareListeningActivity == true)
|
||||
var userData = []
|
||||
for (let i = 0; i < filteredUsers.length; i++) {
|
||||
var user = filteredUsers[i]
|
||||
var listeningStats = await this.getUserListeningStatsHelpers(user.id)
|
||||
var latestItem = user.mediaProgress.sort(p => p.lastUpdate)
|
||||
var latestProgress = latestItem[latestItem.length - 1]
|
||||
if (latestProgress) {
|
||||
var latestItem = this.db.getLibraryItem(latestProgress.id)
|
||||
}
|
||||
var session = {
|
||||
username: user.username,
|
||||
id: user.id,
|
||||
lastSeen: user.lastSeen,
|
||||
latest: {'progress': latestProgress, 'item': latestItem} || null,
|
||||
minutesListened: listeningStats.totalTime || 0,
|
||||
}
|
||||
if (session.minutesListened && session.latest) {
|
||||
userData.push(session)
|
||||
}
|
||||
}
|
||||
res.json(userData)
|
||||
}
|
||||
|
||||
validateCronExpression(req, res) {
|
||||
const expression = req.body.expression
|
||||
|
|
@ -258,4 +286,4 @@ class MiscController {
|
|||
}
|
||||
}
|
||||
}
|
||||
module.exports = new MiscController()
|
||||
module.exports = new MiscController()
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class ServerSettings {
|
|||
|
||||
// Misc Flags
|
||||
this.chromecastEnabled = false
|
||||
this.sharedListeningStats = false
|
||||
this.enableEReader = false
|
||||
this.dateFormat = 'MM/dd/yyyy'
|
||||
this.language = 'en-us'
|
||||
|
|
@ -101,6 +102,7 @@ class ServerSettings {
|
|||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||
this.sortingPrefixes = settings.sortingPrefixes || ['the']
|
||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||
this.sharedListeningStats = !!settings.sharedListeningStats
|
||||
this.enableEReader = !!settings.enableEReader
|
||||
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
||||
this.language = settings.language || 'en-us'
|
||||
|
|
@ -153,6 +155,7 @@ class ServerSettings {
|
|||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
sortingPrefixes: [...this.sortingPrefixes],
|
||||
chromecastEnabled: this.chromecastEnabled,
|
||||
sharedListeningStats: this.sharedListeningStats,
|
||||
enableEReader: this.enableEReader,
|
||||
dateFormat: this.dateFormat,
|
||||
language: this.language,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ class User {
|
|||
filterBy: 'all',
|
||||
playbackRate: 1,
|
||||
bookshelfCoverSize: 120,
|
||||
collapseSeries: false
|
||||
collapseSeries: false,
|
||||
shareListeningActivity: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ class ApiRouter {
|
|||
this.router.get('/search/podcast', MiscController.findPodcasts.bind(this))
|
||||
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
||||
this.router.get('/search/chapters', MiscController.findChapters.bind(this))
|
||||
this.router.get('/social', MiscController.getSocialStats.bind(this))
|
||||
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
||||
this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue