Merge branch 'master' into iosChapterTrack

This commit is contained in:
advplyr 2024-01-01 08:11:59 -06:00
commit 73d70dd480
37 changed files with 1060 additions and 48 deletions

View file

@ -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')

View file

@ -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"

View file

@ -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) {

View file

@ -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')

View file

@ -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 }}&nbsp;</span>{{ displayTitle }} <span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</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>

View file

@ -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)">

View file

@ -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" />

View 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>

View 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>

View 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>

View 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>

View file

@ -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')
} }

View file

@ -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'

View file

@ -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

View file

@ -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
View file

@ -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=="
} }
} }
} }

View file

@ -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"
} }
} }

View file

@ -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>

View file

@ -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() {

View file

@ -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(' ')
} }

View file

@ -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",

View file

@ -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",

View file

@ -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"
} }

View file

@ -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",

View file

@ -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",

View file

@ -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é",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"
} }

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 владельца",

View file

@ -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",

View file

@ -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": "防止索引",