mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-09 20:44:49 +02:00
Merge branch 'master' into iosChapterTrack
This commit is contained in:
commit
73d70dd480
37 changed files with 1060 additions and 48 deletions
|
@ -9,6 +9,7 @@ android {
|
||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':byteowls-capacitor-filesharer')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-browser')
|
implementation project(':capacitor-browser')
|
||||||
implementation project(':capacitor-clipboard')
|
implementation project(':capacitor-clipboard')
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"pkg": "@byteowls/capacitor-filesharer",
|
||||||
|
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
|
|
|
@ -116,6 +116,7 @@ class MainActivity : BridgeActivity() {
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
storageHelper.onSaveInstanceState(outState)
|
storageHelper.onSaveInstanceState(outState)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
outState.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':byteowls-capacitor-filesharer'
|
||||||
|
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
|
||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
<p class="whitespace-normal line-clamp-2" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
||||||
<span v-if="seriesSequence">#{{ seriesSequence }} </span>{{ displayTitle }}
|
<span v-if="seriesSequence">#{{ seriesSequence }} </span>{{ displayTitle }}
|
||||||
</p>
|
</p>
|
||||||
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" width="90%" height="100%">
|
<modals-modal v-model="show" width="90%" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
|
<div v-show="selected !== 'all'" class="absolute top-12 left-4 z-40">
|
||||||
<ui-btn class="text-lg border-yellow-400 border-opacity-40" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
|
<ui-btn class="text-lg border-yellow-400 border-opacity-40 h-10" :padding-y="0" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-fg/20 mt-8" style="max-height: 75%" @click.stop>
|
||||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in items">
|
||||||
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
||||||
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
|
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
|
||||||
|
|
||||||
<div class="absolute z-40 top-10 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
<div class="absolute z-40 top-11 right-4 h-10 w-10 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
||||||
<span class="material-icons text-4xl">close</span>
|
<span class="material-icons text-4xl">close</span>
|
||||||
</div>
|
</div>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
|
|
283
components/stats/YearInReview.vue
Normal file
283
components/stats/YearInReview.vue
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</div>
|
||||||
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { FileSharer } from '@byteowls/capacitor-filesharer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
variant: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
year: Number,
|
||||||
|
processing: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvas: null,
|
||||||
|
dataUrl: null,
|
||||||
|
yearStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
variant() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initCanvas() {
|
||||||
|
if (!this.yearStats) return
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 800
|
||||||
|
canvas.height = 800
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const createRoundedRect = (x, y, w, h) => {
|
||||||
|
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
|
||||||
|
grd1.addColorStop(0, '#44444455')
|
||||||
|
grd1.addColorStop(1, '#ffffff11')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.strokeStyle = '#C0C0C088'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(x, y, w, h, [20])
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
|
||||||
|
ctx.letterSpacing = letterSpacing
|
||||||
|
|
||||||
|
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
|
||||||
|
if (maxWidth) {
|
||||||
|
let txtWidth = ctx.measureText(text).width
|
||||||
|
while (txtWidth > maxWidth) {
|
||||||
|
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
|
||||||
|
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
|
||||||
|
else text = text.slice(0, -3) // First check remove last 3 chars
|
||||||
|
text += '...'
|
||||||
|
txtWidth = ctx.measureText(text).width
|
||||||
|
console.log(`Checking text "${text}" (width:${txtWidth})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addIcon = (icon, color, fontSize, x, y) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontSize} Material Icons Outlined`
|
||||||
|
ctx.fillText(icon, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bg color
|
||||||
|
ctx.fillStyle = '#232323'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Cover image tiles
|
||||||
|
const bookCovers = this.yearStats.finishedBooksWithCovers
|
||||||
|
bookCovers.push(...this.yearStats.booksWithCovers)
|
||||||
|
|
||||||
|
let finishedBookCoverImgs = {}
|
||||||
|
|
||||||
|
if (bookCovers.length) {
|
||||||
|
let index = 0
|
||||||
|
ctx.globalAlpha = 0.25
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(canvas.width / 2, canvas.height / 2)
|
||||||
|
ctx.rotate((-Math.PI / 180) * 25)
|
||||||
|
ctx.translate(-canvas.width / 2, -canvas.height / 2)
|
||||||
|
ctx.translate(-130, -120)
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
const coverIndex = index % bookCovers.length
|
||||||
|
let libraryItemId = bookCovers[coverIndex]
|
||||||
|
index++
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
let sw = img.width
|
||||||
|
if (img.width > img.height) {
|
||||||
|
sw = img.height
|
||||||
|
}
|
||||||
|
let sx = -(sw - img.width) / 2
|
||||||
|
let sy = -(sw - img.height) / 2
|
||||||
|
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
|
||||||
|
resolve()
|
||||||
|
if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) {
|
||||||
|
finishedBookCoverImgs[libraryItemId] = {
|
||||||
|
img,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
// Create gradient
|
||||||
|
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
|
||||||
|
grd1.addColorStop(0, '#000000aa')
|
||||||
|
grd1.addColorStop(1, '#cd9d49aa')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Top Abs icon
|
||||||
|
let tanColor = '#ffdb70'
|
||||||
|
ctx.fillStyle = tanColor
|
||||||
|
ctx.font = '42px absicons'
|
||||||
|
ctx.fillText('\ue900', 15, 36)
|
||||||
|
|
||||||
|
// Top text
|
||||||
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
|
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
|
// Top left box
|
||||||
|
createRoundedRect(50, 100, 340, 160)
|
||||||
|
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
|
||||||
|
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
|
||||||
|
const readIconPath = new Path2D()
|
||||||
|
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.fill(readIconPath)
|
||||||
|
|
||||||
|
// Box top right
|
||||||
|
createRoundedRect(410, 100, 340, 160)
|
||||||
|
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
|
||||||
|
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
|
||||||
|
addIcon('watch_later', 'white', '52px', 440, 180)
|
||||||
|
|
||||||
|
// Box bottom left
|
||||||
|
createRoundedRect(50, 280, 340, 160)
|
||||||
|
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
|
||||||
|
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
|
||||||
|
addIcon('headphones', 'white', '52px', 95, 360)
|
||||||
|
|
||||||
|
// Box bottom right
|
||||||
|
createRoundedRect(410, 280, 340, 160)
|
||||||
|
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
|
||||||
|
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
|
||||||
|
addIcon('local_library', 'white', '52px', 440, 360)
|
||||||
|
|
||||||
|
if (!this.variant) {
|
||||||
|
// Text stats
|
||||||
|
const topNarrator = this.yearStats.mostListenedNarrator
|
||||||
|
if (topNarrator) {
|
||||||
|
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
|
||||||
|
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
|
||||||
|
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
|
||||||
|
}
|
||||||
|
|
||||||
|
const topGenre = this.yearStats.topGenres[0]
|
||||||
|
if (topGenre) {
|
||||||
|
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
|
||||||
|
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
|
||||||
|
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
|
||||||
|
}
|
||||||
|
|
||||||
|
const topAuthor = this.yearStats.topAuthors[0]
|
||||||
|
if (topAuthor) {
|
||||||
|
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
|
||||||
|
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
|
||||||
|
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yearStats.mostListenedMonth?.time) {
|
||||||
|
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
|
||||||
|
const monthName = this.$formatJsDate(jsdate, 'LLLL')
|
||||||
|
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
|
||||||
|
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
|
||||||
|
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
|
||||||
|
}
|
||||||
|
} else if (this.variant === 1) {
|
||||||
|
// Bottom images
|
||||||
|
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
|
||||||
|
if (finishedBookCoverImgs.length > 0) {
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
|
||||||
|
let imgToAdd = finishedBookCoverImgs[i]
|
||||||
|
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.variant === 2) {
|
||||||
|
// Text stats
|
||||||
|
if (this.yearStats.topAuthors.length) {
|
||||||
|
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
|
||||||
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yearStats.topGenres.length) {
|
||||||
|
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
|
||||||
|
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||||
|
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas = canvas
|
||||||
|
this.dataUrl = canvas.toDataURL('png')
|
||||||
|
},
|
||||||
|
refresh() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
share() {
|
||||||
|
const base64Data = this.dataUrl.split(';base64,').pop()
|
||||||
|
FileSharer.share({
|
||||||
|
filename: `audiobookshelf_my_${this.year}.png`,
|
||||||
|
contentType: 'image/png',
|
||||||
|
base64Data
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message !== 'USER_CANCELLED') {
|
||||||
|
console.error('Failed to share', error.message)
|
||||||
|
this.$toast.error('Failed to share: ' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
this.$nativeHttp
|
||||||
|
.get(`/api/me/stats/year/${this.year}`)
|
||||||
|
.then((data) => {
|
||||||
|
this.yearStats = data || []
|
||||||
|
return this.initCanvas()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to load year stats')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
134
components/stats/YearInReviewBanner.vue
Normal file
134
components/stats/YearInReviewBanner.vue
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-4">
|
||||||
|
<!-- hack to get icon fonts loaded on init -->
|
||||||
|
<div class="h-0 w-0 overflow-hidden opacity-0">
|
||||||
|
<span class="material-icons-outlined">close</span>
|
||||||
|
<span class="abs-icons icon-audiobookshelf" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
|
||||||
|
<div class="hidden md:block flex-grow" />
|
||||||
|
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : `See ${yearInReviewYear} Year in Review` }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- your year in review -->
|
||||||
|
<div v-if="showYearInReview">
|
||||||
|
<div class="w-full h-px bg-slate-200/10 my-4" />
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||||
|
<!-- previous button -->
|
||||||
|
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||||
|
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
|
<span class="hidden sm:inline-block pr-2">Previous</span>
|
||||||
|
</ui-btn>
|
||||||
|
<!-- share button -->
|
||||||
|
<ui-btn small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p>
|
||||||
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<!-- refresh button -->
|
||||||
|
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
|
||||||
|
<span class="hidden sm:inline-block">Refresh</span>
|
||||||
|
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
||||||
|
</ui-btn>
|
||||||
|
<!-- next button -->
|
||||||
|
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||||
|
<span class="hidden sm:inline-block pl-2">Next</span>
|
||||||
|
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
|
||||||
|
|
||||||
|
<!-- your year in review short -->
|
||||||
|
<div class="w-full max-w-[800px] mx-auto my-4">
|
||||||
|
<!-- share button -->
|
||||||
|
<ui-btn small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn>
|
||||||
|
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- your server in review -->
|
||||||
|
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
|
||||||
|
<div class="flex items-center justify-center mb-2">
|
||||||
|
<!-- previous button -->
|
||||||
|
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||||
|
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
|
<span class="hidden sm:inline-block pr-2">Previous</span>
|
||||||
|
</ui-btn>
|
||||||
|
<!-- share button -->
|
||||||
|
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn>
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p>
|
||||||
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<!-- refresh button -->
|
||||||
|
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
|
||||||
|
<span class="hidden sm:inline-block">Refresh</span>
|
||||||
|
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
||||||
|
</ui-btn>
|
||||||
|
<!-- next button -->
|
||||||
|
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||||
|
<span class="hidden sm:inline-block pl-2">Next</span>
|
||||||
|
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showYearInReview: false,
|
||||||
|
yearInReviewYear: 0,
|
||||||
|
yearInReviewVariant: 0,
|
||||||
|
yearInReviewServerVariant: 0,
|
||||||
|
processingYearInReview: false,
|
||||||
|
processingYearInReviewShort: false,
|
||||||
|
processingYearInReviewServer: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
shareYearInReviewServer() {
|
||||||
|
this.$refs.yearInReviewServer.share()
|
||||||
|
},
|
||||||
|
shareYearInReview() {
|
||||||
|
this.$refs.yearInReview.share()
|
||||||
|
},
|
||||||
|
shareYearInReviewShort() {
|
||||||
|
this.$refs.yearInReviewShort.share()
|
||||||
|
},
|
||||||
|
refreshYearInReviewServer() {
|
||||||
|
this.$refs.yearInReviewServer.refresh()
|
||||||
|
},
|
||||||
|
refreshYearInReview() {
|
||||||
|
this.$refs.yearInReview.refresh()
|
||||||
|
this.$refs.yearInReviewShort.refresh()
|
||||||
|
},
|
||||||
|
clickShowYearInReview() {
|
||||||
|
this.showYearInReview = !this.showYearInReview
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.yearInReviewYear = new Date().getFullYear()
|
||||||
|
// When not December show previous year
|
||||||
|
if (new Date().getMonth() < 11) {
|
||||||
|
this.yearInReviewYear--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
262
components/stats/YearInReviewServer.vue
Normal file
262
components/stats/YearInReviewServer.vue
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</div>
|
||||||
|
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { FileSharer } from '@byteowls/capacitor-filesharer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
variant: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
processing: Boolean,
|
||||||
|
year: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvas: null,
|
||||||
|
dataUrl: null,
|
||||||
|
yearStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
variant() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initCanvas() {
|
||||||
|
if (!this.yearStats) return
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 800
|
||||||
|
canvas.height = 800
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const createRoundedRect = (x, y, w, h) => {
|
||||||
|
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
|
||||||
|
grd1.addColorStop(0, '#44444455')
|
||||||
|
grd1.addColorStop(1, '#ffffff11')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.strokeStyle = '#C0C0C088'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(x, y, w, h, [20])
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
|
||||||
|
ctx.letterSpacing = letterSpacing
|
||||||
|
|
||||||
|
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
|
||||||
|
if (maxWidth) {
|
||||||
|
let txtWidth = ctx.measureText(text).width
|
||||||
|
while (txtWidth > maxWidth) {
|
||||||
|
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
|
||||||
|
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
|
||||||
|
else text = text.slice(0, -3) // First check remove last 3 chars
|
||||||
|
text += '...'
|
||||||
|
txtWidth = ctx.measureText(text).width
|
||||||
|
console.log(`Checking text "${text}" (width:${txtWidth})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bg color
|
||||||
|
ctx.fillStyle = '#232323'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Cover image tiles
|
||||||
|
let imgsToAdd = {}
|
||||||
|
|
||||||
|
if (this.yearStats.booksAddedWithCovers.length) {
|
||||||
|
let index = 0
|
||||||
|
ctx.globalAlpha = 0.25
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(canvas.width / 2, canvas.height / 2)
|
||||||
|
ctx.rotate((-Math.PI / 180) * 25)
|
||||||
|
ctx.translate(-canvas.width / 2, -canvas.height / 2)
|
||||||
|
ctx.translate(-130, -120)
|
||||||
|
for (let x = 0; x < 5; x++) {
|
||||||
|
for (let y = 0; y < 5; y++) {
|
||||||
|
const coverIndex = index % this.yearStats.booksAddedWithCovers.length
|
||||||
|
let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex]
|
||||||
|
index++
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
let sw = img.width
|
||||||
|
if (img.width > img.height) {
|
||||||
|
sw = img.height
|
||||||
|
}
|
||||||
|
let sx = -(sw - img.width) / 2
|
||||||
|
let sy = -(sw - img.height) / 2
|
||||||
|
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
|
||||||
|
if (!imgsToAdd[libraryItemId]) {
|
||||||
|
imgsToAdd[libraryItemId] = {
|
||||||
|
img,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
// Create gradient
|
||||||
|
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
|
||||||
|
grd1.addColorStop(0, '#000000aa')
|
||||||
|
grd1.addColorStop(1, '#cd9d49aa')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Top Abs icon
|
||||||
|
let tanColor = '#ffdb70'
|
||||||
|
ctx.fillStyle = tanColor
|
||||||
|
ctx.font = '42px absicons'
|
||||||
|
ctx.fillText('\ue900', 15, 36)
|
||||||
|
|
||||||
|
// Top text
|
||||||
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
|
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
|
// Top left box
|
||||||
|
createRoundedRect(40, 100, 230, 100)
|
||||||
|
ctx.textAlign = 'center'
|
||||||
|
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
|
||||||
|
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
|
||||||
|
|
||||||
|
// Box top right
|
||||||
|
createRoundedRect(285, 100, 230, 100)
|
||||||
|
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
|
||||||
|
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
|
||||||
|
|
||||||
|
// Box bottom left
|
||||||
|
createRoundedRect(530, 100, 230, 100)
|
||||||
|
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
|
||||||
|
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
|
||||||
|
|
||||||
|
// Text stats
|
||||||
|
if (this.yearStats.totalBooksAddedSize) {
|
||||||
|
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
|
||||||
|
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
|
||||||
|
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yearStats.totalBooksAddedDuration) {
|
||||||
|
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
|
||||||
|
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
|
||||||
|
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.variant) {
|
||||||
|
// Bottom images
|
||||||
|
imgsToAdd = Object.values(imgsToAdd)
|
||||||
|
if (imgsToAdd.length > 0) {
|
||||||
|
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
|
||||||
|
let imgToAdd = imgsToAdd[i]
|
||||||
|
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.variant === 1) {
|
||||||
|
// Text stats
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
if (this.yearStats.topAuthors.length) {
|
||||||
|
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
||||||
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yearStats.topNarrators.length) {
|
||||||
|
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
|
||||||
|
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
|
||||||
|
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.variant === 2) {
|
||||||
|
// Text stats
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
if (this.yearStats.topAuthors.length) {
|
||||||
|
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
||||||
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.yearStats.topGenres.length) {
|
||||||
|
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
|
||||||
|
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||||
|
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canvas = canvas
|
||||||
|
this.dataUrl = canvas.toDataURL('png')
|
||||||
|
},
|
||||||
|
share() {
|
||||||
|
const base64Data = this.dataUrl.split(';base64,').pop()
|
||||||
|
FileSharer.share({
|
||||||
|
filename: `audiobookshelf_server_${this.year}.png`,
|
||||||
|
contentType: 'image/png',
|
||||||
|
base64Data
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message !== 'USER_CANCELLED') {
|
||||||
|
console.error('Failed to share', error.message)
|
||||||
|
this.$toast.error('Failed to share: ' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refresh() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
this.$nativeHttp
|
||||||
|
.get(`/api/stats/year/${this.year}`)
|
||||||
|
.then((data) => {
|
||||||
|
this.yearStats = data || []
|
||||||
|
return this.initCanvas()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to load year stats')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
192
components/stats/YearInReviewShort.vue
Normal file
192
components/stats/YearInReviewShort.vue
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner />
|
||||||
|
</div>
|
||||||
|
<img v-else-if="dataUrl" :src="dataUrl" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { FileSharer } from '@byteowls/capacitor-filesharer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
year: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canvas: null,
|
||||||
|
dataUrl: null,
|
||||||
|
yearStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initCanvas() {
|
||||||
|
if (!this.yearStats) return
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = 600
|
||||||
|
canvas.height = 200
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
const createRoundedRect = (x, y, w, h) => {
|
||||||
|
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
|
||||||
|
grd1.addColorStop(0, '#44444455')
|
||||||
|
grd1.addColorStop(1, '#ffffff11')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.strokeStyle = '#C0C0C088'
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.roundRect(x, y, w, h, [20])
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
|
||||||
|
ctx.letterSpacing = letterSpacing
|
||||||
|
|
||||||
|
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
|
||||||
|
if (maxWidth) {
|
||||||
|
let txtWidth = ctx.measureText(text).width
|
||||||
|
while (txtWidth > maxWidth) {
|
||||||
|
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
|
||||||
|
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
|
||||||
|
else text = text.slice(0, -3) // First check remove last 3 chars
|
||||||
|
text += '...'
|
||||||
|
txtWidth = ctx.measureText(text).width
|
||||||
|
console.log(`Checking text "${text}" (width:${txtWidth})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addIcon = (icon, color, fontSize, x, y) => {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.font = `${fontSize} Material Icons Outlined`
|
||||||
|
ctx.fillText(icon, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bg color
|
||||||
|
ctx.fillStyle = '#232323'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Cover image tiles
|
||||||
|
const bookCovers = this.yearStats.finishedBooksWithCovers
|
||||||
|
bookCovers.push(...this.yearStats.booksWithCovers)
|
||||||
|
|
||||||
|
if (bookCovers.length) {
|
||||||
|
let index = 0
|
||||||
|
ctx.globalAlpha = 0.25
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(canvas.width / 2, canvas.height / 2)
|
||||||
|
ctx.rotate((-Math.PI / 180) * 25)
|
||||||
|
ctx.translate(-canvas.width / 2, -canvas.height / 2)
|
||||||
|
ctx.translate(-10, -90)
|
||||||
|
for (let x = 0; x < 4; x++) {
|
||||||
|
for (let y = 0; y < 3; y++) {
|
||||||
|
const coverIndex = index % bookCovers.length
|
||||||
|
let libraryItemId = bookCovers[coverIndex]
|
||||||
|
index++
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.crossOrigin = 'anonymous'
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
let sw = img.width
|
||||||
|
if (img.width > img.height) {
|
||||||
|
sw = img.height
|
||||||
|
}
|
||||||
|
let sx = -(sw - img.width) / 2
|
||||||
|
let sy = -(sw - img.height) / 2
|
||||||
|
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
|
// Create gradient
|
||||||
|
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
|
||||||
|
grd1.addColorStop(0, '#000000aa')
|
||||||
|
grd1.addColorStop(1, '#cd9d49aa')
|
||||||
|
ctx.fillStyle = grd1
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// Top Abs icon
|
||||||
|
let tanColor = '#ffdb70'
|
||||||
|
ctx.fillStyle = tanColor
|
||||||
|
ctx.font = '42px absicons'
|
||||||
|
ctx.fillText('\ue900', 15, 36)
|
||||||
|
|
||||||
|
// Top text
|
||||||
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
|
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
|
// Top left box
|
||||||
|
createRoundedRect(15, 75, 280, 110)
|
||||||
|
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
|
||||||
|
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
|
||||||
|
const readIconPath = new Path2D()
|
||||||
|
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
|
||||||
|
ctx.fillStyle = '#ffffff'
|
||||||
|
ctx.fill(readIconPath)
|
||||||
|
|
||||||
|
createRoundedRect(305, 75, 280, 110)
|
||||||
|
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
|
||||||
|
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
|
||||||
|
addIcon('local_library', 'white', '42px', 345, 130)
|
||||||
|
|
||||||
|
this.canvas = canvas
|
||||||
|
this.dataUrl = canvas.toDataURL('png')
|
||||||
|
},
|
||||||
|
share() {
|
||||||
|
const base64Data = this.dataUrl.split(';base64,').pop()
|
||||||
|
FileSharer.share({
|
||||||
|
filename: `audiobookshelf_my_${this.year}_short.png`,
|
||||||
|
contentType: 'image/png',
|
||||||
|
base64Data
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.message !== 'USER_CANCELLED') {
|
||||||
|
console.error('Failed to share', error.message)
|
||||||
|
this.$toast.error('Failed to share: ' + error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refresh() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.$emit('update:processing', true)
|
||||||
|
this.$nativeHttp
|
||||||
|
.get(`/api/me/stats/year/${this.year}`)
|
||||||
|
.then((data) => {
|
||||||
|
this.yearStats = data || []
|
||||||
|
return this.initCanvas()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to load year stats')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.$emit('update:processing', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -30,6 +30,7 @@ export default {
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
paddingX: Number,
|
paddingX: Number,
|
||||||
|
paddingY: Number,
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
disabled: Boolean
|
disabled: Boolean
|
||||||
|
@ -48,14 +49,17 @@ export default {
|
||||||
if (this.small) {
|
if (this.small) {
|
||||||
list.push('text-sm')
|
list.push('text-sm')
|
||||||
if (this.paddingX === undefined) list.push('px-4')
|
if (this.paddingX === undefined) list.push('px-4')
|
||||||
list.push('py-1')
|
if (this.paddingY === undefined) list.push('py-1')
|
||||||
} else {
|
} else {
|
||||||
if (this.paddingX === undefined) list.push('px-8')
|
if (this.paddingX === undefined) list.push('px-8')
|
||||||
list.push('py-2')
|
if (this.paddingY === undefined) list.push('py-2')
|
||||||
}
|
}
|
||||||
if (this.paddingX !== undefined) {
|
if (this.paddingX !== undefined) {
|
||||||
list.push(`px-${this.paddingX}`)
|
list.push(`px-${this.paddingX}`)
|
||||||
}
|
}
|
||||||
|
if (this.paddingY !== undefined) {
|
||||||
|
list.push(`py-${this.paddingY}`)
|
||||||
|
}
|
||||||
if (this.disabled) {
|
if (this.disabled) {
|
||||||
list.push('cursor-not-allowed')
|
list.push('cursor-not-allowed')
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||||
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
PODS:
|
PODS:
|
||||||
- Alamofire (5.6.4)
|
- Alamofire (5.6.4)
|
||||||
|
- ByteowlsCapacitorFilesharer (5.0.0):
|
||||||
|
- Capacitor
|
||||||
- Capacitor (5.4.0):
|
- Capacitor (5.4.0):
|
||||||
- CapacitorCordova
|
- CapacitorCordova
|
||||||
- CapacitorApp (5.0.6):
|
- CapacitorApp (5.0.6):
|
||||||
|
@ -29,6 +31,7 @@ PODS:
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- Alamofire (~> 5.5)
|
- Alamofire (~> 5.5)
|
||||||
|
- "ByteowlsCapacitorFilesharer (from `../../node_modules/@byteowls/capacitor-filesharer`)"
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
|
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
|
||||||
|
@ -49,6 +52,8 @@ SPEC REPOS:
|
||||||
- RealmSwift
|
- RealmSwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
ByteowlsCapacitorFilesharer:
|
||||||
|
:path: "../../node_modules/@byteowls/capacitor-filesharer"
|
||||||
Capacitor:
|
Capacitor:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorApp:
|
CapacitorApp:
|
||||||
|
@ -74,6 +79,7 @@ EXTERNAL SOURCES:
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
|
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
|
||||||
|
ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176
|
||||||
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
|
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
|
||||||
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
|
||||||
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
|
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
|
||||||
|
@ -88,6 +94,6 @@ SPEC CHECKSUMS:
|
||||||
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
|
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
|
||||||
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
|
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7a8fc177ef0646dd60a1ee8aa387964975fcc1e3
|
PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.12.1
|
||||||
|
|
|
@ -589,12 +589,34 @@ class AudioPlayer: NSObject {
|
||||||
|
|
||||||
commandCenter.playCommand.isEnabled = true
|
commandCenter.playCommand.isEnabled = true
|
||||||
commandCenter.playCommand.addTarget { [weak self] event in
|
commandCenter.playCommand.addTarget { [weak self] event in
|
||||||
self?.play(allowSeekBack: true)
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
commandCenter.pauseCommand.isEnabled = true
|
commandCenter.pauseCommand.isEnabled = true
|
||||||
commandCenter.pauseCommand.addTarget { [weak self] event in
|
commandCenter.pauseCommand.addTarget { [weak self] event in
|
||||||
self?.pause()
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
commandCenter.togglePlayPauseCommand.isEnabled = true
|
||||||
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
|
||||||
|
guard let strongSelf = self else { return .commandFailed }
|
||||||
|
if strongSelf.isPlaying() {
|
||||||
|
strongSelf.pause()
|
||||||
|
} else {
|
||||||
|
strongSelf.play(allowSeekBack: true)
|
||||||
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
package-lock.json
generated
32
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.70-beta",
|
"version": "0.9.70-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@byteowls/capacitor-filesharer": "^5.0.0",
|
||||||
"@capacitor/android": "^5.0.0",
|
"@capacitor/android": "^5.0.0",
|
||||||
"@capacitor/app": "^5.0.6",
|
"@capacitor/app": "^5.0.6",
|
||||||
"@capacitor/browser": "^5.1.0",
|
"@capacitor/browser": "^5.1.0",
|
||||||
|
@ -1939,6 +1940,17 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@byteowls/capacitor-filesharer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@byteowls/capacitor-filesharer/-/capacitor-filesharer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-LtIMd8Ge94Kj9BQWF+A646drHAkRVC6ulTPZ1InkQtNH4VIy3WfYilN20VZM5KkOvrQ1lslXFIJwquwKptLgmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"file-saver": "2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor/android": {
|
"node_modules/@capacitor/android": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
|
||||||
|
@ -9195,6 +9207,11 @@
|
||||||
"node": ">=8.9.0"
|
"node": ">=8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
@ -21078,6 +21095,14 @@
|
||||||
"to-fast-properties": "^2.0.0"
|
"to-fast-properties": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@byteowls/capacitor-filesharer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@byteowls/capacitor-filesharer/-/capacitor-filesharer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-LtIMd8Ge94Kj9BQWF+A646drHAkRVC6ulTPZ1InkQtNH4VIy3WfYilN20VZM5KkOvrQ1lslXFIJwquwKptLgmw==",
|
||||||
|
"requires": {
|
||||||
|
"file-saver": "2.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@capacitor/android": {
|
"@capacitor/android": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-5.4.0.tgz",
|
||||||
|
@ -26353,6 +26378,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"file-uri-to-path": {
|
"file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
@ -34083,4 +34113,4 @@
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"ionic:serve": "npm run start"
|
"ionic:serve": "npm run start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@byteowls/capacitor-filesharer": "^5.0.0",
|
||||||
"@capacitor/android": "^5.0.0",
|
"@capacitor/android": "^5.0.0",
|
||||||
"@capacitor/app": "^5.0.6",
|
"@capacitor/app": "^5.0.6",
|
||||||
"@capacitor/browser": "^5.1.0",
|
"@capacitor/browser": "^5.1.0",
|
||||||
|
@ -47,4 +48,4 @@
|
||||||
"postcss": "^8.3.5",
|
"postcss": "^8.3.5",
|
||||||
"tailwindcss": "^3.3.2"
|
"tailwindcss": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<span v-if="!showPlay" class="px-2 text-base">{{ $strings.ButtonRead }} {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">{{ $strings.ButtonRead }} {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
|
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mx-1" :padding-x="2" @click="downloadClick">
|
||||||
<span class="material-icons" :class="(downloadItem || startingDownload) ? 'animate-pulse' : ''">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span>
|
<span class="material-icons" :class="downloadItem || startingDownload ? 'animate-pulse' : ''">{{ downloadItem || startingDownload ? 'downloading' : 'download' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
|
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
|
||||||
<span class="material-icons">more_vert</span>
|
<span class="material-icons">more_vert</span>
|
||||||
|
@ -79,8 +79,8 @@
|
||||||
<div v-if="podcastAuthor" class="text-sm">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="text-sm">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="bookAuthors && bookAuthors.length" class="text-sm">
|
<div v-else-if="bookAuthors && bookAuthors.length" class="text-sm">
|
||||||
<template v-for="(author, index) in bookAuthors">
|
<template v-for="(author, index) in bookAuthors">
|
||||||
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link>
|
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link
|
||||||
<span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">,</span>
|
><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -90,8 +90,8 @@
|
||||||
<div v-if="series && series.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelSeries }}</div>
|
<div v-if="series && series.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelSeries }}</div>
|
||||||
<div v-if="series && series.length" class="truncate text-sm">
|
<div v-if="series && series.length" class="truncate text-sm">
|
||||||
<template v-for="(series, index) in seriesList">
|
<template v-for="(series, index) in seriesList">
|
||||||
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link>
|
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link
|
||||||
<span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,</span>
|
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -101,16 +101,16 @@
|
||||||
<div v-if="narrators && narrators.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelNarrators }}</div>
|
<div v-if="narrators && narrators.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelNarrators }}</div>
|
||||||
<div v-if="narrators && narrators.length" class="truncate text-sm">
|
<div v-if="narrators && narrators.length" class="truncate text-sm">
|
||||||
<template v-for="(narrator, index) in narrators">
|
<template v-for="(narrator, index) in narrators">
|
||||||
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link>
|
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link
|
||||||
<span :key="index" v-if="index < narrators.length - 1">,</span>
|
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="genres.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelGenres }}</div>
|
<div v-if="genres.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelGenres }}</div>
|
||||||
<div v-if="genres.length" class="truncate text-sm">
|
<div v-if="genres.length" class="truncate text-sm">
|
||||||
<template v-for="(genre, index) in genres">
|
<template v-for="(genre, index) in genres">
|
||||||
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link>
|
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link
|
||||||
<span :key="index" v-if="index < genres.length - 1">,</span>
|
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-0 py-4 overflow-y-auto">
|
<div class="w-full h-full px-0 py-4 overflow-y-auto">
|
||||||
|
<!-- Year in review banner shown at the top in December and January -->
|
||||||
|
<stats-year-in-review-banner v-if="showYearInReviewBanner" />
|
||||||
|
|
||||||
<h1 class="text-xl px-4">
|
<h1 class="text-xl px-4">
|
||||||
{{ $strings.HeaderYourStats }}
|
{{ $strings.HeaderYourStats }}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -50,6 +53,9 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Year in review banner shown at the bottom Feb - Nov -->
|
||||||
|
<stats-year-in-review-banner v-if="!showYearInReviewBanner" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -58,7 +64,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listeningStats: null,
|
listeningStats: null,
|
||||||
windowWidth: 0
|
windowWidth: 0,
|
||||||
|
showYearInReviewBanner: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -103,7 +110,12 @@ export default {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
console.log('Loaded user listening data', this.listeningStats)
|
|
||||||
|
let month = new Date().getMonth()
|
||||||
|
// January and December show year in review banner
|
||||||
|
if (month === 11 || month === 0) {
|
||||||
|
this.showYearInReviewBanner = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -94,26 +94,31 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
||||||
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = true) => {
|
||||||
if (isNaN(seconds) || seconds === null) return ''
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
seconds = Math.round(seconds)
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
let minutes = Math.floor(seconds / 60)
|
||||||
seconds -= minutes * 60
|
seconds -= minutes * 60
|
||||||
var hours = Math.floor(minutes / 60)
|
let hours = Math.floor(minutes / 60)
|
||||||
minutes -= hours * 60
|
minutes -= hours * 60
|
||||||
|
|
||||||
var days = 0
|
let days = 0
|
||||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||||
days = Math.floor(hours / 24)
|
days = Math.floor(hours / 24)
|
||||||
hours -= days * 24
|
hours -= days * 24
|
||||||
}
|
}
|
||||||
|
|
||||||
var strs = []
|
// If not showing seconds then round minutes up
|
||||||
|
if (minutes && seconds && !showSeconds) {
|
||||||
|
if (seconds >= 30) minutes++
|
||||||
|
}
|
||||||
|
|
||||||
|
const strs = []
|
||||||
if (days) strs.push(`${days}d`)
|
if (days) strs.push(`${days}d`)
|
||||||
if (hours) strs.push(`${hours}h`)
|
if (hours) strs.push(`${hours}h`)
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
if (seconds) strs.push(`${seconds}s`)
|
if (seconds && showSeconds) strs.push(`${seconds}s`)
|
||||||
return strs.join(' ')
|
return strs.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Popis",
|
"LabelDescription": "Popis",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Interpret",
|
"LabelNarrator": "Interpret",
|
||||||
"LabelNarrators": "Interpreti",
|
"LabelNarrators": "Interpreti",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nedokončeno",
|
"LabelNotFinished": "Nedokončeno",
|
||||||
"LabelNotStarted": "Nezahájeno",
|
"LabelNotStarted": "Nezahájeno",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Datum vydání",
|
"LabelPubDate": "Datum vydání",
|
||||||
"LabelPublishYear": "Rok vydání",
|
"LabelPublishYear": "Rok vydání",
|
||||||
"LabelRead": "Číst",
|
"LabelRead": "Číst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivelse",
|
"LabelDescription": "Beskrivelse",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Fortæller",
|
"LabelNarrator": "Fortæller",
|
||||||
"LabelNarrators": "Fortællere",
|
"LabelNarrators": "Fortællere",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ikke færdig",
|
"LabelNotFinished": "Ikke færdig",
|
||||||
"LabelNotStarted": "Ikke påbegyndt",
|
"LabelNotStarted": "Ikke påbegyndt",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Udgivelsesdato",
|
"LabelPubDate": "Udgivelsesdato",
|
||||||
"LabelPublishYear": "Udgivelsesår",
|
"LabelPublishYear": "Udgivelsesår",
|
||||||
"LabelRead": "Læst",
|
"LabelRead": "Læst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
|
||||||
|
|
|
@ -104,15 +104,16 @@
|
||||||
"LabelContinueBooks": "Bücher fortfahren",
|
"LabelContinueBooks": "Bücher fortfahren",
|
||||||
"LabelContinueEpisodes": "Episoden fortfahren",
|
"LabelContinueEpisodes": "Episoden fortfahren",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Serie fortsetzen",
|
"LabelContinueSeries": "Serie fortsetzen",
|
||||||
"LabelCustomTime": "Benutzerdefinierte zeit",
|
"LabelCustomTime": "Benutzerdefinierte Zeit",
|
||||||
"LabelDescription": "Beschreibung",
|
"LabelDescription": "Beschreibung",
|
||||||
"LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen",
|
"LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen",
|
||||||
"LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Einschlaf-Timer verbleibt. Aktivieren Sie diese Einstellung, um nicht abzuklingen.",
|
"LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Einschlaf-Timer verbleibt. Aktivieren Sie diese Einstellung, um nicht abzuklingen.",
|
||||||
"LabelDisableAutoRewind": "Deaktiviere automatisches Rückspulen",
|
"LabelDisableAutoRewind": "Deaktiviere automatisches Rückspulen",
|
||||||
"LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen",
|
"LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen",
|
||||||
"LabelDisableShakeToResetHelp": "Wenn Sie Ihr Gerät schütteln, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktivieren Sie diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.",
|
"LabelDisableShakeToResetHelp": "Wenn Sie Ihr Gerät schütteln, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktivieren Sie diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.",
|
||||||
"LabelDisableVibrateOnReset": "Deaktiviere vibrieren beim Zurücksetzen",
|
"LabelDisableVibrateOnReset": "Deaktiviere Vibrieren beim Zurücksetzen",
|
||||||
"LabelDisableVibrateOnResetHelp": "Wenn der Einschlaf-Timer zurückgesetzt wird, vibriert Ihr Gerät. Aktivieren Sie diese Einstellung, um nicht zu vibrieren, wenn der Einschlaf-Timer zurückgesetzt wird.",
|
"LabelDisableVibrateOnResetHelp": "Wenn der Einschlaf-Timer zurückgesetzt wird, vibriert Ihr Gerät. Aktivieren Sie diese Einstellung, um nicht zu vibrieren, wenn der Einschlaf-Timer zurückgesetzt wird.",
|
||||||
"LabelDiscover": "Entdecken",
|
"LabelDiscover": "Entdecken",
|
||||||
"LabelDownload": "Herunterladen",
|
"LabelDownload": "Herunterladen",
|
||||||
|
@ -121,7 +122,7 @@
|
||||||
"LabelEbook": "E-Book",
|
"LabelEbook": "E-Book",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnableMp3IndexSeeking": "Aktiviere MP3-Indexsuche",
|
"LabelEnableMp3IndexSeeking": "MP3-Indexsuche",
|
||||||
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn Sie MP3-Dateien haben, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
|
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn Sie MP3-Dateien haben, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
"LabelEndOfChapter": "Kapitelende",
|
"LabelEndOfChapter": "Kapitelende",
|
||||||
|
@ -140,7 +141,7 @@
|
||||||
"LabelHapticFeedback": "Haptische Rückmeldung",
|
"LabelHapticFeedback": "Haptische Rückmeldung",
|
||||||
"LabelHasEbook": "Hat Ebook",
|
"LabelHasEbook": "Hat Ebook",
|
||||||
"LabelHasSupplementaryEbook": "Hat zusätzliches Ebook",
|
"LabelHasSupplementaryEbook": "Hat zusätzliches Ebook",
|
||||||
"LabelHeavy": "Schwer",
|
"LabelHeavy": "Stark",
|
||||||
"LabelHigh": "Hoch",
|
"LabelHigh": "Hoch",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelIncomplete": "Unvollständig",
|
"LabelIncomplete": "Unvollständig",
|
||||||
|
@ -149,14 +150,14 @@
|
||||||
"LabelJumpBackwardsTime": "Rückspulzeit",
|
"LabelJumpBackwardsTime": "Rückspulzeit",
|
||||||
"LabelJumpForwardsTime": "Vorwärtsspulzeit",
|
"LabelJumpForwardsTime": "Vorwärtsspulzeit",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLight": "Licht",
|
"LabelLight": "Leicht",
|
||||||
"LabelLineSpacing": "Zeilenabstand",
|
"LabelLineSpacing": "Zeilenabstand",
|
||||||
"LabelListenAgain": "Erneut anhören",
|
"LabelListenAgain": "Erneut anhören",
|
||||||
"LabelLocalBooks": "Lokale Bücher",
|
"LabelLocalBooks": "Lokale Bücher",
|
||||||
"LabelLocalPodcasts": "Lokale Podcasts",
|
"LabelLocalPodcasts": "Lokale Podcasts",
|
||||||
"LabelLockOrientation": "Ausrichtung sperren",
|
"LabelLockOrientation": "automatische Bildschirmausrichtung sperren",
|
||||||
"LabelLockPlayer": "Mediaplayer sperren",
|
"LabelLockPlayer": "Mediaplayer sperren",
|
||||||
"LabelLow": "Niedrig",
|
"LabelLow": "Wenig",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMedium": "Mittel",
|
"LabelMedium": "Mittel",
|
||||||
"LabelMore": "Mehr",
|
"LabelMore": "Mehr",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Erzähler",
|
"LabelNarrator": "Erzähler",
|
||||||
"LabelNarrators": "Erzähler",
|
"LabelNarrators": "Erzähler",
|
||||||
"LabelNewestAuthors": "Neueste Autoren",
|
"LabelNewestAuthors": "Neueste Autoren",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "Nein",
|
"LabelNo": "Nein",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotStarted": "Nicht begonnen",
|
"LabelNotStarted": "Nicht begonnen",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRead": "Lesen",
|
"LabelRead": "Lesen",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
||||||
|
@ -282,4 +285,4 @@
|
||||||
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen"
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen"
|
||||||
}
|
}
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Descripción",
|
"LabelDescription": "Descripción",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrador",
|
"LabelNarrator": "Narrador",
|
||||||
"LabelNarrators": "Narradores",
|
"LabelNarrators": "Narradores",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "No Terminado",
|
"LabelNotFinished": "No Terminado",
|
||||||
"LabelNotStarted": "Sin Iniciar",
|
"LabelNotStarted": "Sin Iniciar",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
"LabelRead": "Leído",
|
"LabelRead": "Leído",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrateur",
|
"LabelNarrator": "Narrateur",
|
||||||
"LabelNarrators": "Narrateurs",
|
"LabelNarrators": "Narrateurs",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Non terminé(e)",
|
"LabelNotFinished": "Non terminé(e)",
|
||||||
"LabelNotStarted": "Non Démarré(e)",
|
"LabelNotStarted": "Non Démarré(e)",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
"LabelRead": "Lire",
|
"LabelRead": "Lire",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Narrators",
|
"LabelNarrators": "Narrators",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Not Finished",
|
"LabelNotFinished": "Not Finished",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Naratori",
|
"LabelNarrators": "Naratori",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nedovršeno",
|
"LabelNotFinished": "Nedovršeno",
|
||||||
"LabelNotStarted": "Not Started",
|
"LabelNotStarted": "Not Started",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Datam izdavanja",
|
"LabelPubDate": "Datam izdavanja",
|
||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Da Completare",
|
"LabelNotFinished": "Da Completare",
|
||||||
"LabelNotStarted": "Non iniziato",
|
"LabelNotStarted": "Non iniziato",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRead": "Leggi",
|
"LabelRead": "Leggi",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
|
||||||
|
@ -282,4 +285,4 @@
|
||||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso"
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso"
|
||||||
}
|
}
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Aprašymas",
|
"LabelDescription": "Aprašymas",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Skaitytojas",
|
"LabelNarrator": "Skaitytojas",
|
||||||
"LabelNarrators": "Skaitytojai",
|
"LabelNarrators": "Skaitytojai",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nebaigta",
|
"LabelNotFinished": "Nebaigta",
|
||||||
"LabelNotStarted": "Nepasileista",
|
"LabelNotStarted": "Nepasileista",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Publikavimo data",
|
"LabelPubDate": "Publikavimo data",
|
||||||
"LabelPublishYear": "Leidimo metai",
|
"LabelPublishYear": "Leidimo metai",
|
||||||
"LabelRead": "Skaityta",
|
"LabelRead": "Skaityta",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
|
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beschrijving",
|
"LabelDescription": "Beschrijving",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Verteller",
|
"LabelNarrator": "Verteller",
|
||||||
"LabelNarrators": "Vertellers",
|
"LabelNarrators": "Vertellers",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Niet Voltooid",
|
"LabelNotFinished": "Niet Voltooid",
|
||||||
"LabelNotStarted": "Niet Gestart",
|
"LabelNotStarted": "Niet Gestart",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Publicatiedatum",
|
"LabelPubDate": "Publicatiedatum",
|
||||||
"LabelPublishYear": "Jaar van uitgave",
|
"LabelPublishYear": "Jaar van uitgave",
|
||||||
"LabelRead": "Lees",
|
"LabelRead": "Lees",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
|
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivelse",
|
"LabelDescription": "Beskrivelse",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Forteller",
|
"LabelNarrator": "Forteller",
|
||||||
"LabelNarrators": "Fortellere",
|
"LabelNarrators": "Fortellere",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ikke fullført",
|
"LabelNotFinished": "Ikke fullført",
|
||||||
"LabelNotStarted": "Ikke startet",
|
"LabelNotStarted": "Ikke startet",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Publiseringsdato",
|
"LabelPubDate": "Publiseringsdato",
|
||||||
"LabelPublishYear": "Publikasjonsår",
|
"LabelPublishYear": "Publikasjonsår",
|
||||||
"LabelRead": "Les",
|
"LabelRead": "Les",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
|
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Opis",
|
"LabelDescription": "Opis",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Narrator",
|
"LabelNarrator": "Narrator",
|
||||||
"LabelNarrators": "Lektorzy",
|
"LabelNarrators": "Lektorzy",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Nieukończone",
|
"LabelNotFinished": "Nieukończone",
|
||||||
"LabelNotStarted": "Nie rozpoęczto",
|
"LabelNotStarted": "Nie rozpoęczto",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Data publikacji",
|
"LabelPubDate": "Data publikacji",
|
||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
"LabelRead": "Read",
|
"LabelRead": "Read",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Продолжить",
|
"LabelContinueBooks": "Продолжить",
|
||||||
"LabelContinueEpisodes": "Продолжить эпизоды",
|
"LabelContinueEpisodes": "Продолжить эпизоды",
|
||||||
"LabelContinueListening": "Продолжить слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Продолжить серии",
|
"LabelContinueSeries": "Продолжить серии",
|
||||||
"LabelCustomTime": "Пользовательское время",
|
"LabelCustomTime": "Пользовательское время",
|
||||||
"LabelDescription": "Описание",
|
"LabelDescription": "Описание",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Читает",
|
"LabelNarrator": "Читает",
|
||||||
"LabelNarrators": "Чтецы",
|
"LabelNarrators": "Чтецы",
|
||||||
"LabelNewestAuthors": "Новые авторы",
|
"LabelNewestAuthors": "Новые авторы",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "Нет",
|
"LabelNo": "Нет",
|
||||||
"LabelNotFinished": "Не завершено",
|
"LabelNotFinished": "Не завершено",
|
||||||
"LabelNotStarted": "Не запущено",
|
"LabelNotStarted": "Не запущено",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Дата публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublishYear": "Год публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
"LabelRead": "Читать",
|
"LabelRead": "Читать",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Недавние серии",
|
"LabelRecentSeries": "Недавние серии",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
|
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
|
||||||
|
|
|
@ -104,6 +104,7 @@
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "Continue Books",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "Continue Episodes",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCustomTime": "Custom time",
|
"LabelCustomTime": "Custom time",
|
||||||
"LabelDescription": "Beskrivning",
|
"LabelDescription": "Beskrivning",
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
"LabelNarrator": "Berättare",
|
"LabelNarrator": "Berättare",
|
||||||
"LabelNarrators": "Berättare",
|
"LabelNarrators": "Berättare",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "Newest Authors",
|
||||||
|
"LabelNewestEpisodes": "Newest Episodes",
|
||||||
"LabelNo": "No",
|
"LabelNo": "No",
|
||||||
"LabelNotFinished": "Ej avslutad",
|
"LabelNotFinished": "Ej avslutad",
|
||||||
"LabelNotStarted": "Inte påbörjad",
|
"LabelNotStarted": "Inte påbörjad",
|
||||||
|
@ -182,6 +184,7 @@
|
||||||
"LabelPubDate": "Publiceringsdatum",
|
"LabelPubDate": "Publiceringsdatum",
|
||||||
"LabelPublishYear": "Publiceringsår",
|
"LabelPublishYear": "Publiceringsår",
|
||||||
"LabelRead": "Läst",
|
"LabelRead": "Läst",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"ButtonSendEbookToDevice": "将电子书发送到设备",
|
"ButtonSendEbookToDevice": "将电子书发送到设备",
|
||||||
"ButtonSeries": "系列",
|
"ButtonSeries": "系列",
|
||||||
"ButtonSetTimer": "设置定时器",
|
"ButtonSetTimer": "设置定时器",
|
||||||
"ButtonStream": "流",
|
"ButtonStream": "串流播放",
|
||||||
"ButtonSubmit": "提交",
|
"ButtonSubmit": "提交",
|
||||||
"ButtonSwitchServerUser": "切换服务器/用户",
|
"ButtonSwitchServerUser": "切换服务器/用户",
|
||||||
"ButtonUserStats": "用户统计信息",
|
"ButtonUserStats": "用户统计信息",
|
||||||
|
@ -101,10 +101,11 @@
|
||||||
"LabelClosePlayer": "关闭播放器",
|
"LabelClosePlayer": "关闭播放器",
|
||||||
"LabelCollapseSeries": "折叠系列",
|
"LabelCollapseSeries": "折叠系列",
|
||||||
"LabelComplete": "已完成",
|
"LabelComplete": "已完成",
|
||||||
"LabelContinueBooks": "Continue Books",
|
"LabelContinueBooks": "继续看书",
|
||||||
"LabelContinueEpisodes": "Continue Episodes",
|
"LabelContinueEpisodes": "继续剧集",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "继续收听",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueReading": "继续阅读",
|
||||||
|
"LabelContinueSeries": "继续收听系列",
|
||||||
"LabelCustomTime": "自定义时间",
|
"LabelCustomTime": "自定义时间",
|
||||||
"LabelDescription": "描述",
|
"LabelDescription": "描述",
|
||||||
"LabelDisableAudioFadeOut": "禁用音频淡出",
|
"LabelDisableAudioFadeOut": "禁用音频淡出",
|
||||||
|
@ -114,7 +115,7 @@
|
||||||
"LabelDisableShakeToResetHelp": "在计时器运行时摇晃设备,或者在计时器到期后2分钟内摇晃设备将重置睡眠计时器。启用此设置可禁用抖动以重置。",
|
"LabelDisableShakeToResetHelp": "在计时器运行时摇晃设备,或者在计时器到期后2分钟内摇晃设备将重置睡眠计时器。启用此设置可禁用抖动以重置。",
|
||||||
"LabelDisableVibrateOnReset": "重置时禁用振动",
|
"LabelDisableVibrateOnReset": "重置时禁用振动",
|
||||||
"LabelDisableVibrateOnResetHelp": "当睡眠计时器重置时,你的设备会振动。启用此设置以在睡眠计时器重置时不振动。",
|
"LabelDisableVibrateOnResetHelp": "当睡眠计时器重置时,你的设备会振动。启用此设置以在睡眠计时器重置时不振动。",
|
||||||
"LabelDiscover": "Discover",
|
"LabelDiscover": "发现",
|
||||||
"LabelDownload": "下载",
|
"LabelDownload": "下载",
|
||||||
"LabelDownloaded": "已下载",
|
"LabelDownloaded": "已下载",
|
||||||
"LabelDuration": "持续时间",
|
"LabelDuration": "持续时间",
|
||||||
|
@ -151,7 +152,7 @@
|
||||||
"LabelLanguage": "语言",
|
"LabelLanguage": "语言",
|
||||||
"LabelLight": "轻",
|
"LabelLight": "轻",
|
||||||
"LabelLineSpacing": "行间距",
|
"LabelLineSpacing": "行间距",
|
||||||
"LabelListenAgain": "Listen Again",
|
"LabelListenAgain": "再次收听",
|
||||||
"LabelLocalBooks": "本地书籍",
|
"LabelLocalBooks": "本地书籍",
|
||||||
"LabelLocalPodcasts": "本地播客",
|
"LabelLocalPodcasts": "本地播客",
|
||||||
"LabelLockOrientation": "锁定方向",
|
"LabelLockOrientation": "锁定方向",
|
||||||
|
@ -164,7 +165,8 @@
|
||||||
"LabelName": "名称",
|
"LabelName": "名称",
|
||||||
"LabelNarrator": "演播者",
|
"LabelNarrator": "演播者",
|
||||||
"LabelNarrators": "演播者",
|
"LabelNarrators": "演播者",
|
||||||
"LabelNewestAuthors": "Newest Authors",
|
"LabelNewestAuthors": "最新作者",
|
||||||
|
"LabelNewestEpisodes": "最新剧集",
|
||||||
"LabelNo": "取消",
|
"LabelNo": "取消",
|
||||||
"LabelNotFinished": "未听完",
|
"LabelNotFinished": "未听完",
|
||||||
"LabelNotStarted": "未开始",
|
"LabelNotStarted": "未开始",
|
||||||
|
@ -182,8 +184,9 @@
|
||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
"LabelRead": "阅读",
|
"LabelRead": "阅读",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelReadAgain": "再次阅读",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
||||||
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
||||||
"LabelRSSFeedPreventIndexing": "防止索引",
|
"LabelRSSFeedPreventIndexing": "防止索引",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue