Compare commits

...
Author SHA1 Message Date
advplyr 5e7d039454 iOS version bump 0.9.71 2024-01-01 13:55:13 -06:00
advplyr f928df146a Version bump 0.9.71-beta 2024-01-01 12:56:12 -06:00
advplyr 6e57cb49c4 Update:Move modal titles down to not overlap with ios status bar 2024-01-01 12:24:41 -06:00
advplyr 2f8494b013 Add:Tags to item page & allow wrapping to multiple lines #1014 2024-01-01 12:08:15 -06:00
advplyr 6f1b93bd9f Update:Default to alt view so that wooden shelf does not show on initial load 2024-01-01 11:55:57 -06:00
advplyr 19b928d7f6 Fix:Download marker overlapping series sequence on book cover #996 2024-01-01 11:53:28 -06:00
advplyr b849be6f5e Add:Epub reader setting to force single page #1018 2024-01-01 11:31:45 -06:00
advplyrandGitHub 86096df1a4 Merge pull request #1003 from benonymity/playerSwipeBack
Disable iOS swipeBack in fullscreen audio player
2024-01-01 10:52:35 -06:00
advplyrandGitHub 98a465c7b4 Update components/app/AudioPlayer.vue 2024-01-01 10:51:46 -06:00
advplyr 3315f61ae4 Merge branch 'master' into playerSwipeBack 2024-01-01 10:46:01 -06:00
advplyrandGitHub f977445d7a Merge pull request #1004 from benonymity/statusBarTweaks
iOS Status Bar Tweaks
2024-01-01 10:31:58 -06:00
advplyr 095d5e1cec Fix item page gradient for light mode 2024-01-01 10:26:06 -06:00
advplyr 69fbfa4b33 Update nuxtjs/tailwindcss and update tailwind config 2024-01-01 10:19:55 -06:00
advplyr 1c89d33726 Update item page gradient to be absolute and below cover image, add transitions 2024-01-01 09:57:51 -06:00
advplyr acee907932 Merge branch 'master' into statusBarTweaks 2024-01-01 09:00:47 -06:00
advplyrandGitHub 08ad216fe2 Merge pull request #1002 from benonymity/iosChapterTrack
iOS Now Playing Chapter Track (updated #372)
2024-01-01 08:51:59 -06:00
advplyr 3cc7a0937c Fix seeking in NowPlaying to adjust for chapter start time 2024-01-01 08:49:39 -06:00
advplyr 322cad5548 Move AbsAudioPlayer.setChapterTrack to AudioPlayer instead of localStore 2024-01-01 08:22:20 -06:00
advplyr 73d70dd480 Merge branch 'master' into iosChapterTrack 2024-01-01 08:11:59 -06:00
advplyrandGitHub 56bc569d4a Merge pull request #1016 from KaiStarkk/hotfix-pr-1005
[Fix] Prevent crash from force unwrapping nil value in PR-1005
2023-12-31 16:06:50 -06:00
advplyr 981ae58f6a Update:Podfile for capacitor filesharer 2023-12-31 16:02:29 -06:00
Kieran Hannigan b2bfa4c002 [Fix] Prevent crash from force unwrapping nil value in PR #1005 2023-12-30 11:16:10 +08:00
advplyrandGitHub 8b6003c5e3 Merge pull request #1005 from benonymity/playPauseFix
Add togglePlayPauseCommand route to iOS
2023-12-29 15:53:20 -06:00
advplyr 017b6237ca Fix:Filter dialog clear filter button overflow notch on ios #999 2023-12-27 17:23:55 -06:00
advplyr d5496c2e9a Fix:Library list view long titles overflowing, clamped to 2 lines #1013 2023-12-27 17:10:57 -06:00
advplyr 5ef8961c33 Fix:Comma separated links on item page 2023-12-27 17:03:36 -06:00
advplyr e8bea48577 Map shelf translation strings 2023-12-26 16:40:28 -06:00
advplyrandGitHub 6cae78f5c2 Merge pull request #1007 from Jafeyyu/master
Update zh-cn.json
2023-12-26 16:34:01 -06:00
JafeyyuandGitHub d9d31a0620 Update zh-cn.json 2023-12-26 18:09:17 +08:00
advplyr 3682c5fca2 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-25 15:35:34 -06:00
advplyr 14b04a14b6 Add:Year in review cards to stats page 2023-12-25 15:35:29 -06:00
advplyrandGitHub f218d90bfb Merge pull request #997 from Jafeyyu/master
Update zh-cn.json
2023-12-24 12:02:17 -06:00
advplyrandGitHub 020f936ad4 Merge pull request #991 from Hallo951/master
Update de.json
2023-12-24 12:01:58 -06:00
benonymity 224e5efe48 add: togglePlayPause route 2023-12-23 01:51:37 -05:00
benonymity 439ab2c8c0 fix: item page background conflict 2023-12-23 01:45:40 -05:00
benonymity 26d0209763 fix: ios status bar mirror theme 2023-12-23 00:00:50 -05:00
benonymity 29086fc783 fix: no swipeback when audio player fullscreen 2023-12-22 22:34:24 -05:00
benonymity bb656a265f fix: broken isIos 2023-12-22 21:36:49 -05:00
benonymity b11ad9dc5b fix: only call on ios and remove...very professional logging 2023-12-22 21:18:55 -05:00
benonymity 2b1667e532 feat: now playing chapter track 2023-12-22 21:01:37 -05:00
JafeyyuandGitHub 228a198598 Update zh-cn.json 2023-12-21 16:09:21 +08:00
Hallo951andGitHub 2843308fb8 Update de.json 2023-12-19 09:03:06 +01:00
63 changed files with 3472 additions and 2070 deletions
+2 -2
View File
@@ -33,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 101
versionName "0.9.70-beta"
versionCode 102
versionName "0.9.71-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
+1
View File
@@ -9,6 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':byteowls-capacitor-filesharer')
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-clipboard')
@@ -1,4 +1,8 @@
[
{
"pkg": "@byteowls/capacitor-filesharer",
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
},
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
@@ -116,6 +116,7 @@ class MainActivity : BridgeActivity() {
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
outState.clear()
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+3
View File
@@ -2,6 +2,9 @@
include ':capacitor-android'
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'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
+1 -4
View File
@@ -2,6 +2,7 @@
@import "./fonts.css";
@import './defaultStyles.css';
@import './absicons.css';
@import './transitions.css';
* {
-webkit-touch-callout: none;
@@ -16,10 +17,6 @@ textarea {
user-select: auto;
}
body {
background-color: #262626;
}
.layout-wrapper {
height: calc(100vh - env(safe-area-inset-top));
min-height: calc(100vh - env(safe-area-inset-top));
+6 -2
View File
@@ -18,7 +18,7 @@
--color-track: 107 114 128;
--color-track-buffered: 75 85 99;
--gradient-item-page: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%);
--gradient-audio-player: linear-gradient(169deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%);
--gradient-audio-player: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%);
}
@@ -37,7 +37,11 @@
--color-track: 189 191 191;
--color-track-buffered: 129 131 131;
--gradient-item-page: linear-gradient(169deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-audio-player: linear-gradient(169deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-audio-player: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 80%);
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.9) 20%, rgb(255, 255, 255) 60%);
}
body {
@apply bg-primary
}
}
+51
View File
@@ -0,0 +1,51 @@
.slide-enter-active {
-moz-transition-duration: 0.1s;
-webkit-transition-duration: 0.1s;
-o-transition-duration: 0.1s;
transition-duration: 0.1s;
-moz-transition-timing-function: ease-in;
-webkit-transition-timing-function: ease-in;
-o-transition-timing-function: ease-in;
transition-timing-function: ease-in;
}
.slide-leave-active {
-moz-transition-duration: 0.2s;
-webkit-transition-duration: 0.2s;
-o-transition-duration: 0.2s;
transition-duration: 0.2s;
-moz-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-webkit-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
-o-transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
}
.slide-enter-to,
.slide-leave {
max-height: 600px;
overflow: hidden;
}
.slide-enter,
.slide-leave-to {
overflow: hidden;
max-height: 0;
}
.menu-enter,
.menu-leave-active {
transform: translateY(-15px);
}
.menu-enter-active {
transition: all 0.2s;
}
.menu-leave-active {
transition: all 0.1s;
}
.menu-enter,
.menu-leave-active {
opacity: 0;
}
+15 -3
View File
@@ -164,6 +164,7 @@ export default {
showFullscreen(val) {
this.updateScreenSize()
this.$store.commit('setPlayerFullscreen', !!val)
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ""
},
bookCoverAspectRatio() {
this.updateScreenSize()
@@ -632,6 +633,10 @@ export default {
},
touchstart(e) {
if (!this.showFullscreen || !e.changedTouches) return
if (e.pageX < 20) {
e.preventDefault()
e.stopImmediatePropagation()
}
this.touchStartY = e.changedTouches[0].screenY
if (this.touchStartY > window.innerHeight / 3) {
@@ -695,8 +700,8 @@ export default {
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.$localStore.setUseChapterTrack(this.useChapterTrack)
} else if (action === 'total_track') {
this.useTotalTrack = !this.useTotalTrack
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
@@ -704,13 +709,20 @@ export default {
this.updateTimestamp()
this.updateTrack()
this.updateReadyTrack()
this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack)
this.$localStore.setUseChapterTrack(this.useChapterTrack)
} else if (action === 'close') {
this.closePlayback()
}
})
},
updateUseChapterTrack() {
this.$localStore.setUseChapterTrack(this.useChapterTrack)
// Chapter track in NowPlaying only supported on iOS for now
if (this.$platform === 'ios') {
AbsAudioPlayer.setChapterTrack({ enabled: this.useChapterTrack })
}
},
forceCloseDropdownMenu() {
if (this.$refs.dropdownMenu && this.$refs.dropdownMenu.closeMenu) {
this.$refs.dropdownMenu.closeMenu()
@@ -848,7 +860,7 @@ export default {
window.addEventListener('resize', this.screenOrientationChange)
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
document.body.addEventListener('touchstart', this.touchstart)
document.body.addEventListener('touchstart', this.touchstart, { passive: false })
document.body.addEventListener('touchend', this.touchend)
document.body.addEventListener('touchmove', this.touchmove)
this.$nextTick(this.init)
+1 -1
View File
@@ -59,7 +59,7 @@
<div v-if="!collapsedSeries && (!isPodcast || recentEpisode)" 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: width * userProgressPercent + 'px' }"></div>
<!-- Downloaded icon -->
<div v-if="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<div v-if="showHasLocalDownload" class="absolute right-0 top-0 z-20" :style="{ top: (isPodcast || (seriesSequence && showSequence) ? 1.75 : 0.375) * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
</div>
+1 -1
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>
<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 }}
</p>
<p class="truncate text-fg-muted" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
@@ -1,27 +1,28 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate capitalize">{{ $strings.LabelAutoRewindTime }}</p>
</div>
</template>
<div
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
@click="
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="
show = false
manualTimerModal = false
"
>
">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false">
<span class="material-icons text-3xl">arrow_back</span>
</div>
<div class="flex my-2 justify-between">
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">remove</span></ui-btn>
<ui-btn @click="decreaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px">
<span class="material-icons">remove</span>
</ui-btn>
<p class="text-2xl font-mono text-center">{{ manualTimeoutMin }} min</p>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
<ui-btn @click="increaseManualTimeout" class="w-9 h-9" :padding-x="0" small style="max-width: 36px">
<span class="material-icons">add</span>
</ui-btn>
</div>
<ui-btn @click="clickedOption(manualTimeoutMin)" class="w-full">{{ $strings.ButtonSetTimer }}</ui-btn>
</div>
+3 -7
View File
@@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate">{{ $strings.LabelYourBookmarks }}</p>
</div>
</template>
@@ -14,9 +14,7 @@
</div>
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
<div class="flex-grow" />
<p class="text-xl font-mono">
{{ this.$secondsToTimestamp(currentTime) }}
</p>
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
</div>
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
@@ -34,9 +32,7 @@
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80" @click.stop="createBookmark">
<span class="material-icons">add</span>
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
<p class="text-sm font-mono">
{{ this.$secondsToTimestamp(currentTime) }}
</p>
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p>
</div>
</div>
</div>
+3 -3
View File
@@ -1,12 +1,12 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<template #outer>
<div v-show="selected !== 'all'" class="absolute top-10 left-4 z-40">
<ui-btn class="text-lg border-yellow-400 border-opacity-40" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
<div v-show="selected !== 'all'" class="absolute top-12 left-4 z-40">
<ui-btn class="text-lg border-yellow-400 border-opacity-40 h-10" :padding-y="0" @click="clearSelected">{{ $strings.ButtonClearFilter }}</ui-btn>
</div>
</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 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">
<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)">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="halfScreen ? 'h-[50vh] min-h-[400px]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="content" class="relative text-fg h-full w-full bg-bg">
<slot />
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="400" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40">
<div class="absolute top-11 left-4 z-40">
<p class="text-white text-2xl truncate">{{ $strings.HeaderDetails }}</p>
</div>
</template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<modals-modal v-model="show" :width="300" :processing="processing" height="100%">
<template #outer>
<div class="absolute top-8 left-4 z-40" style="max-width: 80%">
<div class="absolute top-11 left-4 z-40" style="max-width: 80%">
<p class="text-white text-2xl truncate">{{ $strings.HeaderLibraries }}</p>
</div>
</template>
+1 -1
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 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>
</div>
<slot name="outer" />
+30 -6
View File
@@ -56,7 +56,7 @@
<!-- ereader settings modal -->
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen>
<div style="box-shadow: 0px -8px 8px #11111155">
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-8">
<div class="flex items-end justify-between h-20 px-4 pb-2 mb-6">
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
<button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span>
@@ -64,24 +64,30 @@
</div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(100vh-85px)]">
<div class="w-full h-full px-4">
<div class="flex items-center mb-8">
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelTheme }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelFontScale }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-8">
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center">
<div class="w-32">
<p class="text-base">{{ $strings.LabelLayout }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
</div>
</div>
</div>
</div>
@@ -109,7 +115,8 @@ export default {
ereaderSettings: {
theme: 'dark',
fontScale: 100,
lineSpacing: 115
lineSpacing: 115,
spread: 'auto'
}
}
},
@@ -154,6 +161,18 @@ export default {
if (this.isEpub) return this.ereaderSettings.theme
return document.documentElement.dataset.theme || 'dark'
},
spreadItems() {
return [
{
text: this.$strings.LabelLayoutSinglePage,
value: 'none'
},
{
text: this.$strings.LabelLayoutAuto,
value: 'auto'
}
]
},
themeItems() {
return [
{
@@ -350,7 +369,12 @@ export default {
try {
const settings = localStorage.getItem('ereaderSettings')
if (settings) {
this.ereaderSettings = JSON.parse(settings)
const ereaderSettings = JSON.parse(settings)
if (!ereaderSettings.spread) {
// Added in 0.9.71
ereaderSettings.spread = 'auto'
}
this.ereaderSettings = ereaderSettings
this.settingsUpdated()
}
} catch (error) {
+283
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>
+134
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>
+262
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>
+192
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>
+6 -2
View File
@@ -30,6 +30,7 @@ export default {
default: ''
},
paddingX: Number,
paddingY: Number,
small: Boolean,
loading: Boolean,
disabled: Boolean
@@ -48,14 +49,17 @@ export default {
if (this.small) {
list.push('text-sm')
if (this.paddingX === undefined) list.push('px-4')
list.push('py-1')
if (this.paddingY === undefined) list.push('py-1')
} else {
if (this.paddingX === undefined) list.push('px-8')
list.push('py-2')
if (this.paddingY === undefined) list.push('py-2')
}
if (this.paddingX !== undefined) {
list.push(`px-${this.paddingX}`)
}
if (this.paddingY !== undefined) {
list.push(`py-${this.paddingY}`)
}
if (this.disabled) {
list.push('cursor-not-allowed')
}
+4 -4
View File
@@ -728,12 +728,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.70;
MARKETING_VERSION = 0.9.71;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -752,12 +752,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 27;
CURRENT_PROJECT_VERSION = 28;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.70;
MARKETING_VERSION = 0.9.71;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+13 -6
View File
@@ -4,17 +4,17 @@ import RealmSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
private let logger = AppLogger(category: "AppDelegate")
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 15,
schemaVersion: 16,
migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@@ -48,10 +48,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["languageCode"] = "en-us"
}
}
if (oldSchemaVersion < 16) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["chapterTrack"] = false
}
}
}
)
Realm.Configuration.defaultConfiguration = configuration
return true
}
@@ -93,7 +100,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
// Stores the completion handler for background downloads
// The identifier of this method can be ignored at this time as we only have one background url session
+1
View File
@@ -15,6 +15,7 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
CAP_PLUGIN_METHOD(closePlayback, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setChapterTrack, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
+11
View File
@@ -119,6 +119,17 @@ public class AbsAudioPlayer: CAPPlugin {
PlayerHandler.setPlaybackSpeed(speed: settings.playbackRate)
call.resolve()
}
@objc func setChapterTrack(_ call: CAPPluginCall) {
let chapterTrack = call.getBool("enabled", true)
logger.log(String(chapterTrack))
let settings = PlayerSettings.main()
try? settings.update {
settings.chapterTrack = chapterTrack
}
PlayerHandler.setChapterTrack()
call.resolve()
}
@objc func playPlayer(_ call: CAPPluginCall) {
PlayerHandler.paused = false
+1
View File
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :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 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
+7 -1
View File
@@ -1,5 +1,7 @@
PODS:
- Alamofire (5.6.4)
- ByteowlsCapacitorFilesharer (5.0.0):
- Capacitor
- Capacitor (5.4.0):
- CapacitorCordova
- CapacitorApp (5.0.6):
@@ -29,6 +31,7 @@ PODS:
DEPENDENCIES:
- Alamofire (~> 5.5)
- "ByteowlsCapacitorFilesharer (from `../../node_modules/@byteowls/capacitor-filesharer`)"
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
@@ -49,6 +52,8 @@ SPEC REPOS:
- RealmSwift
EXTERNAL SOURCES:
ByteowlsCapacitorFilesharer:
:path: "../../node_modules/@byteowls/capacitor-filesharer"
Capacitor:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
@@ -74,6 +79,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c
ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6
@@ -88,6 +94,6 @@ SPEC CHECKSUMS:
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716
PODFILE CHECKSUM: 7a8fc177ef0646dd60a1ee8aa387964975fcc1e3
PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c
COCOAPODS: 1.12.1
@@ -160,3 +160,11 @@ class PlaybackSession: Object, Codable, Deletable {
try container.encode(localMediaProgressId, forKey: .localMediaProgressId)
}
}
extension PlaybackSession {
func getCurrentChapter() -> Chapter? {
return chapters.first { chapter in
chapter.start <= self.currentTime && chapter.end > self.currentTime
}
}
}
@@ -12,6 +12,8 @@ class PlayerSettings: Object {
// The webapp has a persisted setting for playback speed, but it's not always available to the native code
// Lets track it natively as well, so we never have a situation where the UI and native player are out of sync
@Persisted var playbackRate: Float = 1.0
@Persisted var chapterTrack: Bool = true
// Singleton pattern for Realm objects
static func main() -> PlayerSettings {
@@ -39,3 +39,13 @@ class Chapter: EmbeddedObject, Codable {
try container.encode(title, forKey: .title)
}
}
extension Chapter {
func getRelativeChapterCurrentTime(sessionCurrentTime: Double) -> Double {
return sessionCurrentTime - self.start
}
func getRelativeChapterEndTime() -> Double {
return self.end - self.start
}
}
+48 -4
View File
@@ -224,6 +224,8 @@ class AudioPlayer: NSObject {
// Update the UI
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
}
// Update the now playing and chapter info
self.updateNowPlaying()
}
}
}
@@ -443,6 +445,10 @@ class AudioPlayer: NSObject {
}
}
public func setChapterTrack() {
self.updateNowPlaying()
}
public func getCurrentTime() -> Double? {
guard let playbackSession = self.getPlaybackSession() else { return nil }
let currentTrackTime = self.audioPlayer.currentTime().seconds
@@ -583,12 +589,34 @@ class AudioPlayer: NSObject {
commandCenter.playCommand.isEnabled = true
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
}
commandCenter.pauseCommand.isEnabled = true
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
}
@@ -640,7 +668,14 @@ class AudioPlayer: NSObject {
return .noSuchContent
}
self?.seek(event.positionTime, from: "remote")
// Adjust seek time if chapter track is being used
var seekTime = event.positionTime
if PlayerSettings.main().chapterTrack {
if let session = self?.getPlaybackSession(), let currentChapter = session.getCurrentChapter() {
seekTime += currentChapter.start
}
}
self?.seek(seekTime, from: "remote")
return .success
}
@@ -657,7 +692,16 @@ class AudioPlayer: NSObject {
}
private func updateNowPlaying() {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
if let session = self.getPlaybackSession(), let currentChapter = session.getCurrentChapter(), PlayerSettings.main().chapterTrack {
NowPlayingInfo.shared.update(
duration: currentChapter.getRelativeChapterEndTime(),
currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime),
rate: rate,
chapterName: currentChapter.title,
chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1,
chapterCount: session.chapters.count
)
} else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate)
}
}
@@ -61,6 +61,10 @@ class PlayerHandler {
self.player?.setPlaybackRate(speed)
}
public static func setChapterTrack() {
self.player?.setChapterTrack()
}
public static func getSleepTimeRemaining() -> Double? {
return self.player?.getSleepTimeRemaining()
}
+14 -2
View File
@@ -53,7 +53,7 @@ class NowPlayingInfo {
self.setMetadata(artwork: artwork, metadata: metadata)
}
}
public func update(duration: Double, currentTime: Double, rate: Float) {
public func update(duration: Double, currentTime: Double, rate: Float, chapterName: String? = nil, chapterNumber: Int? = nil, chapterCount: Int? = nil) {
// Update on the main to prevent access collisions
DispatchQueue.main.async { [weak self] in
if let self = self {
@@ -62,6 +62,18 @@ class NowPlayingInfo {
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0
if let chapterName = chapterName, let chapterNumber = chapterNumber, let chapterCount = chapterCount {
self.nowPlayingInfo[MPMediaItemPropertyTitle] = chapterName
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = chapterNumber
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = chapterCount
} else {
// Set the title back to the book title
self.nowPlayingInfo[MPMediaItemPropertyTitle] = self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle]
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterNumber] = nil
self.nowPlayingInfo[MPNowPlayingInfoPropertyChapterCount] = nil
}
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo
}
}
@@ -89,7 +101,7 @@ class NowPlayingInfo {
nowPlayingInfo[MPMediaItemPropertyTitle] = metadata!.title
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.title
}
private func shouldFetchCover(id: String) -> Bool {
+1
View File
@@ -2,6 +2,7 @@ const pkg = require('./package.json')
export default {
ssr: false,
target: 'static',
env: {
PROD: '1',
+1999 -1823
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.70-beta",
"version": "0.9.71-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
@@ -13,6 +13,7 @@
"ionic:serve": "npm run start"
},
"dependencies": {
"@byteowls/capacitor-filesharer": "^5.0.0",
"@capacitor/android": "^5.0.0",
"@capacitor/app": "^5.0.6",
"@capacitor/browser": "^5.1.0",
@@ -43,8 +44,7 @@
"@babel/core": "7.13.15",
"@babel/preset-env": "7.13.15",
"@capacitor/cli": "^5.0.0",
"@nuxtjs/tailwindcss": "^4.2.1",
"postcss": "^8.3.5",
"tailwindcss": "^3.3.2"
"@nuxtjs/tailwindcss": "^6.10.3",
"postcss": "^8.3.5"
}
}
+170 -140
View File
@@ -1,149 +1,160 @@
<template>
<div id="item-page" class="w-full h-full px-3 pb-4 overflow-y-auto overflow-x-hidden relative bg-bg">
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
<div class="w-full h-full absolute top-0 left-0" style="background: var(--gradient-item-page)" />
</div>
<div class="z-10 relative">
<!-- cover -->
<div class="w-full flex justify-center relative mb-4">
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
<div style="width: 150vw; overflow: hidden">
<div id="coverBg" style="filter: blur(5vw)">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
</div>
<div id="item-page" class="w-full h-full overflow-y-auto overflow-x-hidden relative bg-bg">
<!-- cover -->
<div class="w-full flex justify-center relative">
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
<div style="width: 150vw; overflow: hidden">
<div id="coverBg" style="filter: blur(5vw)">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
</div>
</div>
<div class="relative" @click="showFullscreenCover = true">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
</div>
</div>
<!-- title -->
<div class="text-center mb-2">
<h1 class="text-xl font-semibold">{{ title }}</h1>
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
<div class="relative" @click="showFullscreenCover = true">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
</div>
<div v-if="hasLocal" class="mx-1">
<div v-if="isLocalOnly" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $strings.MessageMediaNotLinkedToServer }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingServerAddress" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $getString('MessageMediaLinkedToADifferentServer', [localLibraryItem.serverAddress]) }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingUser" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $strings.MessageMediaLinkedToADifferentUser }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingConnectionConfig" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is linked to a different server connection config. Downloaded User Id: {{ localLibraryItem.serverUserId }}. Downloaded Server Address: {{ localLibraryItem.serverAddress }}. Currently connected User Id: {{ user.id }}. Currently connected server address: {{ currentServerAddress }}.</p>
</div>
</div>
<!-- action buttons -->
<div class="col-span-full">
<div v-if="showPlay || showRead" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" class="flex items-center justify-center flex-grow mx-1" :loading="playerIsStartingForThisMedia" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? $strings.ButtonPause : isPodcast ? $strings.ButtonNextEpisode : hasLocal ? $strings.ButtonPlay : $strings.ButtonStream }}</span>
</ui-btn>
<ui-btn v-if="showRead" color="info" class="flex items-center justify-center mx-1" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">{{ $strings.ButtonRead }} {{ ebookFormat }}</span>
</ui-btn>
<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>
</ui-btn>
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
<span class="material-icons">more_vert</span>
</ui-btn>
</div>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-fg mt-4 text-center">
<p>{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-fg-muted text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
<p v-else-if="userIsFinished" class="text-fg-muted text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt) }}</p>
</div>
</div>
<div v-if="downloadItem" class="py-3">
<p v-if="downloadItem.itemProgress == 1" class="text-center text-lg">{{ $strings.MessageDownloadCompleteProcessing }}</p>
<p v-else class="text-center text-lg">{{ $strings.MessageDownloading }} ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
</div>
<!-- metadata -->
<div id="metadata" class="grid gap-2 my-2" style>
<div v-if="podcastAuthor || (bookAuthors && bookAuthors.length)" class="text-fg-muted uppercase text-sm">{{ $strings.LabelAuthor }}</div>
<div v-if="podcastAuthor" class="text-sm">{{ podcastAuthor }}</div>
<div v-else-if="bookAuthors && bookAuthors.length" class="text-sm">
<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>
<span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">,</span>
</template>
</div>
<div v-if="podcastType" class="text-fg-muted uppercase text-sm">{{ $strings.LabelType }}</div>
<div v-if="podcastType" class="text-sm capitalize">{{ podcastType }}</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">
<template v-for="(series, index) in seriesList">
<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>
</template>
</div>
<div v-if="numTracks" class="text-fg-muted uppercase text-sm">{{ $strings.LabelDuration }}</div>
<div v-if="numTracks" class="text-sm">{{ $elapsedPretty(duration) }}</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">
<template v-for="(narrator, index) in narrators">
<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>
</template>
</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">
<template v-for="(genre, index) in genres">
<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>
</template>
</div>
<div v-if="publishedYear" class="text-fg-muted uppercase text-sm">{{ $strings.LabelPublishYear }}</div>
<div v-if="publishedYear" class="text-sm">{{ publishedYear }}</div>
</div>
<div v-if="description" class="w-full py-2">
<p ref="description" class="text-sm text-justify whitespace-pre-line font-light" :class="{ 'line-clamp-4': !showFullDescription }" style="hyphens: auto">{{ description }}</p>
<div v-if="descriptionClamped" class="text-fg text-sm py-2" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }}
<span class="material-icons align-middle text-base -mt-px">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</div>
</div>
<!-- tables -->
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
<tables-chapters-table v-if="numChapters" :library-item="libraryItem" @playAtTimestamp="playAtTimestamp" />
<tables-tracks-table v-if="numTracks" :tracks="tracks" :library-item-id="libraryItemId" />
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" />
<!-- modals -->
<modals-item-more-menu-modal v-model="showMoreMenu" :library-item="libraryItem" :rss-feed="rssFeed" :processing.sync="processing" />
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
<modals-fullscreen-cover v-model="showFullscreenCover" :library-item="libraryItem" />
</div>
<div class="relative">
<!-- background gradient -->
<div id="item-page-bg-gradient" class="absolute top-0 left-0 w-full pointer-events-none z-0" :style="{ opacity: coverRgb ? 1 : 0 }">
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
<div class="w-full h-full absolute top-0 left-0" style="background: var(--gradient-item-page)" />
</div>
<div class="relative z-10 px-3 py-4">
<!-- title -->
<div class="text-center mb-2">
<h1 class="text-xl font-semibold">{{ title }}</h1>
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
</div>
<div v-if="hasLocal" class="mx-1">
<div v-if="isLocalOnly" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $strings.MessageMediaNotLinkedToServer }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingServerAddress" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $getString('MessageMediaLinkedToADifferentServer', [localLibraryItem.serverAddress]) }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingUser" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">{{ $strings.MessageMediaLinkedToADifferentUser }}</p>
</div>
<div v-else-if="currentServerConnectionConfigId && !isLocalMatchingConnectionConfig" class="w-full rounded-md bg-warning/10 border border-warning p-4">
<p class="text-sm">Media is linked to a different server connection config. Downloaded User Id: {{ localLibraryItem.serverUserId }}. Downloaded Server Address: {{ localLibraryItem.serverAddress }}. Currently connected User Id: {{ user.id }}. Currently connected server address: {{ currentServerAddress }}.</p>
</div>
</div>
<!-- action buttons -->
<div class="col-span-full">
<div v-if="showPlay || showRead" class="flex mt-4 -mx-1">
<ui-btn v-if="showPlay" color="success" class="flex items-center justify-center flex-grow mx-1" :loading="playerIsStartingForThisMedia" :padding-x="4" @click="playClick">
<span class="material-icons">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span class="px-1 text-sm">{{ playerIsPlaying ? $strings.ButtonPause : isPodcast ? $strings.ButtonNextEpisode : hasLocal ? $strings.ButtonPlay : $strings.ButtonStream }}</span>
</ui-btn>
<ui-btn v-if="showRead" color="info" class="flex items-center justify-center mx-1" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">{{ $strings.ButtonRead }} {{ ebookFormat }}</span>
</ui-btn>
<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>
</ui-btn>
<ui-btn color="primary" class="flex items-center justify-center mx-1" :padding-x="2" @click="moreButtonPress">
<span class="material-icons">more_vert</span>
</ui-btn>
</div>
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-fg mt-4 text-center">
<p>{{ $strings.LabelYourProgress }}: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="!useEBookProgress && !userIsFinished" class="text-fg-muted text-xs">{{ $getString('LabelTimeRemaining', [$elapsedPretty(userTimeRemaining)]) }}</p>
<p v-else-if="userIsFinished" class="text-fg-muted text-xs">{{ $strings.LabelFinished }} {{ $formatDate(userProgressFinishedAt) }}</p>
</div>
</div>
<div v-if="downloadItem" class="py-3">
<p v-if="downloadItem.itemProgress == 1" class="text-center text-lg">{{ $strings.MessageDownloadCompleteProcessing }}</p>
<p v-else class="text-center text-lg">{{ $strings.MessageDownloading }} ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
</div>
<!-- metadata -->
<div id="metadata" class="grid gap-2 my-2" style>
<div v-if="podcastAuthor || bookAuthors?.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelAuthor }}</div>
<div v-if="podcastAuthor" class="text-sm">{{ podcastAuthor }}</div>
<div v-else-if="bookAuthors?.length" class="text-sm">
<template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline whitespace-nowrap">{{ author.name }}</nuxt-link
><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">, </span>
</template>
</div>
<div v-if="podcastType" class="text-fg-muted uppercase text-sm">{{ $strings.LabelType }}</div>
<div v-if="podcastType" class="text-sm capitalize">{{ podcastType }}</div>
<div v-if="series?.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelSeries }}</div>
<div v-if="series?.length" class="text-sm">
<template v-for="(series, index) in seriesList">
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline whitespace-nowrap">{{ series.text }}</nuxt-link
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </span>
</template>
</div>
<div v-if="numTracks" class="text-fg-muted uppercase text-sm">{{ $strings.LabelDuration }}</div>
<div v-if="numTracks" class="text-sm">{{ $elapsedPretty(duration) }}</div>
<div v-if="narrators?.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelNarrators }}</div>
<div v-if="narrators?.length" class="text-sm">
<template v-for="(narrator, index) in narrators">
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline whitespace-nowrap">{{ narrator }}</nuxt-link
><span :key="index" v-if="index < narrators.length - 1">, </span>
</template>
</div>
<div v-if="genres.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelGenres }}</div>
<div v-if="genres.length" class="text-sm">
<template v-for="(genre, index) in genres">
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline whitespace-nowrap">{{ genre }}</nuxt-link
><span :key="index" v-if="index < genres.length - 1">, </span>
</template>
</div>
<div v-if="tags.length" class="text-fg-muted uppercase text-sm">{{ $strings.LabelTags }}</div>
<div v-if="tags.length" class="text-sm">
<template v-for="(tag, index) in tags">
<nuxt-link :key="tag" :to="`/bookshelf/library?filter=tags.${$encode(tag)}`" class="underline whitespace-nowrap">{{ tag }}</nuxt-link
><span :key="index" v-if="index < tags.length - 1">, </span>
</template>
</div>
<div v-if="publishedYear" class="text-fg-muted uppercase text-sm">{{ $strings.LabelPublishYear }}</div>
<div v-if="publishedYear" class="text-sm">{{ publishedYear }}</div>
</div>
<div v-if="description" class="w-full py-2">
<p ref="description" class="text-sm text-justify whitespace-pre-line font-light" :class="{ 'line-clamp-4': !showFullDescription }" style="hyphens: auto">{{ description }}</p>
<div v-if="descriptionClamped" class="text-fg text-sm py-2" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }}
<span class="material-icons align-middle text-base -mt-px">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</div>
</div>
<!-- tables -->
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
<tables-chapters-table v-if="numChapters" :library-item="libraryItem" @playAtTimestamp="playAtTimestamp" />
<tables-tracks-table v-if="numTracks" :tracks="tracks" :library-item-id="libraryItemId" />
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" />
</div>
</div>
<!-- modals -->
<modals-item-more-menu-modal v-model="showMoreMenu" :library-item="libraryItem" :rss-feed="rssFeed" :processing.sync="processing" />
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
<modals-fullscreen-cover v-model="showFullscreenCover" :library-item="libraryItem" />
<div v-show="processing" class="fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black/50 z-50">
<ui-loading-indicator />
</div>
@@ -201,7 +212,7 @@ export default {
showSelectLocalFolder: false,
showMoreMenu: false,
showFullscreenCover: false,
coverRgb: 'rgb(55, 56, 56)',
coverRgb: null,
coverBgIsLight: false,
windowWidth: 0,
descriptionClamped: false,
@@ -297,6 +308,9 @@ export default {
media() {
return this.libraryItem.media || {}
},
tags() {
return this.media.tags || []
},
mediaMetadata() {
return this.media.metadata || {}
},
@@ -464,6 +478,9 @@ export default {
if (width * this.bookCoverAspectRatio > 325) width = 325 / this.bookCoverAspectRatio
return width
},
coverHeight() {
return this.coverWidth * this.bookCoverAspectRatio
}
},
methods: {
@@ -722,6 +739,10 @@ export default {
this.$socket.$on('rss_feed_closed', this.rssFeedClosed)
this.checkDescriptionClamped()
// Set height of page below cover image
const itemPageBgGradientHeight = window.outerHeight - 64 - this.coverHeight
document.documentElement.style.setProperty('--item-page-bg-gradient-height', itemPageBgGradientHeight + 'px')
// Set last scroll position if was set for this item
if (this.$store.state.lastItemScrollData.id === this.libraryItemId && window['item-page']) {
window['item-page'].scrollTop = this.$store.state.lastItemScrollData.scrollTop || 0
@@ -744,6 +765,15 @@ export default {
</script>
<style>
:root {
--item-page-bg-gradient-height: 100%;
}
#item-page-bg-gradient {
transition: opacity 0.5s ease-in-out;
height: var(--item-page-bg-gradient-height);
}
.title-container {
width: calc(100% - 64px);
max-width: calc(100% - 64px);
+14 -2
View File
@@ -1,5 +1,8 @@
<template>
<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">
{{ $strings.HeaderYourStats }}
</h1>
@@ -50,6 +53,9 @@
</template>
</div>
</div>
<!-- Year in review banner shown at the bottom Feb - Nov -->
<stats-year-in-review-banner v-if="!showYearInReviewBanner" />
</div>
</template>
@@ -58,7 +64,8 @@ export default {
data() {
return {
listeningStats: null,
windowWidth: 0
windowWidth: 0,
showYearInReviewBanner: false
}
},
watch: {
@@ -103,7 +110,12 @@ export default {
console.error('Failed to load listening sesions', err)
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() {
+6 -1
View File
@@ -141,6 +141,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
if (this.player) this.player.playbackRate = value
}
// PluginMethod
setChapterTrack({ enabled }) {
this.useChapterTrack = enabled
}
// PluginMethod
async getCurrentTime() {
return {
@@ -262,4 +267,4 @@ export { AbsAudioPlayer }
export default ({ app, store }, inject) => {
$axios = app.$axios
vuexStore = store
}
}
+11 -6
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'}`
}
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = true) => {
if (isNaN(seconds) || seconds === null) return ''
seconds = Math.round(seconds)
var minutes = Math.floor(seconds / 60)
let minutes = Math.floor(seconds / 60)
seconds -= minutes * 60
var hours = Math.floor(minutes / 60)
let hours = Math.floor(minutes / 60)
minutes -= hours * 60
var days = 0
let days = 0
if (useDays || Math.floor(hours / 24) >= 100) {
days = Math.floor(hours / 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 (hours) strs.push(`${hours}h`)
if (minutes) strs.push(`${minutes}m`)
if (seconds) strs.push(`${seconds}s`)
if (seconds && showSeconds) strs.push(`${seconds}s`)
return strs.join(' ')
}
+1 -1
View File
@@ -184,4 +184,4 @@ class LocalStorage {
export default ({ app, store }, inject) => {
inject('localStore', new LocalStorage(store))
}
}
+3 -3
View File
@@ -64,15 +64,15 @@ export const getters = {
return state.serverSettings[key]
},
getJumpForwardTime: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return 10
if (!state.deviceData?.deviceSettings) return 10
return state.deviceData.deviceSettings.jumpForwardTime || 10
},
getJumpBackwardsTime: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return 10
if (!state.deviceData?.deviceSettings) return 10
return state.deviceData.deviceSettings.jumpBackwardsTime || 10
},
getAltViewEnabled: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return false
if (!state.deviceData?.deviceSettings) return true
return state.deviceData.deviceSettings.enableAltView
},
getOrientationLockSetting: state => {
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Popis",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Jazyk",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Interpret",
"LabelNarrators": "Interpreti",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Nedokončeno",
"LabelNotStarted": "Nezahájeno",
@@ -182,6 +187,7 @@
"LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání",
"LabelRead": "Číst",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Beskrivelse",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Sprog",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Linjeafstand",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Fortæller",
"LabelNarrators": "Fortællere",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Ikke færdig",
"LabelNotStarted": "Ikke påbegyndt",
@@ -182,6 +187,7 @@
"LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår",
"LabelRead": "Læst",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
+14 -8
View File
@@ -104,15 +104,16 @@
"LabelContinueBooks": "Bücher fortfahren",
"LabelContinueEpisodes": "Episoden fortfahren",
"LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Serie fortsetzen",
"LabelCustomTime": "Benutzerdefinierte zeit",
"LabelCustomTime": "Benutzerdefinierte Zeit",
"LabelDescription": "Beschreibung",
"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.",
"LabelDisableAutoRewind": "Deaktiviere automatisches Rückspulen",
"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.",
"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.",
"LabelDiscover": "Entdecken",
"LabelDownload": "Herunterladen",
@@ -121,7 +122,7 @@
"LabelEbook": "E-Book",
"LabelEbooks": "Ebooks",
"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.",
"LabelEnd": "Ende",
"LabelEndOfChapter": "Kapitelende",
@@ -140,7 +141,7 @@
"LabelHapticFeedback": "Haptische Rückmeldung",
"LabelHasEbook": "Hat Ebook",
"LabelHasSupplementaryEbook": "Hat zusätzliches Ebook",
"LabelHeavy": "Schwer",
"LabelHeavy": "Stark",
"LabelHigh": "Hoch",
"LabelHost": "Host",
"LabelIncomplete": "Unvollständig",
@@ -149,14 +150,17 @@
"LabelJumpBackwardsTime": "Rückspulzeit",
"LabelJumpForwardsTime": "Vorwärtsspulzeit",
"LabelLanguage": "Sprache",
"LabelLight": "Licht",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Leicht",
"LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören",
"LabelLocalBooks": "Lokale Bücher",
"LabelLocalPodcasts": "Lokale Podcasts",
"LabelLockOrientation": "Ausrichtung sperren",
"LabelLockOrientation": "automatische Bildschirmausrichtung sperren",
"LabelLockPlayer": "Mediaplayer sperren",
"LabelLow": "Niedrig",
"LabelLow": "Wenig",
"LabelMediaType": "Medientyp",
"LabelMedium": "Mittel",
"LabelMore": "Mehr",
@@ -165,6 +169,7 @@
"LabelNarrator": "Erzähler",
"LabelNarrators": "Erzähler",
"LabelNewestAuthors": "Neueste Autoren",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "Nein",
"LabelNotFinished": "nicht beendet",
"LabelNotStarted": "Nicht begonnen",
@@ -182,6 +187,7 @@
"LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr",
"LabelRead": "Lesen",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRecentSeries": "Aktuelle Serien",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
@@ -282,4 +288,4 @@
"ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen"
}
}
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Description",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Language",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrator",
"LabelNarrators": "Narrators",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Not Finished",
"LabelNotStarted": "Not Started",
@@ -182,6 +187,7 @@
"LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Descripción",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Lenguaje",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Interlineado",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrador",
"LabelNarrators": "Narradores",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "No Terminado",
"LabelNotStarted": "Sin Iniciar",
@@ -182,6 +187,7 @@
"LabelPubDate": "Fecha de Publicación",
"LabelPublishYear": "Año de Publicación",
"LabelRead": "Leído",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Description",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Langue",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Interligne",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrateur",
"LabelNarrators": "Narrateurs",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Non terminé(e)",
"LabelNotStarted": "Non Démarré(e)",
@@ -182,6 +187,7 @@
"LabelPubDate": "Date de publication",
"LabelPublishYear": "Année d’édition",
"LabelRead": "Lire",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Description",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Language",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrator",
"LabelNarrators": "Narrators",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Not Finished",
"LabelNotStarted": "Not Started",
@@ -182,6 +187,7 @@
"LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Description",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Language",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrator",
"LabelNarrators": "Narrators",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Not Finished",
"LabelNotStarted": "Not Started",
@@ -182,6 +187,7 @@
"LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Opis",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Jezik",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrator",
"LabelNarrators": "Naratori",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Nedovršeno",
"LabelNotStarted": "Not Started",
@@ -182,6 +187,7 @@
"LabelPubDate": "Datam izdavanja",
"LabelPublishYear": "Godina izdavanja",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
+7 -1
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Descrizione",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Lingua",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narratore",
"LabelNarrators": "Narratori",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Da Completare",
"LabelNotStarted": "Non iniziato",
@@ -182,6 +187,7 @@
"LabelPubDate": "Data Pubblicazione",
"LabelPublishYear": "Anno Pubblicazione",
"LabelRead": "Leggi",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Email del proprietario personalizzato",
@@ -282,4 +288,4 @@
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed chiuso"
}
}
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Aprašymas",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Kalba",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Tarpas tarp eilučių",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Skaitytojas",
"LabelNarrators": "Skaitytojai",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Nebaigta",
"LabelNotStarted": "Nepasileista",
@@ -182,6 +187,7 @@
"LabelPubDate": "Publikavimo data",
"LabelPublishYear": "Leidimo metai",
"LabelRead": "Skaityta",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Pasirinktinis savininko el. paštas",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Beschrijving",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Taal",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Regelruimte",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Verteller",
"LabelNarrators": "Vertellers",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Niet Voltooid",
"LabelNotStarted": "Niet Gestart",
@@ -182,6 +187,7 @@
"LabelPubDate": "Publicatiedatum",
"LabelPublishYear": "Jaar van uitgave",
"LabelRead": "Lees",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Beskrivelse",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Språk",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Linjemellomrom",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Forteller",
"LabelNarrators": "Fortellere",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Ikke fullført",
"LabelNotStarted": "Ikke startet",
@@ -182,6 +187,7 @@
"LabelPubDate": "Publiseringsdato",
"LabelPublishYear": "Publikasjonsår",
"LabelRead": "Les",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier Epost",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Opis",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Język",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Narrator",
"LabelNarrators": "Lektorzy",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Nieukończone",
"LabelNotStarted": "Nie rozpoęczto",
@@ -182,6 +187,7 @@
"LabelPubDate": "Data publikacji",
"LabelPublishYear": "Rok publikacji",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Продолжить",
"LabelContinueEpisodes": "Продолжить эпизоды",
"LabelContinueListening": "Продолжить слушать",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Продолжить серии",
"LabelCustomTime": "Пользовательское время",
"LabelDescription": "Описание",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Перемотка назад",
"LabelJumpForwardsTime": "Перемотка вперед",
"LabelLanguage": "Язык",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Подсветка",
"LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Слушать снова",
@@ -165,6 +169,7 @@
"LabelNarrator": "Читает",
"LabelNarrators": "Чтецы",
"LabelNewestAuthors": "Новые авторы",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "Нет",
"LabelNotFinished": "Не завершено",
"LabelNotStarted": "Не запущено",
@@ -182,6 +187,7 @@
"LabelPubDate": "Дата публикации",
"LabelPublishYear": "Год публикации",
"LabelRead": "Читать",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Недавно добавленные",
"LabelRecentSeries": "Недавние серии",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
+6
View File
@@ -104,6 +104,7 @@
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCustomTime": "Custom time",
"LabelDescription": "Beskrivning",
@@ -149,6 +150,9 @@
"LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time",
"LabelLanguage": "Språk",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "Light",
"LabelLineSpacing": "Radavstånd",
"LabelListenAgain": "Listen Again",
@@ -165,6 +169,7 @@
"LabelNarrator": "Berättare",
"LabelNarrators": "Berättare",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No",
"LabelNotFinished": "Ej avslutad",
"LabelNotStarted": "Inte påbörjad",
@@ -182,6 +187,7 @@
"LabelPubDate": "Publiceringsdatum",
"LabelPublishYear": "Publiceringsår",
"LabelRead": "Läst",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
+16 -10
View File
@@ -43,7 +43,7 @@
"ButtonSendEbookToDevice": "将电子书发送到设备",
"ButtonSeries": "系列",
"ButtonSetTimer": "设置定时器",
"ButtonStream": "",
"ButtonStream": "串流播放",
"ButtonSubmit": "提交",
"ButtonSwitchServerUser": "切换服务器/用户",
"ButtonUserStats": "用户统计信息",
@@ -101,10 +101,11 @@
"LabelClosePlayer": "关闭播放器",
"LabelCollapseSeries": "折叠系列",
"LabelComplete": "已完成",
"LabelContinueBooks": "Continue Books",
"LabelContinueEpisodes": "Continue Episodes",
"LabelContinueListening": "Continue Listening",
"LabelContinueSeries": "Continue Series",
"LabelContinueBooks": "继续看书",
"LabelContinueEpisodes": "继续剧集",
"LabelContinueListening": "继续收听",
"LabelContinueReading": "继续阅读",
"LabelContinueSeries": "继续收听系列",
"LabelCustomTime": "自定义时间",
"LabelDescription": "描述",
"LabelDisableAudioFadeOut": "禁用音频淡出",
@@ -114,7 +115,7 @@
"LabelDisableShakeToResetHelp": "在计时器运行时摇晃设备,或者在计时器到期后2分钟内摇晃设备将重置睡眠计时器。启用此设置可禁用抖动以重置。",
"LabelDisableVibrateOnReset": "重置时禁用振动",
"LabelDisableVibrateOnResetHelp": "当睡眠计时器重置时,你的设备会振动。启用此设置以在睡眠计时器重置时不振动。",
"LabelDiscover": "Discover",
"LabelDiscover": "发现",
"LabelDownload": "下载",
"LabelDownloaded": "已下载",
"LabelDuration": "持续时间",
@@ -149,9 +150,12 @@
"LabelJumpBackwardsTime": "快退时间",
"LabelJumpForwardsTime": "快进时间",
"LabelLanguage": "语言",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page",
"LabelLight": "轻",
"LabelLineSpacing": "行间距",
"LabelListenAgain": "Listen Again",
"LabelListenAgain": "再次收听",
"LabelLocalBooks": "本地书籍",
"LabelLocalPodcasts": "本地播客",
"LabelLockOrientation": "锁定方向",
@@ -164,7 +168,8 @@
"LabelName": "名称",
"LabelNarrator": "演播者",
"LabelNarrators": "演播者",
"LabelNewestAuthors": "Newest Authors",
"LabelNewestAuthors": "最新作者",
"LabelNewestEpisodes": "最新剧集",
"LabelNo": "取消",
"LabelNotFinished": "未听完",
"LabelNotStarted": "未开始",
@@ -182,8 +187,9 @@
"LabelPubDate": "出版日期",
"LabelPublishYear": "发布年份",
"LabelRead": "阅读",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelReadAgain": "再次阅读",
"LabelRecentlyAdded": "最近添加",
"LabelRecentSeries": "最近添加系列",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
"LabelRSSFeedPreventIndexing": "防止索引",
+7 -11
View File
@@ -1,14 +1,13 @@
const defaultTheme = require('tailwindcss/defaultTheme')
module.exports = {
purge: {
safelist: [
'bg-success',
'bg-info',
'text-info'
]
},
darkMode: false,
content: [
'components/**/*.vue',
'layouts/**/*.vue',
'pages/**/*.vue',
'mixins/**/*.js',
'plugins/**/*.js'
],
theme: {
extend: {
screens: {
@@ -65,8 +64,5 @@ module.exports = {
}
}
},
variants: {
extend: {},
},
plugins: [],
}