mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-03 01:24:37 +02:00
Merge branch 'master' into ios-downloads
This commit is contained in:
commit
7c5ee940d3
50 changed files with 840 additions and 361 deletions
|
@ -32,7 +32,8 @@ export default {
|
|||
onMediaPlayerChangedListener: null,
|
||||
sleepInterval: null,
|
||||
currentEndOfChapterTime: 0,
|
||||
serverLibraryItemId: null
|
||||
serverLibraryItemId: null,
|
||||
serverEpisodeId: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -173,15 +174,15 @@ export default {
|
|||
this.$toast.error(`Cannot cast locally downloaded media`)
|
||||
} else {
|
||||
// Change to server library item
|
||||
this.playServerLibraryItemAndCast(this.serverLibraryItemId)
|
||||
this.playServerLibraryItemAndCast(this.serverLibraryItemId, this.serverEpisodeId)
|
||||
}
|
||||
},
|
||||
playServerLibraryItemAndCast(libraryItemId) {
|
||||
playServerLibraryItemAndCast(libraryItemId, episodeId) {
|
||||
var playbackRate = 1
|
||||
if (this.$refs.audioPlayer) {
|
||||
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
||||
}
|
||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId: null, playWhenReady: false, playbackRate })
|
||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: false, playbackRate })
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
const errorMsg = data.error || 'Failed to play'
|
||||
|
@ -203,6 +204,7 @@ export default {
|
|||
// When playing local library item and can also play this item from the server
|
||||
// then store the server library item id so it can be used if a cast is made
|
||||
var serverLibraryItemId = payload.serverLibraryItemId || null
|
||||
var serverEpisodeId = payload.serverEpisodeId || null
|
||||
|
||||
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||
const { value } = await Dialog.confirm({
|
||||
|
@ -215,6 +217,7 @@ export default {
|
|||
}
|
||||
|
||||
this.serverLibraryItemId = null
|
||||
this.serverEpisodeId = null
|
||||
|
||||
var playbackRate = 1
|
||||
if (this.$refs.audioPlayer) {
|
||||
|
@ -234,6 +237,11 @@ export default {
|
|||
} else {
|
||||
this.serverLibraryItemId = serverLibraryItemId
|
||||
}
|
||||
if (episodeId && !episodeId.startsWith('local')) {
|
||||
this.serverEpisodeId = episodeId
|
||||
} else {
|
||||
this.serverEpisodeId = serverEpisodeId
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
|
@ -99,6 +99,11 @@ export default {
|
|||
text: 'Account',
|
||||
to: '/account'
|
||||
})
|
||||
items.push({
|
||||
icon: 'equalizer',
|
||||
text: 'User Stats',
|
||||
to: '/stats'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.$platform !== 'ios') {
|
||||
|
@ -162,4 +167,4 @@ export default {
|
|||
this.show = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -250,7 +250,7 @@ export default {
|
|||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||
},
|
||||
userProgress() {
|
||||
if (this.episodeProgress) return this.episodeProgress
|
||||
if (this.recentEpisode) return this.episodeProgress || null
|
||||
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
|
||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||
<div class="flex-grow" />
|
||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||
<template v-if="page === 'library'">
|
||||
<div class="relative flex items-center px-2">
|
||||
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||
|
|
|
@ -86,6 +86,11 @@ export default {
|
|||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Language',
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
|
@ -165,6 +170,9 @@ export default {
|
|||
narrators() {
|
||||
return this.filterData.narrators || []
|
||||
},
|
||||
languages() {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
|
|
219
components/stats/DailyListeningChart.vue
Normal file
219
components/stats/DailyListeningChart.vue
Normal file
|
@ -0,0 +1,219 @@
|
|||
<template>
|
||||
<div class="w-96 my-6 mx-auto">
|
||||
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
|
||||
<div class="relative w-96 h-72">
|
||||
<div class="absolute top-0 left-0">
|
||||
<template v-for="lbl in yAxisLabels">
|
||||
<div :key="lbl" :style="{ height: lineSpacing + 'px' }" class="flex items-center justify-end">
|
||||
<p class="text-xs font-semibold">{{ lbl }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-for="n in 7">
|
||||
<div :key="n" class="absolute pointer-events-none left-0 h-px bg-white bg-opacity-10" :style="{ top: n * lineSpacing - lineSpacing / 2 + 'px', width: '360px', marginLeft: '24px' }" />
|
||||
|
||||
<div :key="`dot-${n}`" class="absolute z-10" :style="{ left: points[n - 1].x + 'px', bottom: points[n - 1].y + 'px' }">
|
||||
<div class="h-2 w-2 bg-yellow-400 hover:bg-yellow-300 rounded-full transform duration-150 transition-transform hover:scale-125" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-for="(line, index) in pointLines">
|
||||
<div :key="`line-${index}`" class="absolute h-0.5 bg-yellow-400 origin-bottom-left pointer-events-none" :style="{ width: line.width + 'px', left: line.x + 'px', bottom: line.y + 'px', transform: `rotate(${line.angle}deg)` }" />
|
||||
</template>
|
||||
|
||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||
<template v-for="dayObj in last7Days">
|
||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between pt-12">
|
||||
<div>
|
||||
<p class="text-sm text-center">Week Listening</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
|
||||
<p class="text-sm text-center">minutes</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">Daily Average</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
|
||||
<p class="text-sm text-center">minutes</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">Best Day</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
|
||||
<p class="text-sm text-center">minutes</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-center">Days</p>
|
||||
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
|
||||
<p class="text-sm text-center">in a row</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
listeningStats: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// test: [111, 120, 4, 156, 273, 76, 12],
|
||||
chartHeight: 288,
|
||||
chartWidth: 384,
|
||||
chartContentWidth: 360,
|
||||
chartContentHeight: 268
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
yAxisLabels() {
|
||||
var lbls = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
lbls.push(i * this.yAxisFactor)
|
||||
}
|
||||
return lbls
|
||||
},
|
||||
chartContentMarginLeft() {
|
||||
return this.chartWidth - this.chartContentWidth
|
||||
},
|
||||
chartContentMarginBottom() {
|
||||
return this.chartHeight - this.chartContentHeight
|
||||
},
|
||||
lineSpacing() {
|
||||
return this.chartHeight / 7
|
||||
},
|
||||
daySpacing() {
|
||||
return this.chartContentWidth / 7
|
||||
},
|
||||
linePositions() {
|
||||
var poses = []
|
||||
for (let i = 7; i > 0; i--) {
|
||||
poses.push(i * this.lineSpacing)
|
||||
}
|
||||
poses.push(0)
|
||||
return poses
|
||||
},
|
||||
last7Days() {
|
||||
var days = []
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
var _date = this.$addDaysToToday(i * -1)
|
||||
days.push({
|
||||
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
||||
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||
})
|
||||
}
|
||||
return days
|
||||
},
|
||||
last7DaysOfListening() {
|
||||
var listeningDays = {}
|
||||
var _index = 0
|
||||
this.last7Days.forEach((dayObj) => {
|
||||
listeningDays[_index++] = {
|
||||
dayOfWeek: dayObj.dayOfWeek,
|
||||
// minutesListening: this.test[_index - 1]
|
||||
minutesListening: this.getMinutesListeningForDate(dayObj.date)
|
||||
}
|
||||
})
|
||||
return listeningDays
|
||||
},
|
||||
mostListenedDay() {
|
||||
var sorted = Object.values(this.last7DaysOfListening)
|
||||
.map((dl) => ({ ...dl }))
|
||||
.sort((a, b) => b.minutesListening - a.minutesListening)
|
||||
return sorted[0].minutesListening
|
||||
},
|
||||
yAxisFactor() {
|
||||
var factor = Math.ceil(this.mostListenedDay / 5)
|
||||
|
||||
if (factor > 25) {
|
||||
// Use nearest multiple of 5
|
||||
return Math.ceil(factor / 5) * 5
|
||||
}
|
||||
|
||||
return Math.max(1, factor)
|
||||
},
|
||||
points() {
|
||||
var data = []
|
||||
for (let i = 0; i < 7; i++) {
|
||||
var listeningObj = this.last7DaysOfListening[String(i)]
|
||||
var minutesListening = listeningObj.minutesListening || 0
|
||||
var yPercent = minutesListening / (this.yAxisFactor * 7)
|
||||
data.push({
|
||||
x: 4 + this.chartContentMarginLeft + (this.daySpacing + this.daySpacing / 14) * i,
|
||||
y: this.chartContentMarginBottom + this.chartHeight * yPercent - 2
|
||||
})
|
||||
}
|
||||
return data
|
||||
},
|
||||
pointLines() {
|
||||
var lines = []
|
||||
for (let i = 1; i < 7; i++) {
|
||||
var lastPoint = this.points[i - 1]
|
||||
var nextPoint = this.points[i]
|
||||
|
||||
var x1 = lastPoint.x
|
||||
var x2 = nextPoint.x
|
||||
var y1 = lastPoint.y
|
||||
var y2 = nextPoint.y
|
||||
|
||||
lines.push({
|
||||
x: x1 + 4,
|
||||
y: y1 + 2,
|
||||
angle: this.getAngleBetweenPoints(x1, y1, x2, y2),
|
||||
width: Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) - 2
|
||||
})
|
||||
}
|
||||
return lines
|
||||
},
|
||||
totalMinutesListeningThisWeek() {
|
||||
var _total = 0
|
||||
Object.values(this.last7DaysOfListening).forEach((listeningObj) => (_total += listeningObj.minutesListening))
|
||||
return _total
|
||||
},
|
||||
averageMinutesPerDay() {
|
||||
return Math.round(this.totalMinutesListeningThisWeek / 7)
|
||||
},
|
||||
daysInARow() {
|
||||
var count = 0
|
||||
while (true) {
|
||||
var _date = this.$addDaysToToday(count * -1)
|
||||
var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||
|
||||
if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) {
|
||||
return count
|
||||
}
|
||||
count++
|
||||
|
||||
if (count > 9999) {
|
||||
console.error('Overflow protection')
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
listeningStatsDays() {
|
||||
return this.listeningStats ? this.listeningStats.days || [] : []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAngleBetweenPoints(cx, cy, ex, ey) {
|
||||
var dy = ey - cy
|
||||
var dx = ex - cx
|
||||
var theta = Math.atan2(dy, dx)
|
||||
theta *= 180 / Math.PI // convert to degrees
|
||||
return theta * -1
|
||||
},
|
||||
getMinutesListeningForDate(date) {
|
||||
if (!this.listeningStats || !this.listeningStats.days) return 0
|
||||
return Math.round((this.listeningStats.days[date] || 0) / 60)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<div class="w-full bg-primary bg-opacity-40">
|
||||
<div class="w-full h-14 flex items-center px-4 bg-primary">
|
||||
<p>Collection List</p>
|
||||
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
|
||||
<p class="font-mono text-sm">{{ books.length }}</p>
|
||||
<p class="pr-4">Collection List</p>
|
||||
|
||||
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<p v-if="totalDuration">{{ totalDurationPretty }}</p>
|
||||
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
<template v-for="book in booksCopy">
|
||||
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" />
|
||||
|
@ -39,12 +41,12 @@ export default {
|
|||
totalDuration() {
|
||||
var _total = 0
|
||||
this.books.forEach((book) => {
|
||||
_total += book.duration
|
||||
_total += book.media.duration
|
||||
})
|
||||
return _total
|
||||
},
|
||||
totalDurationPretty() {
|
||||
return this.$elapsedPretty(this.totalDuration)
|
||||
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -211,7 +211,9 @@ export default {
|
|||
|
||||
this.$eventBus.$emit('play-item', {
|
||||
libraryItemId: this.localLibraryItemId,
|
||||
episodeId: this.localEpisode.id
|
||||
episodeId: this.localEpisode.id,
|
||||
serverLibraryItemId: this.libraryItemId,
|
||||
serverEpisodeId: this.episode.id
|
||||
})
|
||||
} else {
|
||||
this.$eventBus.$emit('play-item', {
|
||||
|
|
|
@ -46,7 +46,7 @@ export default {
|
|||
episodesCopy: [],
|
||||
showFiltersModal: false,
|
||||
sortKey: 'publishedAt',
|
||||
sortDesc: false,
|
||||
sortDesc: true,
|
||||
filterKey: 'incomplete',
|
||||
episodeSortItems: [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue